Java多线程之内存可见性

来源:互联网 发布:linux 打开文件目录 编辑:程序博客网 时间:2024/05/11 15:55

这篇文章主要介绍如下几点内容,带着问题去思考。希望这篇文章能够让你从中学到东西。

  1. 内存可见性
  2. 指令重排序
  3. as-if-serial语义
  4. synchronized实现可见性
  5. volatile实现可见性
  6. synchronized与voatile比较

一、概念

1.什么是可见性?
一个线程对共享变量值的修改,能够及时地被其他线程看到,称之为可见性。
2.什么是共享变量?
如果一个变量在多个线程的工作内存中存在副本,那么这个变量就是这几个多线程的共享变量。

二、Java内存模型(JMM)

JMM描述了Java程序中各种线程共享变量的访问规则,以及在JVM中将共享变量存储到内存和从内存中读取出共享变量的底层细节。
有两点需要注意:
  1. 所以的共享变量都存储在主内存中
  2. 每个线程都有自己独立的工作内存,工作你中保存该ianc使用到的共享变量的副本,是主内存中该共享变量的一份拷贝。


JMM的两点规定:
  1. 各个线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中进行读写操作。
  2. 不同线程之间无法直接访问其他线程工作内存中的共享变量,各个线程间共享变量的传递需要通过主内存来完成。

结合JMM的图应该不难理解,这些概念性的知识吧!

问题:线程1对共享变量修改想要及时地被线程2看到,需要怎么做???
从JMM的两点规定,很容易知道只需要分成两个以下步骤即可:
  1. 把工作内存1中更新过的共享变量刷新到主内存中
  2. 将主内存中的共享变量更新到工作内存2中


小结
要实现共享变量的可见性,必须要保证两点:
  1. 线程修改后共享变量的值能够及时从工作内存刷新到主内存中。
  2. 其他线程能够及时地从主内存中的共享变量更新到自己工作内存中

三、Java实现可见性

分析:导致共享变量在各个线程间不可见的原因:
  1. 线程的交叉执行
  2. 重排序(下面有说明什么是重排序)结合线程交叉执行
  3. 共享变量更新后的值没有及时在主内存中更新
小案例:
package org.choimeyu.synchronizedDemo;/** * 存在线程不安全问题. * 解决方法之一是:在读写方法中加入synchronized即可. * @author George */public class SynchronizedDemo {//共享变量private boolean flag = false;private int result = 0;private int num = 1;//读操作public void read() {if(flag) {//1.1result = num * 2;//1.2}System.out.println("result = " + result);}//写操作public void write() {flag = true;//2.1num = 2;//2.2}/* 线程不安全,存在重排序 * 分析: * 第一种情况:result = 4 * 执行顺序:2.1→2.2→1.1→1.2 * 重排序可能导致 * 执行顺序:2.2→2.1→1.1→1.2 * . . . * 第二种情况:result = 0 * 当然,这种执行结果出现的次数相对较少,可能执行了十几次程序才会出现一次结果。 * 这是为什么呢? * 原因很简单,其实Java编译器做了一些优化,Java编译器尝试着去揣摩我们程序执行的意图。 * 但是,往往就执行那么一次,就可能给我们带来不可估计的损失。所以我们还是要杜绝这种共享变量 * 不可见性的发生。 * 执行顺序:2.1→1.1→1.2→2.2 * 重排序可能导致 * 执行顺序:2.2→1.1→1.2→2.1 (涉及了线程的交叉执行) * . . .  *     备注:当然1.1和1.2也是存在重排序的可能性的. *//* * 内部线程类 */private class ReadWriteThread extends Thread {private boolean flag;//根据构造方法中flag参数传入的值,决定执行读操作还是写操作public ReadWriteThread(boolean flag) {this.flag = flag;}@Overridepublic void run() {if (flag) {//flag的值为true时,执行读操作read();} else {//flag的值为false时,执行写操作write();}}}public static void main(String[] args) {SynchronizedDemo sd = new SynchronizedDemo();//启动线程执行写操作sd.new ReadWriteThread(false).start();//启动线程执行读操作sd.new ReadWriteThread(true).start();}}




Java可见性实现方式:synchronized和volatile  ps:其实finally也可以实现内存可见性,因为finally里的变量值不能被修改。这是很特殊的一种情况,这里不做考虑。

synchronized实现可见性

synchronized能够实现:原子性(同步)、可见性

JMM对synchronized的两条规定:
  1. 线程解锁前,必须把共享变量的最新值刷新到主内存中
  2. 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值
注意:加锁和解锁必须是同一把锁,这样就保证了线程解锁前对共享变量的值修改在下次加锁时对其他线程可见,也就实现了内存的可见性。

线程执行synchronized互斥代码的过程:
  1. 获得互斥锁
  2. 清空工作内存
  3. 从主内存拷贝共享变量的最新副本到工作内存
  4. 执行代码
  5. 将更改后的共享变量的值刷新到主内存
  6. 释放互斥锁

四、重排序

什么是重排序?
其实我们书写的代码顺序与实际编译器执行代码顺序有些不同的地方,指令重排序是编译器或处理器为了提高程序的性能而做的优化。
主要分为以下集中重排序:
  1. 编译器优化的重排序(编译器优化)
  2. 指令级并行重排序(处理器优化)
  3. 内存系统的重排序(处理器优化)
例子:重排序有可能导致以下结果:
代码顺序
int number = 1;
int result = 0;
 
执行顺序
int result = 0;
int number = 1;

五、as-if-serial语义

什么是as-if-serial?
其实Java设计者早就想到了,如果指令重排序提高程序的性能做出的优化会导致程序结果不一致,那有什么用。于是就设计了as-if-serial,无论如何重排序,都会保证程序执行的结果与代码顺序执行结果一致。
Java编译器和处理器运行时都会保证Java在单线程下遵循as-if-serial语义

例子:
int num1 = 1;   //①int num2 = 2;   //②int sum = num1 + num2;   //③

单线程:第一和第二行代码的顺序可以重排序,但是第三行不可以。重排序不会给单线程带来内存可见性问题。但是在多线程中,程序交错执行时,重排序可能会造成内存可见性问题。


volatile实现可见性

volatile能够保证可见性,但是不能保证复合操作的原子性。

volatile如何实现内存可见性:

1.深入的说:主要通过加入内存屏障和禁止重排序优化实现的

  1. 对volatile变量执行写操作时,JMM会在写操作后加入一条store屏障指令
  2. 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令
ps:JMM对实现内存可见性,总共有八条指令,欲知详情情查询相关资料。

2.简单的说:volatile变量在每次被线程访问时,都会强迫从主内存中重新读取共享变量的值,而当该共享变量发生变化时,又会强迫工作内存将最新值刷新到主内存中。这样任何时刻,不同的线程总能够看到该共享变量的最新值。


小案例:

number ++ ;
其实该语句这是JVM要执行三步操作:

  1. 读取number值
  2. number+1
  3. 将number的值写入内存
volatile并不能保证原子性,有可能将这三步操作交由三个线程来完成

小结

要在多线程中安全的使用volatile变量,必须同时满足以下两个条件

  1. 对变量的写入操作不依赖其当前值,即不满足:number ++ ; count = count * 5 ; 等语句,满足:boolean变量,记录温度变化的变量等等
  2. 该变量没有包含在具有其他变量的不变式中。即不满足:不变式low<up

六、synchronized与volatile比较

  1. volatile不需要加锁,比synchronized更轻量级,不会阻塞线程。
  2. 从内存可见性角度分析,volatile读操作相当于加锁,volatile写操作相当于解锁
  3. synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性




0 0
原创粉丝点击