电子说
线程间的通信一般有两种方式进行,一是通过消息传递
,二是共享内存
。Java 线程间的通信采用的是共享内存方式,JMM 为共享变量提供了线程间的保障。如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 JMM 规范下会有一系列的操作。我们直接来看下图:
在多线程的情况下,对主内存中的共享变量进行操作可能发生线程安全问题,比如:线程 1 和线程 2 同时对同一个共享变量进行操作,执行+1
操作,线程 1 、线程2 读取的共享变量是否是彼此修改前还是修改后的值呢,这个是无法确定的,这种情况和CPU的高速缓存与内存之间的问题非常相似
如何实现主内存与工作内存的变量同步,为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现:
工作内存即本地内存
。原子性:即一个或者多个操作作为一个整体,要么全部执行,要么都不执行,并且操作在执行过程中不会被线程调度机制打断;而且这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换(context switch) 比如:
int i = 0; //语句1,原子性
i++; //语句2,非原子性
语句1大家一幕了然,语句2却许多人容易犯迷糊,i++
其实可以分为3步:
执行上述3个步骤的时候是可以进行线程切换的,或者说是可以被另其他线程的 这3 步打断的,因此语句2
不是一个原子性操作
在 Java 中,可以借助synchronized
、各种 Lock
以及各种原子类实现原子性。synchronized
和各种Lock
是通过保证任一时刻只有一个线程访问该代码块,因此可以保证其原子性。各种原子类是利用CAS (compare and swap)
操作(可能也会用到 volatile
或者final
关键字)来保证原子操作。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。我们来看一个例子:
public class VisibilityTest {
private boolean flag = true;
public void change() {
flag = false;
System.out.println(Thread.currentThread().getName() + ",已修改flag=false");
}
public void load() {
System.out.println(Thread.currentThread().getName() + ",开始执行.....");
int i = 0;
while (flag) {
i++;
}
System.out.println(Thread.currentThread().getName() + ",结束循环");
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest test = new VisibilityTest();
// 线程threadA模拟数据加载场景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 让threadA执行一会儿
Thread.sleep(1000);
// 线程threadB 修改 共享变量flag
Thread threadB = new Thread(() -> test.change(), "threadB");
threadB.start();
}
}
threadA 负责循环,threadB负责修改 共享变量flag
,如果flag=false时,threadA 会结束循环,但是上面的例子会死循环。原因是threadA无法立即读取到共享变量flag修改后的值。我们只需 private volatile boolean flag = true;
加上volatile
关键字threadA就可以立即退出循环了。
Java中的volatile关键字
提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。
因此,可以使用volatile
来保证多线程操作时变量的可见性。除了volatile
,Java中的synchronized
和final
两个关键字 以及各种 Lock也可以实现可见性。
有序性:即程序执行的顺序按照代码的先后顺序执行。
int i = 0;
int j = 0;
i = 10; //语句1
j = 1; //语句2
但由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。语句可能的执行顺序如下:
指令重排对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。 指令重排不会影响单线程的执行结果,但是会影响多线程并发执行的结果正确性 。在Java 中,可以通过volatile关键字
来禁止指令进行重排序优化,详情可见:https://mp.weixin.qq.com/s/TyiCfVMeeDwa-2hd9N9XJQ。也可以使用synchronized关键字
保证同一时刻只允许一条线程访问程序块。
参考资料:
《java并发编程实战》
https://www.cnblogs.com/czwbig/p/11127124.html
https://www.cnblogs.com/jelly12345/p/14609657.html
https://www.cnblogs.com/bailiyi/p/11967396.html
全部0条评论
快来发表一下你的评论吧 !