《Java并发编程学习》 Java是如何解决可见性与有序性的
创始人
2024-06-02 08:23:56
0

一.线程安全的三个问题

在并发编程的情况下,如果需要保证线程安全,需要解决三个问题:

  1. 由操作系统切换线程带来的原子性问题
  2. 由CPU缓存带来的可见性问题
  3. 由编译器优化代码带来的有序性问题

原子性问题需通过加锁来解决,而可见性和有序性,Java则依靠JMM

JMM区别与JVM JMM 指的是Java内存模型,而JVM 指代的是Java数据存储模型

解决缓存带来的可见性问题需要禁用缓存,解决有序性问题则需要禁止指令重排序。JMM提供了如何按需禁用缓存和指令重排的方法。主要通过8个操作命令 与 7个 Happens-Before规则

二.八个操作命令

JMM 定义了 8 个操作来完成主内存和工作内存之间的交互操作。

八个命令与解释

  1. lock (锁定) - 作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

  2. unlock (解锁) - 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

  3. read (读取) - 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。

  4. load (载入) - 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。

  5. use (使用) - 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时就会执行这个操作。

  6. assign (赋值) - 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  7. store (存储) - 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后 write 操作使用。

  8. write (写入) - 作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。

八个命令需遵守的成双成对规则

JMM 还规定了上述 8 种基本操作,需要满足以下规则:

read 和 load 必须成对出现;store 和 write 必须成对出现。即不允许一个变量从主内存读取了但工作内存不接受,或从工作内存发起回写了但主内存不接受的情况出现。

不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须把变化同步到主内存中。

不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中。

一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign )的变量。换句话说,就是对一个变量实施 use 和 store 操作之前,必须先执行过了 load 或 assign 操作。

一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。所以 lock 和 unlock 必须成对出现。

如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。

如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量。

对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)

实际代码解释

比如下面的代码 (i=0)

i+=1

此代码包含了6个命令:

  1. 调用read命令,从主内存中读取i的值 0;
  2. 调用load命令,把read读取到i的值载入到工作内存
  3. 调用use命令,将工作内存中i的值传递给执行引擎
  4. 调用assign命令,将执行引擎中i的最新值 1 赋值给工作内存中的 i
  5. 调用store命令,将工作内存中的i的值存储到主内存中
  6. 调用write命令,将i的值写入到主内存中,此时其他线程从主内存中再获取i的值就是 1;

三. 七个 Happen-Before 原则

image-20230312112158256

一.程序的顺序性规则

这条规则是指在一个线程中,按照程序顺序(可能是重排序后的顺序),前面的操作 Happens-Before 于后续的任意操作,程序前面对某个变量的修改一定是对后续操作可见的

ClassReordering {int x = 0, y = 0;public void writer() {x = 1;y = 2;}public void reader() {int r1 = y;int r2 = x;}
}

如果先执行 writer() 方法,那么 当 reader() 执行时,r1与r2的值分别是1,2 因为 writer() 先于 reader() 方法执行。

二. volatile 变量规则

这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作

class VolatileExample {int x = 0;volatile boolean v = false;// 线程A 先public void writer() {x = 1;v = true;}// 线程B 后public void reader() {if (v == true) {// 这里x会是多少呢? }}
}

这里由于A 先执行 对 volatile 修饰的变量 V 的写操作,因此,当线程B启动的时候,对变量 V进行读操作时,

V的值就是 true。 再由于 Happen-Before的 传递性 ,在线程 A 中 ,x=1 在前,那么根据规则一(程序的顺序性规则),x=1 Happen-Before v =true这一步,而线程A 对 volatile 修饰的变量 V 的写操作 Happen-Before

线程B 对 volatile 修饰的变量 V 的读操作,因此 x=1 Happen-Before 线程B 对 volatile 修饰的变量 V 的读操作

三. 管程中锁的规则

对一个锁的解锁 Happens-Before 于后续对这个锁的加锁

int x = 1;public void syn() {synchronized (this) { //此处自动加锁if (this.x < 2) {this.x = 22; }  } //此处自动解锁}

此处对 this 的加锁操作一定发生在解锁操作前,众所周知,调用Object类的wait方法会释放锁,如果在非锁的代码中调用 wait命令是会抛出异常的

image-20230312103557384

四. 线程启动规则

它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

static int var = 1;
// 主线程A
public static void t1() {Thread B = new Thread(()->{// 主线程调用B.start()之前// 所有对共享变量的修改,此处皆可见// var=2});// 此处对共享变量var修改var = 2;// 主线程启动子线程B.start();
}

如上述代码,主线程先修改了变量 var 的 值为 2 后启动了 子线程B。因此,当线程B对 主线程修改后的var值2是可见的

五. 线程join规则

它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作结果可见。

static int var = 5;//主线程A
public static void t1() {Thread B = new Thread(()->{// 此处对共享变量var修改var = 6;});// 主线程启动子线程B.start();//主线程等待子线程B结束B.join(),// var==6}

上述代码中,主线程启动B线程后,调用join方法,等待B执行结束,在获取变量var的值一定是B线程修改后的最新值。

六. 线程中断规则

对线程interrupt()方法的调用 Happens-Before 被中断线程的代码检测到中断事件的发生,比如我们可以通过Thread.interrupted()/isInterrupted方法检测到是否有中断发生。

七.对象终结规则

一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

四.final对象逃逸

final int x;
// 错误的构造函数
public FinalFieldExample() { x = 3;y = 4;// 此处就是讲this逸出,global.obj = this;
}

如上述代码 x 被 final 修饰,是不可变的,在jdk1.5中,可能会优化成

final int x;public FinalFieldExample() { global.obj = this;x = 3;y = 4;
}

此时将存在一个问题,外面函数通过global.obj.x获取x可能得到0。这是由于构造函数内可能重排序,先执行了global.obj.x=this,然后再对x赋值。所以不要在构造函数中将对象赋值给外部变量,有可能对象的属性还未初始化

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...