当前位置: 首页 > news >正文

JavaEE初阶——多线程(4)线程安全

目录

一、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;}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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方法的对象要相同

http://www.dtcms.com/a/541436.html

相关文章:

  • 杭州网站推广营销服务深圳做美颜相机的公司
  • 什么样 个人网站 备案适合做网站的软件有哪些
  • 做设计在哪个网站上找高清图青岛做物流网站
  • 数据源切换的陷阱:Spring Boot中@Transactional与@DS注解的冲突博弈与破局之道
  • Kubernetes节点资源优化:自托管代理配置实践
  • 1688网站怎样做推广东莞市路桥收费所
  • 做网站需要用到哪些开发软件潜江资讯网信息发布
  • Day2实战-元组的基本操作
  • 01 数学建模中M的取值影响及分析
  • 深入 Actix-web 源码:解密 Rust Web 框架的高性能内核
  • Linux远程控制Windows桌面的cpolar实战指南
  • 焦作网站建设哪家好自己怎么用h5做网站
  • 论坛程序做导航网站photoshop安卓版
  • FP16 vs INT8:Llama-2-7b 昇腾 NPU 精度性能基准报告
  • Steering Llama 2 via Contrastive Activation Addition
  • 座舱出行Agent实战(三):专能化架构如何实现效率与稳定性的双重飞跃
  • 淘宝联盟怎么新建网站网站设计教程
  • 一篇文章深入理解Elasticsearch高级用法
  • 【数据工程】14. Stream Data Processing
  • Elasticsearch入门指南:从零到精通
  • wordpress 非插件七牛cdn全站加速东至网站建设
  • 进出口网站贸易平台有哪些个人网站可以做推广吗
  • 游戏网站首页设计服务器有了网站怎么做
  • 计算机组成原理---存储系统
  • Vector深度剖析及模拟实现
  • Linux进程:进程属性
  • word文档做网站建立网站地图
  • 大连建设网节能办公室网站随州网站seo
  • SSM老年公寓管理系统4do68(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • 网站制作佛山海拉尔网站建设平台