1 CAS概念
CAS(Compare and swap)比较和交换,属于硬件同步语,从硬件层面保证了原子性。
CAS(V,A,B)包含三个参数,分别为内存位置(V)、旧值(A)、新值(B),在操作期间先对旧值进行比较,若没有发生变化,则交换为新值,若发生了则不交换。
1.1 原子操作
原子性,即原子性操作,原子操作可以是一个步骤,也可以是多个步骤,但是其顺序不能被打乱,也不可以被切割,在整个步骤中资源要保持一致。
具体实例:
public class Counter {
volatile int i = 0;
public void add() {
i++;
}
}
public class CounterTest {
public static void main(String[] args) throws InterruptedException {
final Counter ct = new Counter();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
ct.add();
}
System.out.println("done...");
}
}).start();
}
Thread.sleep(6000L);
System.out.println(ct.i);
}
}
循环开启十个线程,每个线程循环i++操作,按道理来说,循环结束时i的值应该为100000,可是实际操作中,绝大部分值小于100000。为什么呢,这里就要提到原子操作了,虽然i++循环了十万次,但是i++并不是一个原子性操作。
于idea终端(Terminal)反编译查看i++的二进制字节码:
1:cd进入Counter类target下的目录,我这里是
cd D:\workspace_2019\subject-1\target\classes\com\study\cas
2:javap -v -p Counter.class命令进行反编译
这里我们看到反编译后的add方法:
public void add();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 7: 0
line 8: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/study/cas/Counter;
重点是这四句:
2: getfield #2
5: iconst_1
6: iadd
7: putfield #2
- getfield:获取对象字段的值,即获取i的初始值并入栈。
- iconst_1:1(int)值入栈,即将数值1压入栈。
- iadd: 将栈顶两int类型数相加,结果入栈。
- putfield:给对象字段赋值,即将相加后的值重新赋给对象i。
那么又为什么会产生原子性问题呢?
可以看到当多个线程同时运行时,线程1还未来得及将累加后的值重新赋给变量i,线程2又拿到了i的初始值,这就会导致两个线程执行完后i的值本应该是2,实际却为1,违反了不可被切割原则,资源被改变,这就是问题产生的原因。
2 CAS实现
Java中的sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSwapLong()等几个方法来实现CAS。
修改Counter类为CounterUnsafe类:
public class CounterUnsafe {
volatile int i = 0;
private static Unsafe unsafe = null;
//i字段的偏移量
private static long valueOffset;
static {
//unsafe = Unsafe.getUnsafe();
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
Field fieldi = CounterUnsafe.class.getDeclaredField("i");
valueOffset = unsafe.objectFieldOffset(fieldi);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public void add() {
for (;;){
int current = unsafe.getIntVolatile(this, valueOffset);
if (unsafe.compareAndSwapInt(this, valueOffset, current, current+1))
break;
}
}
}
Unsafe不能直接使用,只能通过反射获取,调用compareAndSwapInt方法,传入偏移量,旧值,新值,实现原子性操作。
此时,重新测试:
public class CounterTest {
public static void main(String[] args) throws InterruptedException {
final CounterUnsafe ct = new CounterUnsafe();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
ct.add();
}
System.out.println("done...");
}
}).start();
}
Thread.sleep(6000L);
System.out.println(ct.i);
}
}
多次执行结果都是100000正确值。
JDK中J.U.C包提供了许多Atomic原子操作类,封装了CAS方法以供调用,如AtomicInteger,AtomicIntegerArray,AtomicStampedReference等等。
通过AtomicInteger来解决CAS问题:
public class CounterAtomic {
//volatile int i = 0;
AtomicInteger i = new AtomicInteger(0);
public void add() {
i.incrementAndGet();
}
}
public class CounterTest {
public static void main(String[] args) throws InterruptedException {
final CounterAtomic ct = new CounterAtomic();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
ct.add();
}
System.out.println("done...");
}
}).start();
}
Thread.sleep(6000L);
System.out.println(ct.i);
}
}
多次运行结果都为100000。
查看源码可发现AtomicInteger类中就是使用了unsafe的CAS操作。
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
3 CAS问题
尽管CAS解决了原子性问题,但也不可避免存在了一些问题。
- 自旋的实现会增加CPU的资源消耗
- 仅能针对单个变量,不能用于多个变量来实现原子操作
- ABA问题
3.1 ABA问题原理
当线程1执行完第三步,线程2再执行时,原本应该失败的CAS操作却成功了,这就是ABA问题,因为变量i的值从A变成B之后又变回了A,而CAS操作只识别旧值A没变,为了解决这一问题,就要引入版本号的概念。
3.2 ABA解析
AtomicStampedReference类提供了版本号参数避免ABA问题。
如图,栈中原有A,B元素,线程1线程2都想要TopA,线程2先运行,当线程2运行结束时,栈顶依旧是A,线程1执行Top CAS(A,B)依旧可以成功,但这是错误的,A.next此时已经变成了C。
错误示例代码如下:
// 实现一个 栈(后进先出)
public class Stack {
// top cas无锁修改
AtomicReference<Node> top = new AtomicReference<Node>();
public void push(Node node) { // 入栈
Node oldTop;
do {
oldTop = top.get();
node.next = oldTop;
}
while (!top.compareAndSet(oldTop, node)); // CAS 替换栈顶
}
// 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
public Node pop(int time) {
Node newTop;
Node oldTop;
do {
oldTop = top.get();
if (oldTop == null) { //如果没有值,就返回null
return null;
}
newTop = oldTop.next;
if (time != 0) { //模拟延时
LockSupport.parkNanos(1000 * 1000 * time); // 休眠指定的时间
}
}
while (!top.compareAndSet(oldTop, newTop)); //将下一个节点设置为top
return oldTop; //将旧的Top作为值返回
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
Stack stack = new Stack();
// ConcurrentStack stack = new ConcurrentStack();
stack.push(new Node("B")); //B入栈
stack.push(new Node("A")); //A入栈
Thread thread1 = new Thread(() -> {
Node node = stack.pop(800);
System.out.println(Thread.currentThread().getName() +" "+ node.toString());
System.out.println("done...");
});
thread1.start();
Thread thread2 = new Thread(() -> {
LockSupport.parkNanos(1000 * 1000 * 300L);
Node nodeA = stack.pop(0); //取出A
System.out.println(Thread.currentThread().getName() +" "+ nodeA.toString());
Node nodeB = stack.pop(0); //取出B,之后B处于游离状态
System.out.println(Thread.currentThread().getName() +" "+ nodeB.toString());
stack.push(new Node("C")); //C入栈
stack.push(nodeA); //A入栈
System.out.println("done...");
});
thread2.start();
LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);
System.out.println("开始遍历Stack:");
Node node = null;
while ((node = stack.pop(0))!=null){
System.out.println(node.value);
}
}
}
运行程序后发现,A.next指向了游离的B。为了解决ABA问题,AtomicStampedReference提供了版本号的引入,就算线程2再次将A放入栈顶,但由于版本号变化,CAS操作将不能再执行成功。
将Stack类替换为使用了版本号的ConcurrentStack类再次测试:
public class ConcurrentStack {
// top cas无锁修改
//AtomicReference<Node> top = new AtomicReference<Node>();
AtomicStampedReference<Node> top =
new AtomicStampedReference<>(null, 0);
public void push(Node node) { // 入栈
Node oldTop;
int v;
do {
v = top.getStamp();
oldTop = top.getReference();
node.next = oldTop;
}
while (!top.compareAndSet(oldTop, node, v, v+1)); // CAS 替换栈顶
}
// 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
public Node pop(int time) {
Node newTop;
Node oldTop;
int v;
do {
v = top.getStamp();
oldTop = top.getReference();
if (oldTop == null) { //如果没有值,就返回null
return null;
}
newTop = oldTop.next;
if (time != 0) { //模拟延时
LockSupport.parkNanos(1000 * 1000 * time); // 休眠指定的时间
}
}
while (!top.compareAndSet(oldTop, newTop, v, v+1)); //将下一个节点设置为top
return oldTop; //将旧的Top作为值返回
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
// Stack stack = new Stack();
ConcurrentStack stack = new ConcurrentStack();
stack.push(new Node("B")); //B入栈
stack.push(new Node("A")); //A入栈
Thread thread1 = new Thread(() -> {
Node node = stack.pop(800);
System.out.println(Thread.currentThread().getName() +" "+ node.toString());
System.out.println("done...");
});
thread1.start();
Thread thread2 = new Thread(() -> {
LockSupport.parkNanos(1000 * 1000 * 300L);
Node nodeA = stack.pop(0); //取出A
System.out.println(Thread.currentThread().getName() +" "+ nodeA.toString());
Node nodeB = stack.pop(0); //取出B,之后B处于游离状态
System.out.println(Thread.currentThread().getName() +" "+ nodeB.toString());
stack.push(new Node("C")); //C入栈
stack.push(nodeA); //A入栈
System.out.println("done...");
});
thread2.start();
LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);
System.out.println("开始遍历Stack:");
Node node = null;
while ((node = stack.pop(0))!=null){
System.out.println(node.value);
}
}
}
此时运行结束,A.next指向正确的C,从而解决ABA问题。
4 线程安全的相关概念
CAS的存在避免了原子性问题,也就是保证了线程的安全,另一安全性问题----可见性,这里不做描述,主要说几个名词概念。
- 竞态条件:如果程序运行顺序的改变会影响最终结果,就说存在竞态条件。大多数竞态条件的本质,就是基于某种可能失效的观察结果来做出判断或执行某个计算。
- 临界区:存在竞态条件的代码区域就叫临界区。