我的编程空间,编程开发者的网络收藏夹
学习永远不晚

JavaEE中volatile、wait和notify详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

JavaEE中volatile、wait和notify详解

一.volatile 关键字. 

1.volatile 能保证内存可见性问题

什么是内存可见性?

可见性指 , 一个线程对内存的修改 , 能够及时的被其他线程看到.

Java内存模型(JMM):Java虚拟机规范中定义了Java内存模型 , 目的是屏蔽一切硬件和操作系统的内存访问差异 , 以实现Java程序在各种平台下都能达到一致的并发效果.

  • 线程之间的共享变量存在主内存(Main Memory)
  • 每一个线程都有自己的"工作内存"(寄存器)
  • 当线程要读取一个共享变量时 , 会把共享变量从主内存拷贝到工作内存, 再从工作内存中读取数据.
  • 当线程要修改共享变量时 , 也先修改工作内存中的副本 , 最后同步到主内存中.

由于每个线程都有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的副本 , 此时修改线程 t1 的工作内存中的值 , 线程 t2 的工作内存不一定及时发生变化.这时代码就容易发生问题.

此时引出两个问题:

  • 为什么要这么多内存
  • 为什么要拷贝多次

1) 为什么要这么多内存?

实际并没有这么多的内存 , 这只是Java规范中的一个术语 , 是术语抽象的叫法.

所谓主内存才是真正硬件角度的内存 , 而所谓工作内存 , 则是指CPU的寄存器和高速缓存(cache).至于为什么起名工作内存 , 一方面是为了表述简单 , 另一方面也是避免涉及到硬件的细节和差异 , 例如有的CPU可能没有cache , 有的还存在很多个 , 因此Java就使用工作内存一言蔽之了.

2) 为什么要多次拷贝?

因为CPU访问寄存器和高速缓存的速度 , 比访问寄存器快了3-4个数量级.

如果要连续10次读取同一个数据 , 不断从内存中访问就很慢 , 那么如果第一次从内存中读取到寄存器 , 后面9次从寄存器中读取就会快很多.

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

代码示例:

创建两个线程 t1 和 t2 , t1 线程循环重复快速读取flag , t2 线程对 flag 进行修改.按照预期结构 , 如果我们修改 t2 线程中的 flag  变为非0 , t1 线程就会循环结束.

class MyCounter{
    public int flag = 0;
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        MyCounter myCounter = new MyCounter();
        Thread t1 = new Thread(()->{
            while (myCounter.flag == 0){
                //循环重复快速读取
            }
            System.out.println("循环结束");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个数");
            myCounter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

结果与我们预期并不相符 , 对 flag 作出修改后 , t1 线程并没有循环结束. 

通过 jconsole 查看 t1 线程还在执行 , 而 t2 线程已执行完毕. 

结合内存可见性问题 , 答案显而易见. 一个线程读 , 一个线程改 , 会产生线程不安全问题.从汇编的角度来理解 , 执行下面这段代码分为两个步骤:

  • load 把内存中的值读到寄存器中.
  • cmp 把寄存器的值和0进行比较 , 根据比较结果决定下一步往哪执行(条件循环指令)

上述循环操作在寄存器中 , 执行速度极快(1秒钟执行百万次以上) , 循环这么多次 , 在 t2 真正修改前 , load 得到的执行结果都一样.另一方面 load 相比于 cmp 操作速度慢非常多 , 再加上反复 load 的结果都一样 , JVM 就会认为没有人改 flag 的值 , 从此不再从内存中 load flag 的值 , 直接读取寄存器中保存的 flag , 这时JVM/编译器的一种优化方式 , 但由于多线程的复杂性 , 判定可能存在误差.

解决方式:

此时为了避免上述情况 , 就需要程序员手动干预 , 可以给 flag 这个变量加上 volatile 关键字.意思是告诉编译器这个变量是"易变" , 一定要每次都从内存中重新 load 这个变量 , 不能再进行激进的优化了.

class MyCounter{
    public volatile int flag = 0;
}

2.volatile 不能保证原子性 

volatile 与 synchronized 有本质的区别 , synchronized 保证原子性 , volatile 保证的是内存可见性.

代码示例:

这是最初演示线程安全的代码 , 两个线程分别对 count 自增5万次.

  • 去掉修饰 add 方法的 synchronized 关键字.
  • 给 count 变量加上 volatile 关键字. 

最终代码执行结果并不是预期的10w次. 

class Counter{
    public volatile int count;
    public void add(){
            count++;
    }
}
public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("count = "+counter.count);
    }

二.wait和notify

由于线程的特性是抢占式执行随机调度 , 因此线程之间的先后执行顺序难易预知 , 但实际开发中我们希望合理的协调多个线程之间的先后执行顺序.

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

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

Tips:wait(),notify(),notifyAll()都是Object类的方法.

通过上述介绍可以发现 , wait 和 notify 与 join 和 sleep 在功能上有极大的重合之处 , 那么为什么还要开发 wait 和 notify 呢?

因为 , 使用 join 就必须等待一个线程彻底执行完才能换另一个线程. 如果我们想让线程1执行50% , 然后立即执行线程2 , 显然 join 达不到这个效果. 而且使用 sleep 必须指定休眠多长时间 , 但线程1执行完毕需要花费多少时间并不好估计.所以使用 wait 和 notify 可以更好的解决上述问题.

1.wait方法

wait做的事情:

  • 先释放锁
  • 进行阻塞等待
  • 收到通知后 , 重新尝试获取获取这个锁 , 并且在获取这个锁后 , 继续往下执行.

代码示例:

 public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        object.wait();
    }

运行该代码出现异常 , 这是因为执行 wait 操作 , 需先获取当前线程的锁 , 而当前线程并没加锁 , 所以会出现非法锁状态异常.这就好比 , 我的一个朋友还没收到offer就已经开始挑选公司.

修改后代码:

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

通过运行结果可以得知 , 代码执行到object.wait()就进入阻塞.实际上在阻塞状态之前 , wait 已经释放了锁 , 此时其他线程可以获取到object对象的锁 , 等到 wait 被唤醒后再尝试获取这个锁.

举个例子就是滑稽老铁去ATM机取钱 , 当他进入银行网点后锁上门开始操作ATM机 , 结果发现ATM机没钱 , 由于银行外还有排队等待办理其他业务的人 , 他只能打开锁后出去(相当于 wait 释放锁的操作) , 等待运钞车来存钱(相当于 wait 的阻塞等待) , 当运钞车把钱存进银行 , 站在外面排队等待的滑稽老铁 , 又要和其他竞争进入银行的机会.(重新尝试获取这个锁) , 进入银行后执行取钱操作(重新加锁后继续执行其他操作).

wait结束等待的条件

  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时.(wait 有一个带参方法 , 可以指定等待时间)
  • 其他线程调用该等待线程的 Interrupted 方法 , 导致 wait 抛出InterruptException异常.

2.notify方法

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

  • notifty 方法同样需要在加锁的方法和加锁的代码块中调用 , 该方法是用来唤醒那些因调用 wait方法而阻塞等待的线程 , 通知它们重新获取对象锁.
  • 如果有多个线程调用同一对象处于等待 , 则由线程调度器 , 随机挑选一个呈 wait 状态的线程唤醒.
  • 在 notify 方法执行完毕后 , 当前线程不会立即释放该对象锁 , 要等待执行 notify 方法的线程彻底退出加锁代码块后才会释放锁对象.

代码示例:

public class ThreadDemo3 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(() -> {
            //这个线程负责进行等待
            System.out.println("t1: wait 之前");
            try {
                synchronized (object) {
                    object.wait();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t1: wait 之后");
        });
        Thread t2 = new Thread(() -> {
            System.out.println("t2: notify 之前");
            //notify务必获取锁才能通知
            synchronized (object) {
                object.notify();
            }
            System.out.println("t2: notify 之后");
        });
        t1.start();
//此时让 wait 先执行,防止 notify 空打一炮.
        Thread.sleep(100);
        t2.start();
    }
}

观察代码执行结果明显符合预期. 

为什么 notify 方法也要在同步方法或同步代码块中?

同步方法或同步代码块指的是 , 加锁的方法或加锁的代码块.

代码示例:

假设我们要实现一个阻塞队列 , 如果不加同步代码块实现方法如下:

class BlockingQueen{
    Queue<String> queue = new LinkedList<>();
    Object lock = new Object();
    public void add(String data){
        queue.add(data);
        lock.notify();
    }
    public String take() throws InterruptedException {
        while (queue.isEmpty()){
            lock.wait();
        }
        //返回队列的头结点
        return queue.remove();
    }
}

这段代码的核心思想是 , 当队列为空时使用lock.wait()阻塞 , 如果调用add()方法添加元素时再采用lock.notify()唤醒.这段代码可能产生以下问题:

  • 一个消费者调用 take() 方法获取数据 , 但queue.isEmpty() , 于是反馈给生产者.
  • 在消费者调用 wait 之前 , 由于CPU的调度 , 消费者线程被挂起 , 生产者调用add() , 然后notify().
  • 之后消费者调用wait().由于错误的条件判断导致 wait 调用在 notify 之后.
  • 在这种情况下 , 消费者就会一直被挂起 , 生产者也不再生产 , 这个阻塞队列就有问题.

由此看来 , 在调用 wait 和 notify 这种会挂起的操作时 , 需要一种同步机制保证

3.wait和sleep的对比

理论上 wait 和 sleep 没有可比性 , 因为 wait 常用于线程间通信 , sleep 则是让线程阻塞一段时间 , 唯一的相同点是都可以让线程放弃执行一段时间.

  • 1.wait 需要搭配 synchronized 关键字使用 , 而sleep则不需要.
  • 2.wait 是object 方法 , sleep则是Thread类的静态方法.
  • 3.wait 被notify 唤醒属于正常的业务范畴 , sleep 被Interrupt 唤醒需要报异常.

总结 

到此这篇关于JavaEE中volatile、wait和notify的文章就介绍到这了,更多相关JavaEE volatile、wait和notify内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

JavaEE中volatile、wait和notify详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

JavaEE中volatile、wait和notify详解

这篇文章主要给大家介绍了关于JavaEE中volatile、wait和notify的相关资料,文中通过实例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
2023-02-03

Java多线程wait()和notify()方法详细图解

wait()和notify()是直接隶属于Object类,也就是说所有对象都拥有这一对方法,下面这篇文章主要给大家介绍了关于Java多线程wait()和notify()方法详细图解的相关资料,需要的朋友可以参考下
2022-11-13

Java多线程死锁问题详解(wait和notify)

线程之间形成相互等待资源的环时,就会形成顺序死锁,下面这篇文章主要给大家介绍了关于Java多线程死锁问题(wait和notify)的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
2023-01-05

JUC中的wait与notify方法实现原理详解

这篇文章主要介绍了JUC中的wait与notify方法实现原理,在进行wait()之前,就代表着需要争夺Synchorized,而Synchronized代码块通过javap生成的字节码中包含monitor enter和monitor exit两个指令
2023-03-10

怎么在Java中正确使用wait, notify和notifyAll

这篇文章将为大家详细讲解有关怎么在Java中正确使用wait, notify和notifyAll,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。wait, notify 和 notifyAll,这些在多线程
2023-06-17

Java使用wait() notify()方法操作共享资源详解

Java多个线程共享资源;  1)wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。  2)调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对象的monitor(即
2023-05-31

Java中怎么使用wait和notify实现线程间的通信

这篇“Java中怎么使用wait和notify实现线程间的通信”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Java中怎么使
2023-06-30

java中的wait()和notify()方法实现生产者消费者模式实例

这篇文章主要介绍“java中的wait()和notify()方法实现生产者消费者模式实例”,在日常操作中,相信很多人在java中的wait()和notify()方法实现生产者消费者模式实例问题上存在疑惑,小编查阅了各式资料,整理出简单好用的
2023-06-20

JAVA线程sleep()和wait()详解及实例

JAVA线程sleep()和wait()详解及实例sleep1.sleep是Thread的一个静态(static)方法。使得Runnable实现的线程也可以使用sleep方法。而且避免了线程之前相互调用sleep()方法,引发死锁。2.sl
2023-05-31

Java线程之线程同步synchronized和volatile详解

上篇通过一个简单的例子说明了线程安全与不安全,在例子中不安全的情况下输出的结果恰好是逐个递增的(其实是巧合,多运行几次,会产生不同的输出结果),为什么会产生这样的结果呢,因为建立的Count对象是线程共享的,一个线程改变了其成员变量num值
2023-05-30

Linux网络编程wait()和waitpid()的详细讲解

本篇内容介绍了“Linux网络编程wait()和waitpid()的详细讲解”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!客户端断开连接后,
2023-06-13

Java中Volatile关键字详解及代码示例

一、基本概念先补充一下概念:Java内存模型中的可见性、原子性和有序性。可见性:可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了
2023-05-30

系统编程-进程wait、waitpid和WIFEXITED系列宏超级详解

在系统编程中,进程的等待和状态检查是非常重要的操作。在Linux系统中,我们可以使用wait、waitpid函数以及一系列的宏来实现进程的等待和状态检查。1. wait函数:wait函数用于等待任意子进程结束,并返回子进程的状态信息。其原型
2023-09-15

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录