【JavaEE】多线程之线程安全(volatile篇),wait和notify

news2024/11/25 21:27:01

目录

内存可见性问题

volatile关键字

从JMM的角度来看内存可见性

 wait和notify

wait

notify-notifyAll


内存可见性问题

首先运行一段代码,线程t1 用 Mycount.flag 作为标志符,当不为0的时候就跳出循环,线程t2 通过输入来改变 Mycount.flag 标志符,从而控制线程t1 的循环。对于运行结果,我们的预期是:当输入一位不为0的数时,线程t1 应该停止循环。

import java.util.Scanner;

class MyCount{
    public int flag = 0;
}
public class ThreadDemo14 {
    public static void main(String[] args) {
        MyCount myCount = new MyCount();
        Thread t1 = new Thread(()->{
            while (myCount.flag == 0) {

            }
            System.out.println("t1循环结束");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            myCount.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

 

 从上述运行结果和 jconsole查询也可以看出,这与预期结果并不相符,在输入1 后,线程t2 已经执行完了,已经销毁了,线程t1 仍然处在循环中,依然存在。

对于线程t1 中 while (myCount.flag == 0),使用汇编来解析主要分为两步:

1. load:把内存中的flag的值,读取到寄存器中;

2. cmp:把寄存器的值,和 0 进行比较,根据比较结果,决定下一步的执行方向(条件跳转指令)

CPU针对寄存器的操作,要比内存操作快很多;计算机对于内存的操作,要比硬盘快很多

因此在线程t2 真正输入之前,线程t1 循环了很多次,且 load 得到的的结果都是一样的,另一方面,load 操作和 cmp 操作相比,速度慢很多。

由于 load 执行速度太慢(相比于 cmp 来说),再加上反复 load 到的结果都一样,JVM 就做出了一个优化的决策:就是不再重复读取的 load 了,只读取一次。这也是编译器优化的一种方式。这就导致了上述问题的出现,即使线程t2 修改了标志符,但是线程t1 仍然在循环中。

因此,内存可见性问题,可以理解为:一个线程针对一个变量进行读取操作,同时另一个线程针对这个变量进行修改。此时线程读取到的值,不一定是修改之后的值。也就是读线程没有感知到变量的变化。

归根结底,也是编译器/ jvm 在多线程环境下优化时产生了误判。 

volatile关键字

因此,针对上述问题,我们可以手动干预,对变量添加 volatile 关键字。本质上就是解决编译器优化问题,告诉编译器,这个变量是易变的,因此每次都应该重新读取这个变量的内存内容,也就不再进行激进的优化了。(volatile 只能修饰变量)

此时再去运行程序,也就可以达到预期要求了。 

 所以说,volatile 也是解决了一种线程安全问题。

局部变量只能在当前的方法里使用,出了方法变量就没了,方法内部的变量在 "栈" 这样的内存空间上。每一个线程都有自己的栈空间。即使是同一个方法,在多个线程中被调用,这里的局部变量也会处在不同的栈空间中,本质上也是不同变量。

从这里我们也可以看出,volatile 是不修饰方法里的变量的,因为方法里的局部变量,只能在当前线程使用,不能多线程之间同时读取或者调用,也就是天然规避了线程安全问题。

一个程序,如果针对同一个变量,在两个线程中,一个读,一个写,就应该考虑 volatile 。

而加上 volatile 之后,效果也可见,牺牲了运行速度,换来了准确率。 

从JMM的角度来看内存可见性

Java Memory Model - java内存模型

从JMM 的角度重新表述内存可见性问题:

java程序里,有主内存,每个线程还有自己的工作内存(线程t1 和线程t2 的工作内存不是同一个东西)

t1 线程进行读取的时候,只读取了工作内存的值;

t2 线程进行修改的时候,先修改工作内存的值,然后再把工作内存的内容同步到主内存中。

但是由于编译器优化,导致线程t1 没有重新的从主内存同步数据到工作内存,所以读到的结果就是 “修改之前” 的结果。

主内存:这里所说的主内存,就可以等价于我们所说的内存;

工作内存: 也称为工作存储区。工作内存就并非是内存,而是指 CPU 上存储数据的单元。(寄存器,缓存 cache)

缓存 cache

寄存器存储空间小,读写速度块,价格高;

内存存储空间大,读写速度慢,价格便宜;(相比于寄存器来说)

于是,就引出了 cache:存储空间居中,读写速度居中,价格居中;

因此,当CPU 要读取一个内存数据的时候,可能是直接读取内存,也可能是读取 cache,也能是读取寄存器;

工作内存(工作存储区)也一般指:CPU的寄存器 + CPU的 cache

(缓存一般分为L1,L2,L3三级缓存的,L1,L2是在CPU中的,这是对于之前的CPU;现在的CPU也有L3的专属空间了。)

 wait和notify

线程最大的问题,就是抢占式执行,随机调度。

但正常情况下,都不喜欢随机性的东西,因此也就有了一些方法,来控制线程之间的执行顺序。虽然线程在内核里的调度是随机的,但是可以通过一些 api 让线程主动阻塞,主动放弃CPU,来给其他线程让路。

例如,有 t1 和 t2 两个线程,希望 t1 先干活,干的差不多了,再让 t2 来运行。这时候就可以让 t2 先 wait(进入阻塞状态,主动放弃CPU),等 t1 干的差不多了,再通过 notify 通知 t2,将 t2 唤醒,让 t2 干活。

在这时候,大家可能就要问了,在这种场景下,join或者sleep不行么?

使用 join ,则必须 t1 彻底执行完,t2 才能运行,但是如果是要求 t1 先干 50% ,就让 t2 干活的话,join 就无能为力了;

使用sleep,要指定一个休眠时间,对于程序运行的时间,是很难估计的,所以也不合适。

因此在这种情况下,wait/notify 是更好的选择。 

wait

wait做的事情:
  1. 使当前执行代码的线程进行等待 . ( 把线程放到等待队列中 )
  2. 释放当前的锁
  3. 满足一定条件时被唤醒 , 重新尝试获取这个锁
wait 结束等待的条件 :
  1.  其他线程调用该对象的 notify 方法。
  2. wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本 , 来指定等待时间 )。
  3. 其他线程调用该等待线程的 interrupted 方法 , 导致 wait 抛出 InterruptedException 异常 。 

当一个线程调用 wait 的时候,就会进入阻塞,此时就处在 WAITING 的状态。 

wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
因为 wait 操作是释放锁,然后进入阻塞等待,在收到notify通知后,重新获取锁,继续执行。所以说,得先有锁,才可以进行接下来的释放锁操作。
当一个线程 wait 后,这个线程会释放锁,然后进入 WAITING 状态,此时其他线程是可以获取到释放的锁对象的。
这样在执行到  wait  之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个唤醒的方法notify()

notify-notifyAll

notify 方法是唤醒等待的线程
1. 方法 notify() 也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的 其它线程,对其发出通知notify ,并使它们重新获取该对象的对象锁。
2. 如果有多个线程等待,则由线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到 ")
wait 和 notify 的使用对象,应该是相同的,notify 只能唤醒在同一个对象上等待的线程。

针对下面代码进行分析: 

public class ThreadDemo16 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(()->{
            //这个线程负责进行等待
            System.out.println("wait之前");
            try {
                synchronized (object) {
                    object.wait();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("wait之后");
        });
        //这个线程负责进行通知
        Thread t2 = new Thread(()->{
            System.out.println("notify之前");
            synchronized (object) {
                //notify 务必要获取到锁,才能进行通知
                System.out.println(" t1 在等待,t2正在做任务 ");
                object.notify();
            }
            System.out.println("notify之后");
        });

        t1.start();
        //为了保证t1先执行,也就是为了 先wait再notify,以免notify的时候,object对象没有在wait
        //此处写的 sleep 1000 是大概率会让当前 t1 先执行 wait 的
        //但是也避免不了有时候的极端情况,可能 t2 先执行 notify
        Thread.sleep(1000);
        t2.start();
    }
}

t1 线程负责执行 wait ,t2 线程负责唤醒 处在WAITING 状态的 t1线程。创建 object 对象后,在线程t1 中对 object 进行加锁,然后进行 wait 操作。此时 t1 线程释放锁,并进入 WAITING状态。线程t2 获取到锁,开始执行,执行完任务后,调用 object.notify 来唤醒 object 对象,此时线程t1 重新获取到锁,继续执行。

因为线程调度的不确定性,不能保证线程t2 notify的时候,t1线程一定处于 WAITING状态,此时就相当于notify空打一炮,就属于是无效通知。所以让线程t1 先 start 后,等待1秒再 start 线程t2。保证t1线程先执行 wait,t2 线程后执行 notify,这样才是有意义的。

notify 方法只是唤醒某一个等待线程 . 使用  notifyAll  方法可以一次唤醒所有的等待线程。
虽然是同时唤醒 3 个线程 , 但是这 3 个线程需要竞争锁 . 所以并不是同时执行 , 而仍然是有先有后的执行。

wait notify notifyAll 都是 Object 类的方法  

对于wait,有两个版本,带参数和不带参数;

带参数,则是指定了最大等待时间;不带参数就是死等;

 wait 带有等待时间的版本,看起来和sleep有一些相似,但实际还是有区别的,虽然都能指定等待时间,也都能被提前唤醒(wait 是使用notify唤醒的,sleep是使用interrupt唤醒的)

但notify唤醒wait,是正常的业务逻辑,不会有任何异常;

interrupt唤醒sleep,则是出异常了;

如果当前有多个线程在等待object对象,此时有一个线程 object.notify() ,此时会随机唤醒一个等待的线程。但其实可以规避这种不确定性的情况,可以使用多组不同的对象,来决定线程之间的执行顺序。

例如有三个线程,希望先执行线程1,在执行线程2,最后执行线程3 

这个时候就可以创建 object1,供线程1,2使用;

创建 object2,供线程2,3使用;

线程3:object2.wait() ,等待线程2 执行 object2.notify() 完后唤醒再进行执行;

线程2:object1.wait() ,等待线程1 执行 object1.notify() 完后唤醒再进行执行。执行完自己的任务后 object2.notify() 来唤醒线程3 执行;

线程1:执行自己的任务,执行完后,object1.notify() 来唤醒线程2 执行;

代码演示: 

// 有三个线程,分别只能打印 A,B,C 控制三个线程固定按照 ABC 的顺序进行打印
public class ThreadDemo18 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(()->{
            System.out.println("A");
            synchronized (locker1) {
                locker1.notify();
            }
        });

        Thread t2 = new Thread(()->{
            synchronized (locker1) {
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("B");
            synchronized (locker2) {
                locker2.notify();
            }
        });

        Thread t3 = new Thread(()->{
            synchronized (locker2) {
                try {
                    locker2.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("C");
        });

        t2.start();
        t3.start();
        Thread.sleep(100);
        t1.start();                     // t1 最后执行是为了防止 t1 在 notify 的时候 t2 还没有 wait ,那就进入死等了

    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/181259.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot 入门

springboot是什么 传统的开发模式下,无论是基于xml或注解,都要做许多配置,如果项目中集成越多的其他框架,配置内容也会越多。为了让开发人员以最少的配置去开发应用,springboot诞生了。springboot的原则是约定大于配置…

VSCode中4个Settings(JSON)的区别与联系

目录 🔥 前言 1. Preferences: Open Default Settings(JSON) 2. Preferences: Open User Settings 3. Preferences: Open Settings(JSON) 4. Preferences: Open Workspace Settings(JSON) 🔥 总结 🔥 前言 在VSCode中输入快捷键ctrlsh…

读书笔记:梯度法求函数的最小值 gradient_method.py ← 斋藤康毅

● 由多元函数全部变量的偏导数汇总而成的向量称为梯度(gradient)。梯度指示的方向是各点处的函数值减小最多的方向。● 虽然梯度的方向并不一定指向最小值,但沿着它的方向能够最大限度地减小函数的值。因此,在寻找函数的最小值&a…

一篇五分生信临床模型预测文章代码复现——Figure 8 生存曲线鲁棒性分析

之前讲过临床模型预测的专栏,但那只是基础版本,下面我们以自噬相关基因为例子,模仿一篇五分文章,将图和代码复现出来,学会本专栏课程,可以具备发一篇五分左右文章的水平: 本专栏目录如下: Figure 1:差异表达基因及预后基因筛选(图片仅供参考) Figure 2. 生存分析,…

Linux输入子系统简析

1. 前言 限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。 2. 背景 本文基于 Linux 4.14 内核源码进行分析。 3. 简介 Linux 内核输入子系统,负责对系统中的输入设备进行管理。 一方面&#xf…

Spring Cloud_Ribbon负载均衡服务调用

目录一、概述1.是什么2.官网资料3.能干嘛二、Ribbon负载均衡演示1.架构说明2.POM三、Ribbon核心组件Irule1.IRule2.如何替换3.新建package(注意:包的位置)4.上面包下新建MySelfRule规则类5.主启动类添加RibbonClient6.测试四、Ribbon负载均衡…

Python将JSON格式文件导入 redis,多种方法

在导入前需要先确定你已经安装 Redis,并且可以启动相关服务。 windows 上启动 redis 的命令是 redis-server.exe redis.windows.conf,效果图如下: 文章目录使用 Python 连接 redis安装 redis 与 导入使用代码连接 Redis写入键值操作 JSON 文…

第26章 分布式缓存数据库配置的定义实现

1 Core.Configuration.CacheConfig namespace Core.Configuration { /// <summary> /// 【缓存配置--类】 /// <remarks> /// 摘要&#xff1a; /// 通过该类中的属性成员实例对“appsettings.json”文件中的1个指定缓存项(键/值对)在内存或指定分布式软件中…

mysql-installer-community-8.0.22.0安装教程

1. 下载 mysql-installer-community-8.0.22.0安装包 首先去官网&#xff1a;https://dev.mysql.com/downloads/installer/ 下载MySQL。 2. 默认Next 3. 点击Execute 4. 同意安装 5. 点击Next后点Yes 6. 点击Execute 这里出现10个选项是正确的&#xff0c;如果不是&#xff0c;…

C++ vector 容器介绍

C vector 容器介绍 C的vector是标准库中常见的一种容器&#xff0c;使用起来非常方便&#xff0c;可以用来代替c原本的数组。vector是种容器&#xff0c;类似数组一样&#xff0c;但它的size可以动态改变。vector的元素在内存中连续排列&#xff0c;这一点跟数组一样。由于vect…

队列同步器AQS的实现与分析——独占锁模式

AQS独占锁模式源码分析1、tryAcquire()、acquire()方法2、addWaiter()方法3、acquireQueued()方法4、shouldParkAfterFailedAcquire()方法5、tryRelease()、release()方法1、tryAcquire()、acquire()方法 protected boolean tryAcquire(int arg) {throw new UnsupportedOperat…

glassfish任意文件读取漏洞

glassfish任意文件读取漏洞1.简介1.1.漏洞类型1.2.漏洞成因1.3.语法搜索1.4.影响版本2.漏洞复现2.1.POC2.2.访问地址2.3.GlassFish的敏感目录2.3.1.获取数据库密码2.3.2.获取GlassFish的后台密码2.4.POC脚本1.简介 GlassFish是一款强健的商业兼容应用服务器&#xff0c;达到产品…

MyEclipse提示过期,MyEclipse Subscription Expired激活方案

一、错误描述 紧接上文&#xff0c;虽然解决了MyEclipse提示过期问题&#xff0c;但是你会发现出现一行红色提示如下&#xff1a; 1.错误日志 Product activation must be completed within 5 days. 2.错误说明 产品激活必须在5天内完成。 二、解决方案 从错误日志很明显的可…

C++语法复习笔记-2. c++基础句法

文章目录1. 图灵机与三种基本结构1. 顺序结构2. 分支结构自定义结构-枚举结构体与联合体结构体数据对齐问题3. 循环结构三种循环结构反汇编查看三种结构效率实例&#xff1a;输出所有形如aabb的四位数的完全平方数方案1: 构造aabb数&#xff0c;再判断方案2&#xff1a;反向操作…

《网络编程实战》学习笔记 Day10

系列文章目录 这是本周期内系列打卡文章的所有文章的目录 《Go 并发数据结构和算法实践》学习笔记 Day 1《Go 并发数据结构和算法实践》学习笔记 Day 2《说透芯片》学习笔记 Day 3《深入浅出计算机组成原理》学习笔记 Day 4《编程高手必学的内存知识》学习笔记 Day 5NUMA内存知…

自制DAPLink 基于ARM官方源码以及STM32F103C8T6

【本文发布于https://blog.csdn.net/Stack_/article/details/128771308&#xff0c;未经许可禁止转载&#xff0c;转载须注明出处】 一、安装工具并配置环境变量 1、python3 【官网】 【网盘】 链接&#xff1a;https://pan.baidu.com/s/1zW_H_eQlkzX3FkXuClFnTA 提取码&#…

python 操作 json 文件的种种知识点

本篇博客将带你全方位了解 Python 操作 json 文件的技术点 让你碰到 json 格式文件不在发愁 文章目录json 模块读取 JSON写入 JSON读取与写入基本用法如下json 模块进阶用法控制输出格式在 JSON 中存储 Python 特殊类型对数据进行验证和清洗第三方模块json 模块 Python 提供了…

CE自动汇编之AOB注入

目录 一、什么是AOB注入&#xff1f; 二、什么时候使用AOB注入&#xff1f; 三、代码注入 四、全部注入 五、“全部注入”和“AOB注入”的分别 六、代码注入与AOB注入的区别 CE自动汇编的模板中&#xff0c;有三种注入代码的方式&#xff1a; 第一种是代码注入&#xff…

Qt使用数据库模型中的删除详解

以下使用 QSqlTableModel 模型&#xff0c;使用tableView显示内容 以下为界面&#xff1a; 这里主要介绍删除操作&#xff1a; 删除一行为&#xff1a; int rowui->tableView->currentIndex().row();//获取行号model->revertRow(row);//删除该行model->submitAll(…

git 关于分支和仓库的理解

何时需要initgit init//初始化本地仓库.git目录如果初始化就会在当前文件夹中出现.git的目录&#xff0c;该目录默认是隐藏的&#xff0c;需要关闭显示隐藏文件才能看到。执行完git init命令后&#xff0c;当前目录就成为了工作区&#xff08;工作区可以理解为操作本地仓库的车…