【JavaEE初阶】 volatile关键字 与 wait()方法和notify()方法详解

简介: 【JavaEE初阶】 volatile关键字 与 wait()方法和notify()方法详解


本文内容重点:

  • 了解并掌握volatile关键字的用法
  • 了解并掌握wait方法的使用
  • 明白wait与notify之间的联系
  • 知道wait 和 sleep 的区别

🌲volatile 关键字

首先我们知道,volatile是一个关键字,那么它有什么作用?又具有那些特性呢?请看下文

🚩volatile能保证内存可见性

volatile修饰的变量, 能够保证 “内存可见性”.

这是什么意思呢?

首先我们先来看一下下面这个例子:

  • 创建两个线程 t1 和 t2
  • t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
  • t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
  • 预期当用户输入非 0 的值的时候, t1 线程结束
class Conter{
    public int flag = 0;
}
public class TestMain {
    public static void main(String[] args) {
        Conter conter = new Conter();
        Thread thread1 = new Thread(() -> {
           while (conter.flag == 0) {
                //一系列操作
           }
        });
        thread1.start();
        Thread thread2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入:");
            conter.flag = scanner.nextInt();
        });
        thread2.start();
    }
}

但是当我们运行后我们会发现:

程序并没有跟我们想象中一样停止

这是什么原因呢?

其实这就与我们的内存有关了,这里博主暂时将内存分为两大类,主内存和工作内存

主内存一般是存储数据,工作内存做运算

而我们上述代码的 flag==0 的判断也需要经历两步

  • 第一步:从主内存上读取flag的值到工作内存中
  • 第二步:在工作内存中进行判断

由于第一步的执行时间相较于第二步太慢了

所以在编译时编译器会对该变量进行判断,若该变量后面不会进行修改,那么编译器就会自动进行优化,优化内容为

  • 只读取一次,以此提高运行速度

这就导致上述的flag一直保持这0,并没有读取新的flag

当然这属于编译器的误判,也不是100%会导致上述情况

比如我们对上述代码加上一个操作,我们在while循环里加上一个sleep的操作,降低了循环速度,这时候我们发现刚刚的误判消失了。

但是这种方法终究是奇技淫巧,而且也不是100%不会误判

所以我们面对这种情况Java引入里voltie关键字

volatile修饰的变量,不会被编译器优化,每一次都读取,这就保证了内存的可见性

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

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候,

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

接下来我们用volatile关键字对代码进行修改,我们再看看效果,代码如下:

class Conter{
    public volatile int flag = 0;
}
public class TestMain {
    public static void main(String[] args) {
        Conter conter = new Conter();
        Thread thread1 = new Thread(() -> {
           while (conter.flag == 0) {
                //一系列操作
           }
        });
        thread1.start();
        Thread thread2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入:");
            conter.flag = scanner.nextInt();
        });
        thread2.start();
    }
}

结果为:

结果如我们所料

注意:我们使用vola关键字使得运算更加准确了,但是由于每次都会读取,就会降低我们的效率,所以具体情况具体使用,不要滥用

🚩volatile 不保证原子性

注意:volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

比如我们一下例子:

这个是最初的演示线程安全的代码.

  • 给 increase 方法去掉 synchronized
  • 给 count 加上 volatile 关键字.
class Count {
  //加入关键字
    public volatile int count = 0;
    //加锁
    // synchronized void increase() {
    //    count++;
    //}
    void increase() {
      count++;
    }
}
public class Counter {
    public static void main(String[] args) throws InterruptedException {
        final Count count = new Count();
        //搞两个线程,分别对count进行++操作,每一个线程加50000次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.count);
    }
}

我们会发现结果最终 count 的值仍然无法保证是 100000,依然存在线程不安全的问题

所以也就说明了volatile不保证原子性

🎋wait 和 notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.

但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序

就比如:

球场上的每个运动员都是独立的 “执行流” , 可以认为是一个 “线程”. 而完成一个具体的进攻得分动作, 则需要多个运动员相互配合,按照一定的顺序执行一定的动作, 线 程1 先 “传球” , 线程2 才能 "扣篮

完成这个协调工作, 主要涉及到三个方法

  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程

注意: wait, notify, notifyAll 都是 Object 类的方法.

所以任何类都可以使用这三个方法

🚩wait()方法

wait 做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒, 重新尝试获取这个锁

注意:wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 ( wait(long timeout) 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出InterruptedException 异常

代码示例: 观察wait()方法使用

public static void main(String[] args) throws InterruptedException {
  Object object = new Object();
  synchronized (object) {
  System.out.println("等待中");
  object.wait();
  System.out.println("等待结束");
}

这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()

🚩notify()方法

notify 方法是唤醒等待的线程.

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁

代码示例: 使用notify()方法唤醒线程

  • 创建 thread1线程,里面调用一次wait
  • 创建 thread2线程,里面调用一次notify
  • 注意,thread1 和 thread2 内部持有同一个 Object locke(同一个锁)thread1和thread2 要想配合就需要搭配同一个 Object(同一个对象)

代码如下:

注意:notify方法要在wait方法后才会起作用,所以下面代码里,博主在notify前面加了sleep,确保threa1线程的wait先执行

public class WaitAndNotify {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread thread1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("wait之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("wait之后");
            }
        });
        thread1.start();
        Thread thread2 = new Thread(() -> {
            synchronized (locker) {
               try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("notify之前");
                locker.notify();
                System.out.println("notify之后");
            }
        });
        thread2.start();
    }
}

运行结果如下:

🚩notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程

代码实例:

使用notifyAll()方法唤醒所有等待线程,

  • 创建 3 个 线程调用wait. 1 个 线程例调用notifyAll

代码如下:

注意:notifyAll方法要在wait方法后才会起作用,所以下面代码里,博主在notifyAll前面加了sleep,确保调用wait的线程先调用wait

public class WaitAndNotifyAll {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread thread1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("thread1:wait之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("thread1:wait之后");
            }
        });
        thread1.start();
        Thread thread2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("thread2:wait之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("thread2:wait之后");
            }
        });
        thread2.start();
        Thread thread3 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("thread3:wait之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("thread3:wait之后");
            }
        });
        thread3.start();
        Thread thread4 = new Thread(() -> {
            synchronized (locker) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("notify之前");
                locker.notifyAll();
                System.out.println("notify之后");
            }
        });
        thread4.start();
    }
}

我们来看一下调用结果:

此时可以看到, 调用 notifyAll 能同时唤醒 3 个wait 中的线程

注意: 虽然是同时唤醒 3 个线程, 但是这 3 个线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行,顺序随机,抢占式执行

🚩理解notify 和 notifyAll

notify 只唤醒等待队列中的一个线程. 其他线程还是乖乖等着

notifyAll 一下全都唤醒, 需要这些线程重新竞争锁

🌳wait 和 sleep 的对比

其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间

不同点在于:

  1. wait 需要搭配 synchronized 使用. sleep 不需要.
  2. wait 是 Object 的方法 sleep 是 Thread 的静态方法

⭕总结

关于《【JavaEE初阶】 volatile关键字 与 wait()方法和notify()方法详解》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!

相关文章
|
3月前
|
设计模式 安全 编译器
线程学习(3)-volatile关键字,wait/notify的使用
线程学习(3)-volatile关键字,wait/notify的使用
26 0
|
4月前
|
Java
9.synchronized 是个啥东西?应该怎么使用?
9.synchronized 是个啥东西?应该怎么使用?
28 0
9.synchronized 是个啥东西?应该怎么使用?
|
4月前
|
Java 调度
【多线程】Thread类的基本用法
【多线程】Thread类的基本用法
【多线程】Thread类的基本用法
|
8月前
|
存储 Java
|
8月前
|
安全
synchronized关键字 - - 三种使用方法
synchronized关键字 的 三种使用方法: 第一种 synchronized(对象) { 临界区 } 第二种 加在 非静态方法 上 第三种 加在 静态方法 上
43 0
|
8月前
|
程序员 调度
多线程之Thread 类的基本用法
多线程之Thread 类的基本用法
|
9月前
|
Java
synchronized关键字的底层原理你了解吗?
synchronized关键字是Java中用于实现线程同步的关键字,它可以用于修饰方法或代码块。synchronized关键字的底层原理涉及到Java内存模型和对象监视器的概念。
65 0
|
安全 Java 编译器
【JavaEE】synchronized监视锁 volatile关键字wait和notify
【JavaEE】synchronized监视锁 volatile关键字wait和notify
【JavaEE】synchronized监视锁 volatile关键字wait和notify
|
存储 消息中间件 缓存
Android高级:内部类的理解,多态,run和start,wait和seelp,线程安全,堆和栈,synchronized 和volatile ,AsyncT
Android高级:内部类的理解,多态,run和start,wait和seelp,线程安全,堆和栈,synchronized 和volatile ,AsyncT
120 0
|
小程序 调度
一文掌握多线程并发中 Thread 类 yield 方法具体作用
一文掌握多线程并发中 Thread 类 yield 方法具体作用
287 0
一文掌握多线程并发中 Thread 类 yield 方法具体作用