深入浅出多线程编程实战线程三大特性(原子性可见性有序性)
Posted 、Dong
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入浅出多线程编程实战线程三大特性(原子性可见性有序性)相关的知识,希望对你有一定的参考价值。
深入浅出多线程编程实战(四)线程三大特性
一、原子性
原子性:熟悉数据库特性的我们都知道数据库sql执行中也有原子性,数据库中的原子性是这样定义的在一个事务中要么所有的sql都执行,要么都不执行。
java内存模型中的原子性也是类似,要么所有的指令都执行,要么都不执行。这样才能保证并发操作的安全性和一致性。但是并发在带给我们方便的同时,却不能很好的
解决原子性的问题。下面我们看一下Java并发操作中是怎么产生原子性问题的。
操作系统为了提高并行处理问题的能力,会将时间分成一个个小的分片,例如50毫秒,过了50毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”)
针对同一个cpu,线程A获取到cpu的使用权后开始执行自己的任务,到了一定时间分片后让出使用权,此时线程B会占有cpu的使用权。但是如果此时线程A的任务没有执行完成,就进行了线程切换,此时线程A的操作就无法保证原子性了。
举一个例子:代码count+=1;至少需要三条 CPU 指令.
(1) 将count的值从内存中读取到寄存器中。
(2) 在寄存器中进行+1操作。
(3) 将结果写入内存中。
当上述三个过程中线程A 刚执行完步骤(1)后,进行了线程切换,线程B重新执行上述操作,最后内存中存储的数据是1而不是2。具体流程如图2所示。
以上便是线程切换带来的Java并发原子性问题。
二、可见性
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。
每个 CPU 都有自己的缓存,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。比如下图中,线程 A 操作的是 CPU-1 上的缓存,而线程 B 操作的是 CPU-2 上的缓存,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了。
代码示例:
public class Test {
private int count = 0;
private void add() {
int idx = 0;
while(idx++ < 1000) {
count += 1;
}
}
public static long calc() {
final Test test = new Test();
// 创建两个线程,执行add()操作
Thread th1 = new Thread(()->{
test.add();
});
Thread th2 = new Thread(()->{
test.add();
});
// 启动两个线程
th1.start();
th2.start();
// 等待两个线程执行结束
th1.join();
th2.join();
return count;
}
}
启动了2个线程分别对一个从0开始的数字分别加1000次,最后返回的结果一定是介于1000–2000中间的数字。出现这个问题的原因是多线程操作时一个线程修改了内存中的数值时没有来得及通知其他线程所在cpu的缓存中。导致另外一个线程从缓存中读到的数据还是原来的数据,所以最后的结果达不到2000。这也就是并发问题中的可见性问题。
三、有序性
有序性:程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,但是不会影响最终的执行结果。有序性比较经典的例子就是利用双重检查创建单例对象。(代码可以参考设计模式分类中的单例模式)
在Java创建对象的时候主要进行以下几个步骤:
(1)分配一块内存区域W;
(2)在内存W上初始化 Singleton 对象;
(3) 然后 W 的地址赋值给 instance 变量。
但是Java在编译的时候会进行编译优化,把方法的执行顺序修改成
(1)分配一块内存区域W;
(2)然后 W 的地址赋值给 instance 变量。
(3)在内存W上初始化 Singleton 对象;
如果线程A按照优化好的逻辑执行到第(2)步骤给开辟的内存地址分配给instance对象后,线程B的请求也打过来,此时getInstance()方法时 instance 是不等于null的,线程B会认为已经创建好单例对象,直接返回,后面的业务代码在进行对象操作的时候会出现空指针的问题。具体流程如下图所示。
以上是关于深入浅出多线程编程实战线程三大特性(原子性可见性有序性)的主要内容,如果未能解决你的问题,请参考以下文章
多线程&高并发深入理解JMM产生的三大问题原子性可见性有序性