概述
volatile是java虚拟机提供的轻量级同步机制,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级。
特性
- 保证可见性
- 不保证原子性
- 禁止指令重排
JMM java内存模型
- 可见性
- 原子性
- 有序性
可见性
指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
各个线程对主内存中共享变量的操作(读取和赋值)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成,再将变量写回主内存。各线程都有变量的副本拷贝,不同线程之间不能访问对方工作内存空间。线程间通信必须通过主内存完成。
可见性证明Demo:
如果变量没有volatile,主线程会卡死,如果加上volatile,会正常结束程序
class MyData{
public volatile int num = 0;
public void addNum(){
num = 60;
}
}
public class TestVolatileDemo {
public static void main(String[] args) {
//volatile可以保证可见性,及时通知其他线程,主物理内存及时修改。
seeOKByVolatile();
}
public static void seeOKByVolatile() {
//共享资源
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\tcome in");
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
}
myData.addNum();
System.out.println("update complete num: " + myData.num);
}, "AAA").start();
//另外一个线程是main线程
while (myData.num == 0){
}
System.out.println(Thread.currentThread().getName() + " is over");
}
}
原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作
(1)基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操作。
(2)所有引用reference的赋值操作
(3)java.concurrent.Atomic.* 包中所有类的一切操作
volatile不保证原子性Demo:
class MyData{
public volatile int num = 0;
//synchronized 可以保证线程操作统一资源类安全,但是大材小用。
public void addPlus(){
num ++;
}
}
public class TestVolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
//创建20个线程
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000 ; j++) {
myData.addPlus();
}
}, String.valueOf(i)).start();
}
//线程全部计算完之后 开始向下进行。后台默认两个线程,1.main线程 2.GC线程
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(myData.num);
}
}
不满足原子性的原因就是线程自己的工作内存执行了三步如图:
- 读取值
- 值+1
- 赋值
这里借助其他文章例子:
一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。
线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。
那么如果解决原子性问题,如果synchronized有点大材小用。所以可以使用atomic包下的AtomicInteger
//最好的解决办法是用atomic下的的AtomicInteger
AtomicInteger atomicInteger = new AtomicInteger();
public void atomicPlus(){
//类似i++
atomicInteger.getAndIncrement();
//类似++i atomicInteger.incrementAndGet();
//对应+特殊值 atomicInteger.getAndAdd()
}
有序性
即程序执行的顺序按照代码的先后顺序执行。
处理器在进行重排序时必须考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致是无法确定的,结果无法预测。
volatile的作用就是禁止指令重排优化
部分内容参考
https://blog.csdn.net/u012723673/article/details/80682208