(一).内存可见性(volatile)

1.概念

在多线程的场景下,一个线程读取,另一个线程修改,修改线程修改的值,但是并没有被读线程读到,此时就称这个问题为“内存可见性”问题。

2.示例

    public static int flag=0;
    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            while (flag==0){

            }
            System.out.println("thread1线程结束");
        });

        Thread thread2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入flag的值");
            flag=scanner.nextInt();
        });

        thread1.start();
        thread2.start();
    }

通过上面的代码和运行图片看到 ,即使我输入了1,但是thread1线程也没有结束,这又是为什么?

这是由于“编译器优化”在捣鬼,“编译器优化”就是 “编译器” / “JVM” 会将我们写的代码在逻辑不变的情况下,将代码进行调整。但是在多线程的程序中,编译器的判断会有错误,会导致优化后的逻辑与优化前的逻辑出现细节上的偏差。

对于 “flag==0”这个操作,类似于compare操作,但是在JVM中短时间会进行很多次这样的操作,首先,会先从内存中读入flag这个变量,即load,读取到寄存器,然后再进行cmp,对于load,是读内存的操作,cmp是纯cpu寄存器操作,所以load消耗的时间是cmp的几千倍

在执行了很多次这个循环之后,JVM发现flag一直是0,所以JVM就会觉得既然都是0,就没必要修改这么多次了,于是就将读内存的操作优化成了读寄存器的操作,即把内存中的值读取到了寄存器,后续再load的时候就不再重新读取内存,直接从寄存器中取。最终,等到用户真正输入新的值的时候,真正修改flag的时候,thread1已经感受不到了

优化

    public static int flag=0;

    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            while (flag==0){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread1线程结束");
        });

        Thread thread2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入flag的值");
            flag=scanner.nextInt();
        });

        thread1.start();
        thread2.start();
    }

当我在while()循环中加入了一个sleep(1)之后,程序运行起来的时候,发现thread1线程就结束了

本来一个循环一秒执行了几千万那次,但是加上sleep(1)之后,循环的次数就大大降低了,当引入sleep(1)这个操作,相对于load和cmp来说,消耗的时间又快了很多。假设读取flag的操作是1ns,通过之前的优化, 1ns  =>0.5ns,提升了50%,但是引入sleep之后,sleep直接占用1ms,所以优不优化就无所谓了

引入volatile关键字

    public volatile static int flag=0;

    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            while (flag==0){

            }
            System.out.println("thread1线程结束");
        });

        Thread thread2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入flag的值");
            flag=scanner.nextInt();
        });

        thread1.start();
        thread2.start();
    }

上述的代码,我在flag成员变量上加入了一个volatile关键字,通过volatile关键字来修饰flag,此时编译器在对这个变量的读取操作时,就不会优化成“读寄存器”,这样,thread2修改了,thread1就能及时看到了,从而避免了“内存可见性”的问题

注意:volatile关键字只能修饰变量,同时volatile关键字只能解决 “内存可见性”的问题,如果想要解决“原子性”的问题,还是需要加锁

3.JMM(Java内存模型)

谈到 “内存可见性”的问题,就不得不了解JMM了,我们上述说的读内存的操作优化成读寄存器的操作,在Java官方文档的术语中有原话:每个线程都有自己的“工作内存”,同时这些线程共享同一个“主内存”。当一个线程循环进行上述读取变量操作的时候,就会把“主内存”中的数据,拷贝到该线程的 “工作内存”中,后续另一个线程修改,也是先修改自己的“工作内存”,拷贝到“主内存”里,由于第一个线程仍然在读自己的“工作内存”,因此感知不到“主内存”的变化

注意:这里说的“工作内存”其实是指的“cpu寄存器”,“主内存”是指我们说的“内存”

(二).wait() 和 notify()

1.概念

wait(),等待。notify(),通知。这两个方法的作用就是协调线程之间的执行逻辑的顺序的。wait()和notify()方法可以让后执行的逻辑等待先执行的逻辑先跑。虽然无法直接干预调度器的调度顺序,但是可以让后执行的逻辑等待,等待到先执行的逻辑跑完了,通知一下当前的线程让他继续执行。

注意:wait()和notify()都是Object的方法,所以java中任意对象都提供了wait()和notify()方法

2.join(),“锁” 和 wait()的区别

①.join是等待另一个线程彻底执行完才能继续走

②.“锁”的等待是不受控制的,某个线程的代码执行到加锁,不一定触发等待,因为不确定其

     他的线程是否是“加锁”的状态

③.wait()是等到另一个线程执行notify()之后,才继续走,不需要另一个线程彻底执行完

3.线程饿死(线程饥饿)

站在操作系统的角度,操作系统的调度是随机的,其他线程都属于在锁上阻塞等待,对于当前释放锁的这个线程,是就绪状态,那么这个线程有很大的概率能再次获取到这把锁,从而导致后面的线程一直等待,无法获取到cpu资源,即“线程饿死”(汤老师ATM)

对于线程饿死,不是每个线程一直获取到这把锁,导致后面的线程饿死,可能获取到几十次之后,其他的线程有机会获取到当前这把锁,使用wait()和notify()的目的就是为了优化掉“获取几十次”这个操作,从避免程序效率变低。

4.示例1

    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        System.out.println("wait()之前");
        object.wait();
        System.out.println("wait()之后");
    }

当执行代码的时候,发现程序报错了,报的错误是“非法的锁状态”,那么这里就出问题了,我们又没有加锁,为什么会报一个“非法的锁状态”的错误?

这是因为wait()这个方法,会先释放锁,然后再进行阻塞等待。对于先释放锁的前提是,首先要加锁。所以回到上面的代码中,我们要先释放掉object对象对应的锁,但是释放锁的前提是,object对象要处于加锁的状态,由于object对象没有加锁,所以调用wait()方法的时候才会报错

对于wait()这个方法,最关键的一点就是要先释放锁,然后给其他线程获取锁的机会,既然要释放锁,首先要先加上锁才能谈释放


    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        System.out.println("wait()之前");
        synchronized (object){  // 重点:synchronized的锁对象要和调用wait()方法的对象一致
            //加上锁之后,代码进入wait之后就会先释放锁,并且阻塞等待,如果其他线程做完了必要的工作
            //调用notify()唤醒这个wait()线程,wait()线程就会解除阻塞,并且重新获取这把锁,继续执行并返回
            object.wait();
        }
        System.out.println("wait()之后");
    }

5.示例2

    public static void main(String[] args) {
        Object object=new Object();
        Thread thread1=new Thread(()->{
            System.out.println("wait之前");
            synchronized (object){
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("wait之后");
        });

        Thread thread2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("输入任意内容,唤醒thread1");
            scanner.next();
            synchronized (object){
                object.notify();
            }
        });
        
        thread1.start();
        thread2.start();
    }

这里我们要注意的点:

①.synchronized()的锁对象要和和调用wait(),notify()的方法的对象是一致的。

②.在thread2线程中,我们是通过Scanner来进行线程阻塞,等待用户控制台输入,只不过这里的阻塞是等待IO进入的阻塞,这样做的目的是确保notify()要在wait()之后

③.notify()和wait()一样,也要先拿到锁,再进行notify()这是Java给出的限制

6.示例3

如果多个线程在同一个对象上wait()那么notify()的时候是随机唤醒其中的一个线程

    public static void main(String[] args) {
        Object locker=new Object();
        Thread thread1=new Thread(()->{
            System.out.println("thread1 wait之前");
            synchronized (locker){
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread1 wait之后");
        });

        Thread thread2=new Thread(()->{
            System.out.println("thread2 wait之前");
            synchronized (locker){
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread2 wait之后");
        });

        Thread thread3=new Thread(()->{
            System.out.println("输入任意内容,唤醒任意一个线程");
            Scanner scanner=new Scanner(System.in);
            scanner.next();
            synchronized (locker){
                locker.notify();
            }
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }

通过上面的代码,我们可以知道,thread1线程和thread2线程都在wait(),然后thread3线程负责notify(),当我输入一个值的时候,发现只唤醒了两个线程中的随机一个线程,所以如果多个线程在同一个对象上wait()那么notify()的时候是随机唤醒其中的一个线程

解法方法:有多少个线程wait()就要有多少个notify()进行唤醒

    public static void main(String[] args) {
        Object locker=new Object();
        Thread thread1=new Thread(()->{
            System.out.println("thread1 wait之前");
            synchronized (locker){
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread1 wait之后");
        });

        Thread thread2=new Thread(()->{
            System.out.println("thread2 wait之前");
            synchronized (locker){
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread2 wait之后");
        });

        Thread thread3=new Thread(()->{
            System.out.println("输入任意内容,唤醒任意一个线程");
            Scanner scanner=new Scanner(System.in);
            scanner.next();
            synchronized (locker){
                locker.notify();
            }

            System.out.println("输入任意内容,唤醒另一个线程");
            scanner.next();
            synchronized (locker){
                locker.notify();
            }
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }

7.示例4

    public static void main(String[] args) {
        Object locker=new Object();
        Thread thread1=new Thread(()->{
            System.out.println("thread1 wait之前");
            synchronized (locker){
                try {
                    locker.wait(10000);  //wait()带参数版本,最多等10秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread1 wait之后");
        });

        Thread thread2=new Thread(()->{
            System.out.println("thread2 wait之前");
            synchronized (locker){
                try {
                    locker.wait();  //死等
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread2 wait之后");
        });

        Thread thread3=new Thread(()->{
            System.out.println("输入任意内容,唤醒任意所有线程");
            Scanner scanner=new Scanner(System.in);
            scanner.next();
            synchronized (locker){
                //一下子唤醒全部的线程
                //虽然同时唤醒了thread1和thread2,但是wait唤醒之后会重新加锁,其中某个线程先上了锁,开始执行
                //另一个线程因为加锁失败了,再次阻塞等待,等到先走的线程释放了锁,后走的线程才能加上锁,继续执行
                locker.notifyAll();
            }
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }

8.wait()和sleep()的相同点和不同点

相同点:

            ①.wait()有时间等待;sleep()也有时间等待

            ②.wait()可以使用notify()提前唤醒;sleep()可以通过interrupt提前唤醒,但是interrupt

            本身的作用是线程终止

不同点:

            ①.wait()必须搭配锁进行使用,先加锁,才可以使用;sleep()不需要搭配锁

            ②.如果都在synchronized内部使用,wait()会先释放锁;sleep()不会释放锁

9.作业题

    public static void main(String[] args) throws InterruptedException {
        Object locker1=new Object();
        Object locker2=new Object();
        Object locker3=new Object();

        Thread thread1=new Thread(()->{
            for (int i = 0; i < 10; i++) {
                synchronized (locker1){
                    try {
                        locker1.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print("A");
                synchronized (locker2){
                    locker2.notify();
                }
            }
        });
        Thread thread2=new Thread(()->{
            for (int i = 0; i < 10; i++) {
                synchronized (locker2){
                    try {
                        locker2.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print("B");
                synchronized (locker3){
                    locker3.notify();
                }
            }
        });
        Thread thread3=new Thread(()->{
            for (int i = 0; i < 10; i++) {
                synchronized (locker3){
                    try {
                        locker3.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("C");
                synchronized (locker1){
                    locker1.notify();
                }
            }
        });
        thread1.start();
        thread2.start();
        thread3.start();

        //主线程中,先通知一次 locker1 让上述的逻辑从thread1开始执行
        //需要确保到上述三个线程都执行到wait,再进行notify
        Thread.sleep(1000);
        synchronized (locker1){
            locker1.notify();
        }
    }
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐