目录

1.线程的状态

2.线程不安全的原因

2.1:原子性

2.2: 可见性

2.3:有序性

3.解决线程不安全问题

3.1:synchronized

3.1.1:互斥

3.1.2:可重入

3.2:volatile关键字

3.3:wait和notify

3.3.1:wait()方法

3.3.2:notify()

3.3.3notifyAll()方法

4.wait()和sleep()方法的对比(面试题)


前言:

我们如果要了解线程安全的话,首先要明白线程的状态,正如常言所说:知己知彼,方能,百战不殆。

1.线程的状态

1.new:创建了对象但未被调用start(),内核里还没有创建PCB。就好像安排了工作,还未开始行动。

2.RUNNABLE:可以工作的,又可以分成正在工作中(正在cpu上执行)和即将开始工作的。

3.TERMINATED:工作完成了,pcb执行完毕,但Thread对象还在。

我们常说阻塞状态,接下来,我们就要细说这个阻塞状态。

4.BLOCKED:排队等着其他事情的完成 ( 这是因为加锁的原因造成的。)

5.WAITING:排队等着其他事情的完成.(这是因为 wait ,join造成的)。

6.TIME_WAITING:排队等着其他事情的完成.(这是因为 sleep造成的)


2.线程不安全的原因

既然要谈到线程不安全的原因,我们首先要明白什么是线程安全,如果代码在多线程环境下运行的结果是符合我们预期的,即使在单线程环境应该的结果,则说这个程序是线程安全的。

2.1:原子性

原子性是指一个操作是不可中断的,那么全部执行成功,那么全部执行失败。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程中断。

class count{
    public int num=0;
        public void add () {
                num++; 
        }
}
public class ThreadDemo1 {
    //原子性
    public static void main(String[] args) {
        count count=new count();
        Thread t=new Thread(()->{
            for (int i = 0; i <1000 ; i++) {
                count.add();
            }
        });
        Thread t1=new Thread(()->{
            for (int i = 0; i <1000 ; i++) {
                count.add();
            }
        });
        t.start();
        t1.start();
        try {
            t.join();
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(count.num);
    }
}

看上面的代码,你会下意识认为count最后的值2000吗?这不是的哟,这是一个线程不安全的代码。我出来的结果是1515但每一次的结果都不一样。这是为啥勒?

Java线程不安全 demo java线程不安全的情况_java

Java线程不安全 demo java线程不安全的情况_Java线程不安全 demo_02


2.2: 可见性

一个线程修改了公共变量的值,其他线程使用这个公共变量的时候,能够立即得到这个通知并修改了这个变量。

class  Counter{
    public int flag=0;
}
public class ThreadDemo2 {
    public static int num = 0;
    public static void main(String[] args) {
        Counter counter=new Counter();
        Thread t = new Thread(() -> {
           while(counter.flag==0){

           }
            System.out.println("循环结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scan=new Scanner(System.in);
            System.out.println("请输入一个数字来修改flag值");
            counter.flag=scan.nextInt();
        });
        t.start();
        t2.start();
    }
}

当t2线程修改了flag的值,首先是在自己的CPU内修改,之后再会修改内存中flag的值。之后t线程会因为编译器优化,认为flag的值不会轻易改变的,所以每次都看t线程的CPU中的flag值是否会发生变化,而不是比较内存中flag的值。编译器这种优化是为了提高速度。


2.3:有序性

有序性:代码重排序的本质就是编译器的优化。在单线程中,就是有可能发生顺序调整,但是多线程的代码执行复杂程度更高,编译器很难在编译阶段对代码执行效果进行预测。因此激进的重排序很容易导致优化后的逻辑和之前不一样。


3.解决线程不安全问题

3.1:synchronized

为了解决原子性问题,提出了synchronized(加锁)

3.1.1:互斥

synchronized会起到互斥效果,莫个线程执行到莫个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待

synchronized如果修饰的普通方法,锁对象就调用者本身。

Java线程不安全 demo java线程不安全的情况_Java线程不安全 demo_03

 如果修饰静态方法,锁对象就是这个类对象。

Java线程不安全 demo java线程不安全的情况_开发语言_04

 如果修饰的代码块,就是自己收到设置的锁对象。

Java线程不安全 demo java线程不安全的情况_java_05

 进入synchronized修饰的代码块,相当于加锁。

退出synchronized修饰的代码块,相当于解锁。

sychrnoized是不需要手动解锁的。


3.1.2:可重入

这个意思就是说一个线程对莫个对象或者方法加锁了,但还没有解锁,又一次进行加锁,不会死锁。

可以举一个例子,就是你给一个女生表白了,之后你们在一起了,但你再表白一次,不会有啥不好是的。

Java线程不安全 demo java线程不安全的情况_java_06


3.2:volatile关键字

volatile能保证内存可见性。

代码在写入volatile修饰的变量的时候:

会改变线程内存中volatile变量副本的值,将改变后的副本的值从该线程的cpu刷新到内存中。

代码在读入volatile修饰的变量的时候:

从内存中读取volatile变量的最新值到该线程的cpu中,从cpu中读取volatile变量的副本。

Java线程不安全 demo java线程不安全的情况_加锁_07

 这个代码就是解决上面可见性代码。


3.3:wait和notify

这个是解决代码的有序性。

3.3.1:wait()方法

1.使当前执行代码进行等待(把线程放到等待队列中)

2.释放当前的锁,进行阻塞等待。

3.满足一定条件时被唤醒,重新尝试获取锁

public static void main(String[] args) throws InterruptedException {
        Object lock=new Object();
        Thread t=new Thread(()->{
            synchronized (lock){
                System.out.println("我这个部分已经干完了");
                System.out.println("我只等你一个小时");
                try {
                    lock.wait(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        Thread.sleep(1000);//为了让t 线程先运行
        System.out.println("别急,我正在努力赶");
    }
}

wait(参数)是毫秒,wait等待时间超时的时候,它会自动结束等待条件。

还有一种让它就是结束等待,就是有其他线程调用该对象的notify()方法。

3.3.2:notify()

随机唤醒一个处于阻塞队列的线程(是使用同一个锁对象的线程)。

在notify()方法后,当前线程不会马上释放该对象的锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

public static void main(String[] args) throws InterruptedException {
        Object lock=new Object();
        Thread t1=new Thread(()->{
            synchronized (lock) {
                System.out.println("你下班了吗?你下班了,我就来上班");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("那我来上班了");
            }
        });
        Thread t2=new Thread(()->{
            synchronized (lock){
                System.out.println("我下班了");
                lock.notify();
            }
        });
        t1.start();
        Thread.sleep(200);
        t2.start();
    }

这里为啥要t1线程启动之后,等待200毫秒,再启动线程t2了。接下来到了故事小课堂。

wait()方法就相当于一个坐校车上学的学生。notify()方法就相当于校车。如果notify()方法先启动,wait()方法后启动,就相当于校车来了,发现这个没有学生到就走了。但等到学生到的时候,就木有校车来接她。她就会一直在那里等校车来。(这就相当于这个线程一直处于阻塞队列,一直等别人来唤醒。)这样会出现线程不安全。


3.3.3notifyAll()方法

使用notifyAll()方法可以一次唤醒所有等待的线程(使用同一个锁对象的线程)。

4.wait()和sleep()方法的对比(面试题)

1.wait是object的方法,sleep是Thread的静态方法。

2.调用sleep()方法的线程不会释放对象锁,而调用wait()方法会释放对象锁。

3.使用wait()不会抛出异常,但需要搭配synchronized来使用。而sleep()会抛出异常  InterruptedException。


总结:

以上就是我总结的线程不安全的原因以及解决办法,若有错误的地方,希望各位铁子留言纠错,若感觉不错,请一键三连。