一、内存可见性
Java Memory Model (JAVA 内存模型)描述线程之间如何通过内存(memory)来进行交互。 具体说来, JVM中存在一个主存区(Main Memory或Java Heap Memory),对于所有线程进行共享,而每个线程又有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。
package com.dason.juc; /* * 一、volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见。 * 相较于 synchronized 是一种较为轻量级的同步策略。 * * 注意: * 1. volatile 不具备“互斥性” * 2. volatile 不能保证变量的“原子性” */ public class TestVolatile { public static void main(String[] args) { ThreadDemo td = new ThreadDemo(); new Thread(td).start(); while(true){ if(td.isFlag()){ System.out.println("------------------"); break; } } } } class ThreadDemo implements Runnable { //保证了内存可见性 private volatile boolean flag = false; @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { } flag = true; System.out.println("flag=" + isFlag()); } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } }
二、缺陷
Volatile虽然保证了线程中变量内存的可见性,但是他不能实现线程之间的互斥性以及变量的原子性。互斥性的实现通常我们采用的是同步的方式(其实同步Synchronized就已经保证了这三个特性,因为它拒绝同一时刻多个线程对同一资源的访问,Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。)。那么什么是原子性呢?即不可再分割,不可被中断。
在 java.util.concurrent.atomic 包下提供了一些原子变量。使用Volatile保证其内存可见性,采用CAS算法保证数据变量的原子性。其中以Atomic开头的包装类。例如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。
package com.dason.juc; import java.util.concurrent.atomic.AtomicInteger; /* * 一、i++ 的原子性问题:i++ 的操作实际上分为三个步骤“读-改-写” * int i = 10; * i = i++; //10 * * int temp = i; * i = i + 1; * i = temp; * * 二、原子变量:在 java.util.concurrent.atomic 包下提供了一些原子变量。 * 1. volatile 保证内存可见性 * 2. CAS(Compare-And-Swap) 算法保证数据变量的原子性 * CAS 算法是硬件对于并发操作的支持 * CAS 包含了三个操作数: * ①内存值 V * ②预估值 A * ③更新值 B * 当且仅当 V == A 时, V = B; 否则,不会执行任何操作。 */ public class TestAtomicDemo { public static void main(String[] args) { AtomicDemo ad = new AtomicDemo(); for (int i = 0; i < 10; i++) { new Thread(ad).start(); } } } class AtomicDemo implements Runnable{ //不保证变量的原子性 // private volatile int serialNumber = 0; private AtomicInteger serialNumber = new AtomicInteger(0); @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { } System.out.println(getSerialNumber()); } public int getSerialNumber(){ // return serialNumber++; return serialNumber.getAndIncrement(); } }
那么问题又来了,什么又是CAS算法呢?
三、CAS算法
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值B,要修改的新值B。
当且仅当 V == A 时, V = B; 否则,不会执行任何操作。
举例:
1.在内存地址V当中,存储着值为10的变量。
2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
线程1:A = 10 B = 11
3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
线程2:V = 11
4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
线程1:A = 10 B = 11(此时 V = 11)
A != V 失败
5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
A = 11 B = 12
6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
A == V
7.线程1进行SWAP,把地址V的值替换为B,也就是12。
V = B
package com.dason.juc; /* * 模拟 CAS 算法(没有实现自旋) */ public class TestCompareAndSwap { public static void main(String[] args) { final CompareAndSwap cas = new CompareAndSwap(); /** * 可能成功,可能失败。 * 失败的时候,第一个线程在比较的时候,另一个线程就将其修改了 */ for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { int expectedValue = cas.get(); boolean b = cas.compareAndSet(expectedValue, (int)(Math.random() * 101)); System.out.println(b); } }).start(); } } } class CompareAndSwap{ private int value; //获取内存值 public synchronized int get(){ return value; } //比较 无论成功与否都返回旧值 public synchronized int compareAndSwap(int expectedValue, int newValue){ int oldValue = value;//1.拿到内存地址V if(oldValue == expectedValue){//2.比较预期值与内存值 this.value = newValue;//3.将内存值替换为新值 } return oldValue; } //设置: 成功就替换并返回true,失败返回false public synchronized boolean compareAndSet(int expectedValue, int newValue){ return expectedValue == compareAndSwap(expectedValue, newValue); } }CAS的缺点:
1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
3.ABA问题
这是CAS机制最大的问题所在。
什么是ABA问题?怎么解决?请参见 ABA问题。