线程学习(3)-volatile关键字,wait/notify的使用

简介: 线程学习(3)-volatile关键字,wait/notify的使用

💕"命由我作,福自己求"💕

作者:Mylvzi

文章主要内容:线程学习(2)

一.volatile关键字

volatile关键字是多线程编程中一个非常重要的概念,它主要有两个功能:保证内存可见性,和禁止指令重排序

1.内存可见性

内存可见性(Memory Visibility) 指的是在多线程编程环境下,一个线程修改共享变量,其他线程能够立即看到共享变量修改后的值

先来了解一个底层知识,我们写的代码是要读取数据的,最常见的数据就是我们定义的变量,变量被保存在内存之中,系统要使用数据需要cpu进行读内存(load)的操作,而load这个操作对于cpu来说是一个非常慢的数据,因为cpu的速度是很快的,读内存这个操作比读寄存器要慢上几千倍,所以load对于cpu来说是一个很大的开销

// 设计一个标志位
private static int isQuit = 0;
Thread t1 = new Thread(() -> {
  while(isQuit == 0) {
  // ...... 
  }
  System.out.println("t1线程结束");
});
// 在t2线程中修改isQuit的值
Thread t2 = new Thread(() -> {
  System.out.println("请输入isQuit的值:");
  Scanner scan = new Scanner(System.in);
  isQuit = scan.nextInt();
});
t1.start();
t2.start();

结果截图:

在t2中将isQUit设置为1,按理说t1中的循环条件已经不满足了啊,整个进程应该会终止才对,但是什么都没有打印,这是为什么呢?这其实就和我们上面说的读内存对cpu开销大这一事实有关

先说结论,为了解决读内存的问题,并提高效率,编译器会对读取数据进行一些优化,减少读内存的次数,尽可能多的直接从cpu上读取,来提高效率

知道了这个结论,就很容易解释上述问题了:

编译器是好心的,但是他为了提高效率却忽视了准确性,而这个准确性对我们来说是很重要的(这涉及到整个进程的执行),不能忽视。这也属于一个编译器的bug

而关键字volatile就是用于解决这个问题,被volatile修饰的变量,编译器不会对其执行上述的优化,也就是告诉编译器,我这个变量很重要,不要为了效率就忽视准确性

当我们在t2线程中修改变量isQuit时,实际上是内存中的isQuit发生了改变,其他线程(t1)能够立即看到这个改变后的值,循环终止,这就是volatile关键字的内存可见性功能,它保证了共享变量在所有线程中的公开,透明,其他线程能够立即看到共享变量的改变,及时做出调整。

在输入1之后,t1线程结束循环,立即终止

内存可见性也是线程安全问题的一种,之前学习过的一个线程安全问题是两个线程同时针对同一个变量进行修改,但实际上两个线程,一个线程修改变量,一个线程读取变量可能也会发生线程安全问题

再补充一点,编译器之所以进行优化是因为短时间内大量的load操作.如果我们放慢/减少 load,编译器就不会进行优化,最常见的方式就是添加sleep

Thread t1 = new Thread(() -> {
            while(isQuit == 0) {
                // 让线程休眠一会儿
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // ......
            }
            System.out.println("t1线程结束");
        });

运行结果:

由于t1内部让线程短暂阻塞了一会儿,load操作执行的次数就会大大减少,此时编译器就没有进行优化的必要,所以每次读取都是读内存.

补充:

volatile的本意是不稳定的,易挥发的,使用它修饰一个变量相当于告诉编译器"喂(#`O′),我这个变量是不稳定的啊,你不要把他给我加载到工作内存(cpu寄存器)中,让他老老实实的代码主内存中就行",这样编译器就不会进行优化了

2.指令重排序

指令重排序也是编译器优化的一种,它是指在保证执行结果正确的前提下,对指令的执行顺序进行调整,达到效率提高的目的,比如去菜市场买菜,我只要最后买到我要买的菜即可,我按照什么路径去买无所谓,但是我追求最快买完

在单线程模式下,指令重排序不会带来问题,反而还是一个好处.但是在多线程下,由于线程的调度是随机的,就有可能带来意想不到的 结果,所以我们需要禁止指令的重排序,往往是对可能涉及到指令重排序对象进行volatile的修饰,就能让编译器不对其进行优化

关于指令重排序 的具体应用会在后面的单例模式部分讲到

二.wait/notify

1.引言

在多线程编程中,我们经常要协调线程的执行顺序来实现一些场景需求,比如之前学习过的join()方法,他可以控制线程的结束顺序,也是协调线程的一种方式

但是有些时候我们想线程不结束,也能控制他们的执行顺序.而不是只能等到一个线程结束再去执行其他线程,此时,就可以使用wait/notify来实现上述需求

2.wait方法的使用

wait和notify方法都是属于Object类的方法,需要通过实例化一个object对象来进行调用

wait方法的具体执行过程分为三步:

  1. 释放当前对象的锁
  2. 阻塞等待
  3. 等待对象使用notify方法唤醒

wait方法使用的过程中有一个最常见的错误就是调用wait方法的对象事先并没有加锁,直接wait会报错

public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        System.out.println("wait 开始");
        locker.wait();
        System.out.println("wait 结束");
    }

运行结果:

这里显示**"非法监视器状态异常"**,监视器 是什么?我们之前学习过synchronized,他是用于给对象加锁的,他其实还有另一个名字,叫做 监视器锁.

更本质的说,监视器(monitor)其实是一种机制,每个对象都会关联一个监视器,用于实现同步,同步就是保证多个线程对于共享变量的使用是合理的,是没有线程安全问题的(比如使用加锁来实现同步,可以避免多个线程针对同一变量进行修改这种线程安全问题).我想,这里的同步的意思是指在多线程编程中,每个线程获取到的共享资源的状态是同步的,不会出现一个线程修改了共享变量,另一个线程却还在使用修改之前的变量.

synchronized被称为监视器锁,是因为它本质上是获取到了对象关联监视器的锁,他保证了同一时间只能有一个线程访问进入synchronized修饰的代码块/方法,保证了线程安全.

回到本文,由于创建的locker对象事先并没有加锁,与其关联的监视器的并没有上锁,没有上锁你却想先释放锁,这不是bug吗,所以会报出非法监视器状态异常,为了解决这个异常,我们需要先对locker对象进行加锁

public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        synchronized(locker) {
            System.out.println("wait 开始");
          locker.wait();
          System.out.println("wait 结束");
    }
    }

运行结果

注:wait方法也可以带参数,和带参数的join方法一样,可以设置等待的时间

3.notify方法的使用

wait方法会先释放调用对象的锁,然后使调用的线程处于阻塞状态,直到对象使用notify进行唤醒,notify在使用的时候也需要先获取到与对象关联的锁,否则也会抛出非法监视器状态异常,这样做也是为了保证线程安全

示例代码:

public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(() ->{
            synchronized (locker) {
                System.out.println("wait 开始");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("wait 结束");
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker) {
                System.out.println("使用notify 方法 进行唤醒");
                locker.notify();
            }
        });
        t1.start();
        t2.start();
    }

运行结果:

除了使用notify,我们还可以使用notifyAll方法来一次性唤醒当前对象所有wait的线程,当然看似是一次性,实际上还是一个一个进行唤醒的,如果一次性唤醒全部,会导致锁冲突,出现线程不安全问题

今天的学习就到这里,下期预告<<多线程设计模式讲解(1)>>

目录
相关文章
|
9月前
|
Java 调度 开发者
Java线程池ExecutorService学习和使用
通过学习和使用Java中的 `ExecutorService`,可以显著提升并发编程的效率和代码的可维护性。合理配置线程池参数,结合实际应用场景,可以实现高效、可靠的并发处理。希望本文提供的示例和思路能够帮助开发者深入理解并应用 `ExecutorService`,实现更高效的并发程序。
206 10
|
10月前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
211 7
|
11月前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
208 9
|
11月前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
111 3
|
10月前
|
Java 调度
|
11月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
124 4
|
12月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
155 1
|
12月前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
103 1
|
3月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
145 0
|
3月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。