目录

一、volatile关键字

1.1 volatile保证内存可见性

1.2 CPU层面

1.3 JAVA层面

1.4 volatile避免指令重排序

1.5 volatile总结

二、wait和notify

2.1 wait方法

2.2 notify方法

2.3 notifyAll方法

2.4 总结


一、volatile关键字

我们在之前的文章已经介绍了synchronized关键字用来保证“原子性”和间接保证“内存可见性”

JavaEE初阶——多线程(3)线程安全-CSDN博客https://blog.csdn.net/Yoko_999/article/details/153752962?spm=1001.2014.3001.5502

1.1 volatile保证内存可见性

创建两个线程,t1线程用flag==0为while循环的条件,t2改变flag的值,我们希望观察到的现象是当t2改变flag值之后t1和t2线程都结束

public class Demo_502 {
    // 定义退出标识
    static  int flag = 0;

    public static void main(String[] args) {

        // 创建执行任务的线程
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程启动.");
            while (flag == 0) {
                // 不停的循环. 处理任务

            }
            System.out.println(Thread.currentThread().getName() + "线程退出.");
        }, "t1");
        // 启动线程
        t1.start();

        // 输入停止标识
        Thread t2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程启动.");
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个非零的整数:");
            flag = scanner.nextInt();
            System.out.println(Thread.currentThread().getName() + "线程退出.");
        }, "t2");
        // 线程启动
        t2.start();
    }
}

结果显示,当t2改变了flag的值为1后,t1并没有感知到flag已经不为0,仍在循环,未退出线程,与我们的预想不一样,出现了线程不安全现象!这是因为内存不可见,t1线程没有感知到t2线程对变量的修改。

我们在指令层面分析一下t1线程的循环判断逻辑while(flag==0)

这条语句对应的指令是

LOAD:把flag的值从主内存加载到t1的工作内存

CMP:比较flag的值和0是否相等

但是对于t1来说,判断逻辑是比较flag的值,而不是像flag++这样的修改操作。所以之后的比较,CPU都会认为这个值永远不会改变,从而也不会从主内存中读取flag,自然也不会得到修改后的flag值。

public class Demo_502 {
    // 定义退出标识
    static volatile int flag = 0;
    public static void main(String[] args) {

        // 创建执行任务的线程
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程启动.");
            while (flag == 0) {
                // 不停的循环. 处理任务
            }
            System.out.println(Thread.currentThread().getName() + "线程退出.");
        }, "t1");
        // 启动线程
        t1.start();
        // 输入停止标识
        Thread t2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程启动.");
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个非零的整数:");
            flag = scanner.nextInt();
            System.out.println(Thread.currentThread().getName() + "线程退出.");
        }, "t2");
        // 线程
        t2.start();
    }
}

我们在flag前加上volatile关键词,此时再来观察运行结果,发现t1按预期退出了,这说明t1感知到了flag的更新

那volatile是如何实现这样的结果呢?

1.2 CPU层面

在CPU中,在缓存和主内存之间存在“MESI缓存一致性协议”,我们可以简单理解为一种通知机制

  • 当所有处理器没有修改共享变量时,各自处理器只读取自己缓存中的值,而不用去主内存读取,从而提高效率
  • 当某一个处理器往主内存中写回数据的时候,缓存中的值就会失效,通知其他处理器从主内存中重新获取新的值

这样就保证一个线程可以感知到另一个线程对共享变量的修改,从而保证内存可见性

1.3 JAVA层面

在Java中存在内存屏障,可以保证指令执行的先后顺序

我们以下面两个例子讲解内存屏障如何保证内存可见性

当加了volatile关键字的变量要进行修改时(volatile写)

  • StoreStore屏障会使之前的普通写的结果“提前暴露”出来,然后再进行volatile写操作,避免其他线程读到普通写的过期数据
  • StoreLoad屏障则会强制让后续普通读的操作等到volatile写的结果同步到主内存之后再进行

我们可以加多个内存屏障,当加了volatile关键字的变量要进行获取内存的值时(volatile读)

  • StoreLoad屏障会等待其他线程的写操作结果同步到内存之后再进行读取,避免读到过期数据
  • LoadLoad可以避免后续的普通读操作读到过期数据
  • LoadStore保证后续的写操作依赖的是volatile读的最新结果

所以内存屏障通过强制缓存同步,让线程感知到变量的改变,从而保证内存可见性

1.4 volatile避免指令重排序

还是同样依靠我们上文提到的内存屏障,规定volatile读写操作和其他读写操作的顺序,比如StoreLoad屏障保证必须进行写操作之后才可以进行读操作,有效的避免了指令的重排序

1.5 volatile总结

  • volatile保证了内存可见性
  • volatile实现了有序性避免了指令重排序
  • volatile不保证原子性
  • 多个线程之间涉及到修改共享变量的逻辑就只管加volatile
  • 因为不保证原子性,往往会和synchronized搭配使用

二、wait和notify

之前提到,线程之间是抢占性执行,所以多线程情况下线程执行的先后顺序难以预知。但是在实际开发中,我们有时候明确需要等待一个操作结束之后再执行另一个操作,所以我们需要协调线程之间的执行顺序

例如:

  • 篮球场每个球员都是线程,想要得分就要不同线程配合,扣篮的线程要等待传球的线程
  • 包子铺的顾客和老板是线程,买包子这个线程必须等到老板包好包子并且出锅这个线程

wait方法为等待线程,notify方法则为唤醒线程,我们还可以和之前提到的join()方法进行对比区分

我们还是以买包子为例:妈妈让我下楼买包子,我等包子出锅

join()方法属于Thread类,一般是针对主线程对子线程的等待:妈妈让我下楼买包子,妈妈发出的命令就是主线程,而我下楼买包子就是子线程。妈妈需要等我买包子这个线程运行结束才去做其他事情

wait()方法属于Object类,一般就不分主次线程之分:我买包子和包子出锅两个线程更适合抽象于对资源的等待,而不是对线程结束的等待。

  • 比如包子出锅这个线程会从6点一直运行到10点才结束,我在8点等待包子这个资源
  • 当包子没做好我就等待(判断是否有资源,没有就调用wait方法等待资源)
  • 等到8点10分包子出锅我买到了(当资源充足了,调用notify方法,唤醒了刚刚调用wait方法的线程,我可以买包子了)
  • 那么我买包子线程就结束了,而不是要一直等到包包子线程10点结束买包子的线程才结束。

wait方法和notify方法都一定要搭配synchronized一起使用

2.1 wait方法

调用wait的结果

  • 使当前执行代码的线程进行等待,也就是把当前线程放到等待队列
  • 释放当前锁
  • 满足一定条件之后被唤醒,重新尝试获取这个锁

wait结束等待的条件

  • 其他线程调用该对象的notify方法
  • wait等待时间超时(wait方法可以设定等待时间)
  • 其他线程调用该等待线程的interrupted方法,导致wait抛出异常

2.2 notify方法

notify用来唤醒处于等待状态的线程

  • 方法notify也要和synchronized结合使用,用来唤醒持有相同锁对象的等待线程,并使他们重新获取该对象的对象锁
  • 如果有多个线程等待,则有线程调度器随机挑选出一个wait状态的线程
  • 在notify方法之后,当前线程不会马上释放对象锁,要等到notify方法所在的的线程执行完全,也就是退出同步代码块之后才会释放对象锁
public class Demo_503 {
    public static void main(String[] args) {
        // 定义一个锁对象
        Object locker = new Object();
        
        // 创建调用wait() 的线程
        Thread t1 = new Thread(() -> {
            while (true) {


                synchronized (locker) {
                    System.out.println("调用wait()之前...");
                    // 执行线程的逻辑
                    // 如果没有满足线程所需要的数据,那么就等待
                    try {
                        locker.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("wait()唤醒之后..");
                    System.out.println("===================");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            while (true) {
                System.out.println("notify()之前...");
                // 同等待的锁对象进行唤醒
                synchronized (locker) {
                    locker.notify();
                }

                System.out.println("notify()之后...");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 启动线程
        t1.start();
        t2.start();
    }
}

我们能看到打印结果,调用wait之后t1进入等待状态,此时t2唤醒之后t1再继续执行

2.3 notifyAll方法

notify方法只是唤醒等待线程中的一个,而notifyAll方法可以一次性唤醒所有的等待线程

public class Demo_05 {
    static class WaitTask implements Runnable {

        private Object locker;

        public WaitTask(Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker) {
                while (true) {
                    try {
                        System.out.println("wait 开始");
                        locker.wait();
                        System.out.println("wait 结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    static class NotifyTask implements Runnable {
        // 代码不变
        private Object locker;

        public NotifyTask(Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker) {
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notify 结束");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(new WaitTask(locker));
        Thread t3 = new Thread(new WaitTask(locker));
        Thread t4 = new Thread(new WaitTask(locker));
        Thread t2 = new Thread(new NotifyTask(locker));
        t1.start();
        t3.start();
        t4.start();
        Thread.sleep(1000);
        t2.start();
    }
}

我们使用notify唤醒线程,观察结果notify结束后能看到只有一个线程被唤醒后打印wait结束,继续循环打印wait开始继续等待

public class Demo_05 {
    static class WaitTask implements Runnable {

        private Object locker;

        public WaitTask(Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker) {
                while (true) {
                    try {
                        System.out.println("wait 开始");
                        locker.wait();
                        System.out.println("wait 结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    static class NotifyTask implements Runnable {
        // 代码不变
        private Object locker;

        public NotifyTask(Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker) {
                System.out.println("notifyAll 开始");
                locker.notifyAll();
                System.out.println("notifyAll 结束");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(new WaitTask(locker));
        Thread t3 = new Thread(new WaitTask(locker));
        Thread t4 = new Thread(new WaitTask(locker));
        Thread t2 = new Thread(new NotifyTask(locker));
        t1.start();
        t3.start();
        t4.start();
        Thread.sleep(1000);
        t2.start();
    }
}

我们把notify替换成notifyAll,观察结果显示notifyAll调用后,三个等待线程都被唤醒,之后这三个线程重新竞争锁

2.4 总结

wait和notify必须配置synchronized一起使用,并且使用同一个锁对象

public class Demo_502 {
    public static void main(String[] args) throws InterruptedException {
        Object locker=new Object();
        synchronized(locker){
            locker.wait();
        }
    }
}
public class Demo_502 {
    public static void main(String[] args) throws InterruptedException {
        Object locker=new Object();
        synchronized(locker){
            locker.notify();
        }
    }
}

synchronized获取的锁对象,和调用wait和notify方法的对象要相同

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐