文章目录
1 什么是 JUC 原子操作封装类2 JUCA 的应用场景3 锁与 JUCA 的选择问题4 为什么使用 JUCA5 JUCA 源码分析5.1 原子操作类总纲5.2 Atomic 类5.3 AtomicArray 类5.4 AtomicFieldUpdater 类5.5 AtomicReference 类5.6 多操作单元 Atomic 类5.7 原子操作类总结
6 CAS 浅析6.1 什么是 CAS6.2 CAS 的应用场景6.3 为什么要用 CAS6.4 CAS 原理介绍6.5 CAS 存在的问题6.6 ABA 问题
参考
1 什么是 JUC 原子操作封装类
原子操作封装类是 JUCA(java.util.concurrent.atomic)包下的类的总称都是基于 CAS(compare and swap) 实现的它可以将单个变量的赋值操作封装成原子性操作
2 JUCA 的应用场景
多线程下,不使用锁,实现单个变量的赋值操作
3 锁与 JUCA 的选择问题
极其高的并发下,可能有许多线程一直做循环 CAS,导致 CPU 无效使用率太高,进一步导致赋值操作效率越来越低
一个线程设置完,其它线程如果都读取到旧值,后面争取 CPU 和比较操作都是无意义的消耗,并且一个线程可能有好多次的这种无意义的消耗而如果使用锁,就不存在读取旧值、比较操作的 CPU 消耗,只有加锁/释放锁的消耗,并且只有固定次数的锁消耗 上面说的极其高也没有一个具体的说法,并且随着 synchornized 锁的不断优化,原生锁的性能也会越来越高只是 JUCA 包下的类使用会更简洁,并不一定性能就高具体场景可以综合测试最后做出选择,一般没必要纠结,最赋值操作就简单的选择 JUCA
4 为什么使用 JUCA
提供了多线程下单个变量赋值的原子性操作使用简单
5 JUCA 源码分析
5.1 原子操作类总纲
5.2 Atomic 类
Atomic 类有三种子类,分别代表原子更新的布尔类型、整型、长整型AtomicBoolean 方法解析:6个方法
内部通过 volatile int value 属性的 CAS 操作实现原子性操作可见 Boolean 属性最终转换为 int 属性来处理了
AtomicBoolean ab
= new AtomicBoolean(true);
ab
.compareAndSet(true,false);
ab
.weakCompareAndSet(true,false);
ab
.get();
ab
.getAndSet(true);
ab
.lazySet(false);
ab
.set(true);
AtomicInteger 方法解析:16个方法
内部通过 volatile int value 属性的 CAS 操作实现原子性操作相比AtomicBoolean类,多了很多赋值方法
AtomicInteger ai
= new AtomicInteger(0);
ai
.getAndIncrement();
ai
.incrementAndGet();
ai
.getAndDecrement();
ai
.decrementAndGet();
ai
.getAndAdd(2);
ai
.addAndGet(2);
ai
.getAndAccumulate(6,Math
::max
);
ai
.accumulateAndGet(4,(x
,y
)->{
return x
+y
;
});
ai
.getAndUpdate(Math
::abs
);
ai
.updateAndGet((x
)->{
return x
*x
;
});
ai
.get();
ai
.getAndSet(2);
ai
.set(3);
ai
.compareAndSet(1,2);
ai
.lazySet(3);
ai
.weakCompareAndSet(3,3);
AtomicLong 方法解析:和 AtomicInteger 方法一模一样,只是 value 的属性从 int 变成了 long
5.3 AtomicArray 类
实现数组中元素的原子性更新操作
AtomicArray 类内部维护一个 final 修饰的 array 数组
array 数组并没有被 volatile 修饰即使数组被 volatile 修饰,只是数组的引用可见,数组的内部元素并不可见通过 unsafe.getIntVolatile(…) 方法保证 array 数组中元素的可见性的
其余的方法和 Atomic 类是一样的,只是所有方法都多了一个 index 属性,表明操作数组的哪一位
AtomicReferenceArray:原子更新的是引用类型的引用(即只能引用重新指向),而不是引用类型内部的值
5.4 AtomicFieldUpdater 类
实现目标类中的字段的原子性更新操作
AtomicFieldUpdater 类内部维护一个 final 修饰的 offset 变量(通过反射得到类中 filed 属性对应的偏移量)
通过偏移量,使用 CAS 来实现原子操作,操作和 Atomic 类一样
目标类中的对应字段必须被 volatile 修饰,且不能被 private 修饰
AtomicIntegerFieldUpdater AIFU
= AtomicIntegerFieldUpdater
.newUpdater(Target
.class,"age");
Target target
= new Target("wk",24);
aifu
.accumulateAndGet(target
,56,(x
,y
)->{
return x
+y
;
});
System
.out
.println(aiuf
.getAge());
AtomicLongFieldUpdater ALFU
= AtomicLongFieldUpdater
.newUpdater(Target
.class,"time");
AtomicReferenceFieldUpdater ARFU
= AtomicReferenceFieldUpdater
.newUpdater(Target
.class,man
.class,"man");
5.5 AtomicReference 类
AtomicReference 方法解析:10个方法
和 AtomicInteger 类似,只是这个原子更新的是引用类型的引用地址
AtomicReference ar
= new AtomicReference();
System
.out
.println(ar
.get());
ar
.compareAndSet(null
,"string");
System
.out
.println(ar
.get());
AtomicFieldUpdater
.aiuf a
= new AtomicFieldUpdater.aiuf("wk",22);
AtomicFieldUpdater
.aiuf b
= new AtomicFieldUpdater.aiuf("wkk",222);
AtomicReference ar1
= new AtomicReference(a
);
System
.out
.println(ar1
.get());
ar1
.compareAndSet(a
,b
);
System
.out
.println(ar1
.get());
AtomicStampedReference 方法解析:
AtomicFieldUpdater
.aiuf a
= new AtomicFieldUpdater.aiuf("wk",22);
AtomicFieldUpdater
.aiuf b
= new AtomicFieldUpdater.aiuf("wkk",222);
AtomicStampedReference asr
= new AtomicStampedReference(a
,0);
boolean flag
= asr
.attemptStamp(a
,1);
System
.out
.println(flag
);
asr
.compareAndSet(a
,b
,1,2);
int[] stampHolder
= new int[1];
AtomicFieldUpdater
.aiuf aa
= (AtomicFieldUpdater
.aiuf
)asr
.get(stampHolder
);
System
.out
.println(aa
);
asr
.getReference();
asr
.getStamp();
asr
.set(a
,2);
asr
.weakCompareAndSet(a
,b
,2,3);
5.6 多操作单元 Atomic 类
多操作单元类:
其他 Atomic 类中更新的只是一个变量多操作变量,更新的是多个变量,最后取的时候再合并这多个变量这样就提高了 Atomic 类的更新速度(多个线程可以同时更新不同的单元)但是读取时,需要综合所有单元的值,所以适合写多读少的场景 DoubleAccumulator 方法解析
这个类操作的是 double 类型的数据,有4个方法源码过于难理解,并且获取的值还不准确,可能用于不精确的多更新操作吧
DoubleAccumulator da
= new DoubleAccumulator((x
,y
)->{
return x
*y
;
},1.2);
da
.accumulate(3);
da
.get();
da
.reset();
da
.getThenReset();
LongAccumulator 方法解析
和 DoubleAccumulator 的区别仅仅在于返回值的类型变成了 long DoubleAdder 方法解析
可以当作 DoubleAccumulator 的子类,它的 accumulator 为 二元相加运算符值也不准确 LongAdder 方法解析
可以当作 LongAccumulator 的子类,它的 accumulator 为 二元相加运算符值也不准确
5.7 原子操作类总结
普通 Atomic 类,是通过 volatile 修饰 value 属性,来保证可见性的AtomicArray类,是通过 getXXXVolatile()、putXXXVolatile() 方法,来保证数组内部元素的可见性的所有的原子类操作,最后大多会调用 unsafe.compareAndSwapXXX(this,offset,old,new) 方法
XXX 的种类:Int、Long、Objectoffset:偏移量(属性具体的地址)old:预期的值new:要更新成的值
6 CAS 浅析
6.1 什么是 CAS
CAS 是 compare and swap 的简称,意为:比较并替换执行 CAS 操作时,当且仅当内存地址 V 的值与预期值 A 相同时,将内存地址 V 的值修改为 B,否则就什么都不做。整个比较过程是原子性操作
6.2 CAS 的应用场景
大量用于 JUCA 包中,来做原子性更新变量的操作对单个变量做原子性更新的操作
6.3 为什么要用 CAS
通过 CAS 可以实现无锁同步,避免锁带了的消耗(当然也存在一些问题)
6.4 CAS 原理介绍
通过 AtomicInteger 类源码来理解 CAS
AtomicInteger ai
= new AtomicInteger(0);
ai
.getAndIncrement();
public final int getAndIncrement() {
return unsafe
.getAndAddInt(this, valueOffset
, 1);
}
public final int getAndAddInt(Object var1
, long var2
, int var4
) {
int var5
;
do {
var5
= this.getIntVolatile(var1
, var2
);
} while(!this.compareAndSwapInt(var1
, var2
, var5
, var5
+ var4
));
return var5
;
}
public final native boolean compareAndSwapInt(Object var1
, long var2
, int var4
, int var5
);
以上代码是循环 CAS 操作,单纯的 CAS 只有两个步骤
步骤1:获取偏移量地址的值,作为预期值步骤2:比较偏移量地址的值(这时该地址的值可能已被别的线程修改了)和预期值,如果相等将该地址的值设置为 更新值,设置成功返回 true,否则返回 false 通过以上代码可以看出,CAS 需要 4 个参数:对象地址、偏移量、预期值、更新值如果对象地址对应的偏移量地址的值 和 预期值相同,就将这个 偏移量地址的值 设置为 更新值
6.5 CAS 存在的问题
如果要用 循环 CAS 来代替加锁操作,可能要循环很久才会成功
只能对一个变量进行操作,如果要对多个变量操作,必须要加锁
ABA 问题
6.6 ABA 问题
- 有一个变量 p
= 4
- 线程
1 通过步骤
1读取了 p 的预期值
4
- 线程
2 通过步骤
1读取了 p 的预期值
4,并通过步骤
2成功将其设置成
9
- 线程
3 通过步骤
1读取了 p 的预期值
9,并通过步骤
2成功将其设置成
4
- 线程
1 通过步骤
2成功将其设置成了
21
虽然这里线程
1成功 CAS 了,但是我们真实意图其实线程
1应该 CAS 失败的
因为步骤
2成功的标准是 p 的值没有变,没有变真正含义,应该是没有变动
而 p 的值虽然最终没有变,但它变动了
因为以上语义问题导致的经典 ABA 问题
- 有一个栈,目前的元素为:A
->B
->C
->null
, A 为 top,即 top 指针指向 A
- 现成我们通过 CAS 做出栈入栈操作
- 线程
1 做 pop 操作,首先取到 top 指向的值 为A,并且取到 A 的下一个节点 B
- 然后 线程
1 准备(注意:是准备,但是没有做)做 CAS 操作,即
CAS(&top
,A
,B
):如果top指针指向的仍然是 A 的话,则把 top 指针指向 B
- 这时,线程
2 做了一次完整的 pop 操作,此时栈的元素为:B
->C
->null
- 线程
2 又做了一次完整的 pop 操作,此时栈的元素为:C
->null
- 接着,线程
3 做了一次 完整的
push(A
) 操作,此时栈的元素为:A
->C
->null
,现在 top 又指向 A 了
- 这时,线程
1 终于正式
CAS(&top
,A
,B
)了,成功执行!!!把 top 执行了 B,这样栈的元素为:B
->null
- 问题所在:原本的栈:A
->C
->null,突然变为 B
->null。这时一个很严重的问题
- 最关键的是,线程
1 最后的 CAS 操作不应该成功,线程
1 应该重新开始 pop,最终的栈也应该是:C
->null!!!
java 源码模拟以上问题
public class CasStack {
AtomicReference
<Node> top
= new AtomicReference<>();
public void push(Node node
) {
Node oldTop
;
do {
oldTop
= top
.get();
node
.next
= oldTop
;
} while (!top
.compareAndSet(oldTop
, node
));
}
public Node
pop() {
Node newTop
;
Node oldTop
;
do {
oldTop
= top
.get();
if (oldTop
== null
) {
return null
;
}
newTop
= oldTop
.next
;
LockSupport
.parkNanos(1000 * 1000 * 5);
} while (!top
.compareAndSet(oldTop
, newTop
));
oldTop
.next
= null
;
return oldTop
;
}
}
class Node {
public String value
;
public Node next
;
public Node(String value
) {
this.value
= value
;
}
}
ABA 问题解决方案
一般通过额外添加一个版本号属性,比较值的同时比较版本号,如果两个都相同才 CAS 替换
public class ConcurrentStack {
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));
}
public Node
pop() {
Node newTop
;
Node oldTop
;
int v
;
do {
v
= top
.getStamp();
oldTop
= top
.getReference();
if (oldTop
== null
) {
return null
;
}
newTop
= oldTop
.next
;
LockSupport
.parkNanos(1000 * 1000 * 5);
}
while (!top
.compareAndSet(oldTop
, newTop
, v
, v
+1));
oldTop
.next
= null
;
return oldTop
;
}
}
使用同一个测试方法
public class Test {
public static void main(String
[] args
) throws InterruptedException
{
Stack stack
= new Stack();
stack
.push(new Node("B"));
stack
.push(new Node("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);
System
.out
.println(Thread
.currentThread().getName() +" "+ nodeA
.toString());
Node nodeB
= stack
.pop(0);
System
.out
.println(Thread
.currentThread().getName() +" "+ nodeB
.toString());
stack
.push(new Node("D"));
stack
.push(new Node("C"));
stack
.push(nodeA
);
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
);
}
}
}
参考
JDK 1.8u171