volatile详解
并发编程有三大特性
- 可见行
- 原子性
- 有序性
首先说结论:volatile是JVM轻量级的同步机制;保证可见性,不保证原子性,禁止重排序;轻量级是相对于synchronized关键字(三大特性都满足)来说的;
补充一下JMM对同步的定义:
1.加锁前必须读取主内存中的最新值到工作内存
2.解锁前必须把共享变量的值刷新到主内存
3.加锁解锁必须是同一把锁
volatile-可见性
可见性:多个线程访问同一个共享变量的时候,一个线程对共享变量修改后,其他线程立即可见;
class Source {
//volatile boolean flag = true;
boolean flag = true;
volatile int i = 0;
}
@Test
public void testVisual() {
// 验证的逻辑:A线程对变量进行修改之后,对于主线程并不可见并不可见
// 当共享变量改变之后,对于主线程来说并不是可见的,程序无法停止
Source source = new Source();
source.flag = true;
new Thread(() -> {
// A 线程首先睡眠3秒钟,这主要是为了让主线程先运行
try {Thread.sleep(3000); } catch (InterruptedException e) {}
source.flag = false;
}, "A").start();
while (source.flag) {
}
}
程序的执行结果是:程序无法停止,一直卡死,
也就是说明了一个问题,A线程修改了共享变量,但是对于主线程不可见,所以程序无法正常终止
解决方案:在变量flag变量前增加volatile关键字,就可以正常终止程序的正常运行;
volatile-原子性
首先volatile不保证原子性,首先示例一下有问题的代码:
对Resource的成员变量i,采用10个线程同时Resource.i进行更新,每个线程i++执行1000次,看最终的结果
public void testAtomic() {
Thread.currentThread().getThreadGroup().list();
Source source = new Source();
// 10个线程,每个线程加1000次
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
source.addPlus();
}
}, "A" + i).start();
//idea会创建一个监控进程Thread[Monitor Ctrl-Break,5,main]
// 所以这边会判断2
while(Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.activeCount());
System.out.println("res " + source.i);
}
运行结果小于10000
我可把Resource.i加上volatile,我们再次运行时,结果还是少于10000;
着我们就验证了volatile不保证原子性
i++有三步了:
1.复制共享变量到工作内存
2.在工作内存中对共享变量进行操作
3.将工作内存共享变量副本刷入主内存
这个过程中会有写覆盖的问题
例如:线程A,B同时读区主内存的共享变量加载到工作内存,并且此时,线程A和线程B分别操作各自工作内存的共享变量副本,此时线程A先刷入主内存,因为volatile保证可见性,线程B的共享变量副本更新到最新值但是此时线程B需要执行3步,把旧数据的操作结果写入到主内存,此时发生了写覆盖;
volatile-有序性
volatile 有禁止指令重排序的作用;
对于这个我们只进行理论分析,
class Source {
boolean flag = false;
volatile int i = 0;
public void setValue() {
i = 1 ; ---- 1
flag = true; ------2
}
public void setValue2() {
if(flag) {
i = i + 5;
System.out.println(i);
}
}
}
前提创建两个线程,
线程A调用setValue方法,线程B调用setValue2方法,
执行线程A时发生重排序,可以先执行2后执行1,之所以会发生指令重排序,这两个操作之间没有数据的依赖性;
线程B可能出现 flag = true,但是此时的i = 0; 打印的结果是 5