一、内存可见性
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问题。