一、可见性问题:
前一篇文章大概解释了JMM,此时若有两个线程A,B,A读取了变量x的值,同时线程B对主内存中的变量x进行修改,B需要先将其读入自己的工作内存,在自己的工作内存中对变量x进行修改之后,再写入到主内存中,若此时cpu一直处于占用状态,那么修改的x变量对于A是不可见的(每个线程有自己的工作内存),此时就出现了可见性问题。可以使用volatile关键字解决。
volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见。相较于 synchronized 是一种较为轻量级的同步策略。
volatile关键字原理:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当某一线程对变量进行修改时,会导致其他线程的工作内存中缓存变量的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于其他线程的工作内存中缓存变量的缓存行无效,所以其他线程再次读取变量的值时会去主存读取。
synchronized与volatile关键字的区别:
volatile 只具备“可见性”
synchronized 具备“可见性”、“互斥性”、“原子性”
注意:
1. volatile 不具备“互斥性”
2. volatile 不能保证变量的“原子性”
synchronized和lock都具备“可见性”、“互斥性”、“原子性”,
1. 线程解锁前,必须把共享变量的最新值刷新到主内存中
2. 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
二、原子性问题
i++ 的操作实际上分为三个步骤“读-改-写”,原子性问题是由于对共享变量执行多个操作(读、改),而可见性问题只对共享变量执行一个操作并且不会再次从主存中加载数据
i++;可以理解为:
(1)int temp = i;
(2)i = i + 1;
同样此时两个线程A,B,主内存中的变量i初始值为0,同时对其进行i++,A先将i读入自己的工作内存,再创建temp=0,此时CPU执行B线程,B将i读入自己的工作内存,创建temp=0,然后B执行i=i+1后写回主内存中时(i=1),此时CPU再来执行A线程,执行i=i+1(由于工作内存中的i为0),所以主内存中i=1,由于没有1,2两步分开执行了,此时就出现了原子性问题。
解决方案:
原子变量:在 java.util.concurrent.atomic 包下提供了一些原子变量(AtomicInteger等)。
* 1. volatile 保证内存可见性(原子变量里的变量都使用的是volatile修饰的)
* 2. CAS(Compare-And-Swap) 算法保证数据变量的原子性,通过unsafe类(提供了硬件级别的原子操作)来实现。
* CAS 算法是硬件对于并发操作的支持
* CAS 包含了三个操作数:
* ①内存值 V(旧值)
* ②预估值 A
* ③更新值 B
* 当且仅当 V == A 时, V = B; 否则,不会执行任何操作。(与乐观锁相似)
CAS由处理器cmpxchg指令实现
CAS漏洞:“ABA问题”;
解决方法:
java并发包中提供了一个带有标记的原子引用类"AtomicStampedReference",它可以通过控制变量值的版本来保证CAS的正确性(与乐观锁相似)。