JUC线程进阶篇02:volatile关键字与CAS算法
标签: 多线程
通过《JUC线程高级篇01:Java内存模型》,知道了主存和线程各自缓存之间的关系,以及并发的三大特点。下面聊聊Volatitle变量
Volatile变量
什么是Volatile
Java语言提供了一种比锁稍弱的同步机制:volatile变量。用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作仪器重排序。volatile变量不会被缓存在计算器或其他存储器不可见的地方,因此在读取volatile变量时总会返回最新写入的值。
———— 《Java并发编程实战》
也就是说,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile
修饰之后,那么就具备了两层语义:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
禁止进行指令重排序。
Volatile关键字是一个轻量级的同步机制,是一个比锁要弱,如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会使线程阻塞,引起线程上下文的切换和调度,这也需要很大的开销。
Volatile保证可见性
先看一段代码,假如线程1先执行,线程2后执行:
//线程1
boolean stop = false;
while(!stop){
System.out.println("do something");
}
//线程2
stop = true;
System.out.println("stop = " + stop);
当执行代码的时候,我们发现,我们已经打印了stop = true
,但是还是没有执行do somthing
。
这是因为while
执行的速度很快,线程2还没将stop = true
写回到内存中,就一直循环了。这就是前一篇说的不可见性。
但是用volatile修饰之后就变得不一样了:
//线程1
volatile boolean stop = false;
while(!stop){
System.out.println("do something");
}
//线程2
stop = true;
System.out.println("stop = " + stop);
可以从表面上认为,对线程对stop的操作是在主存中进行的,对其他线程是可见的。(底层不是这样的)
Volatile保证有序性
volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。这里有两层意思:
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
在以上代码中,由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,保证:语句1和语句2在语句3之前执行,语句4和语句5在语句3之后执行,但语句12、45之间的顺序不能保证,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
即写在volatile变量之前的语句执行完了,才执行在volatile变量,再执行后面的。
volatile不能确保原子性
public class TestAtomicDemo {
AtomicDemo ad = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(ad).start;
}
}
class AtomicDemo implements Runnable {
private volatile int serialNumber = 0;
public void run() {
System.out.println(getSerialNumber());
}
public int getSerialNumber() {
return serialNumber++;
}
}
输出会发现有线程安全问题。volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。
在i = i++
中,操作实际上分三步:
int temp = i;
temp = temp + 1;
i = temp;
自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行。
解决方案:可以通过synchronized或lock,进行加锁,来保证操作的原子性。也可以通过AtomicInteger。
在java 1.5的java.util.concurrent.atomic包下提供了一些原子变量,对原子变量进行操作都是原子性的。
这些原子变量都用volatile进行修饰,保证内存的可见性,使用CAS算法保证数据的原子性。
CAS算法
什么是CAS算法
CAS(Compare And Swap)算法是硬件对于并发操作共享数据的支持。CAS包含了三个操作数:
- 内存值 V
- 预估值 A
- 更新值 B
当且仅当 V==A 时,B = A,否则将不做任何操作。
- 即先从缓存中读取值为 V;
- 然后再从缓存读取值为 A ,若 V==A 则立即 B = A,若 V!=A 则什么都不做。
其中1是原子性操作,2是原子性操作。
如果线程1对变量进行了操作,在线程1没改变值之前,线程2获取该值。在线程2获改变值之前,线程1已经改变主存,则 V!=A ,线程2失败。
使用原子变量
关于java.util.concurrent.atomic包,详见:http://ifeve.com/java-atomic/
public class TestAtomicDemo {
AtomicDemo ad = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(ad).start;
}
}
class AtomicDemo implements Runnable {
private AtomicInteger int serialNumber = new AtomicInteger();
public void run() {
System.out.println(getSerialNumber());
}
public int getSerialNumber() {
return serialNumber.getAndIncrement;
}
}
CAS算法的效率比锁高,因为不会阻塞,不会放弃CPU,可以立即再尝试
模拟CAS算法
class CompareAndSwap {
private int value;
// 获取内存值
public synchronized int get() {
return value;
}
// 比较
public synchronized int compareAndSwap(int expecteValue,int new Value) {
int oldValue = value;
if (oldValue == expecteValue) {
this.value = newValue;
}
return oldValue;
}
// 设置
publicsynchronized boolwan compareAndSet(int expecteValue,int new Value) {
return expectedValue == compareAndSwap(expecteValue,new Value);
}
}
使用模拟的CAS算法
public class TestCompareAndSwap {
final CompareAndSwap = new CompareAndSwap();
for (int i - 0 ; i < 10 ; i++) {
new Thread(new Runnable() {
public void run() {
int expectedValue = cas.get();
boolean b = cas.compareAndSet(expectedValue,(int)(Math.random()*101));
System.out.println(b);
}
}).start();
}
}