什么是CAS
- CAS的全称是Compare And Swap , 顾名思义就是比较和交换
- 他是一个乐观锁, 是解决多线程并行情况下使用重量级锁造成性能损耗的一种机制
- CAS操作包含三个操作数:
- 内存值
- 预期原值
- 新值
- 如果内存值的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值并且返回true。否则,处理器不做任何操作返回false。
原理剖析
- 在JAVA中,sun.misc.Unsafe 类提供了硬件级别的原子操作(大量native方法)来实现这个CAS。
public final class Unsafe {
private static final Unsafe theUnsafe;
private static native void registerNatives();
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
public native int getInt(Object var1, long var2);
public native void putInt(Object var1, long var2, int var4);
public native Object getObject(Object var1, long var2);
public native void putObject(Object var1, long var2, Object var4);
public native boolean getBoolean(Object var1, long var2);
public native void putBoolean(Object var1, long var2, boolean var4);
public native byte getByte(Object var1, long var2);
public native void putByte(Object var1, long var2, byte var4);
public native short getShort(Object var1, long var2);
public native void putShort(Object var1, long var2, short var4);
public native char getChar(Object var1, long var2);
public native void putChar(Object var1, long var2, char var4);
public native long getLong(Object var1, long var2);
public native void putLong(Object var1, long var2, long var4);
public native float getFloat(Object var1, long var2);
public native void putFloat(Object var1, long var2, float var4);
public native double getDouble(Object var1, long var2);
public native void putDouble(Object var1, long var2, double var4);
}
- java.util.concurrent 包下的大量类都使用了这个 Unsafe.java 类的CAS操作。
CAS的使用
- java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现, 下面以 AtomicInteger.java 的部分实现来大致讲解下这些原子类的实现。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private volatile int value;// 初始int大小
// 省略了部分代码...
// 带参数构造函数,可设置初始int大小
public AtomicInteger(int initialValue) {
value = initialValue;
}
// 不带参数构造函数,初始int大小为0
public AtomicInteger() {
}
// 获取当前值
public final int get() {
return value;
}
// 设置值为 newValue
public final void set(int newValue) {
value = newValue;
}
//返回旧值,并设置新值为 newValue
public final int getAndSet(int newValue) {
/**
* 这里使用for循环不断通过CAS操作来设置新值
* CAS实现和加锁实现的关系有点类似乐观锁和悲观锁的关系
* */
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}
// 原子的设置新值为update, expect为期望的当前的值
public final boolean compareAndSet(int expect, int update) {
//本质上还是使用unsafe的native方法实现的, 就是硬件的原子操作帮助实现CAS
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// 获取当前值current,并设置新值为current+1
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
// 此处省略部分代码,余下的代码大致实现原理都是类似的
}
ABA问题
- 这个问题就是加入现在有个num为0 有一个线程把他修改为1, 然后紧接着又有一个线程把他修改为0了 那此时仅仅通过CAS的比较是无法区分的
- 解决这个问题就需要引入额外的信息 (给变量加一个版本号 每次进行修改 都递增版本号)
- 从Java 1.5开始, JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
- 是乐观锁的一种实现,在竞争小的时候很高效。
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
耗能问题
- 一般来说在竞争不是特别激烈的时候,使用该包下的原子操作性能比使用 synchronized 关键字的方式高效的多(查看getAndSet(),可知如果资源竞争十分激烈的话,这个for循环可能换持续很久都不能成功跳出。不过这种情况可能需要考虑降低资源竞争才是)。
- 改进方法就是通过一个计数比如循环100次就不在循环, 直接进行阻塞等待.
实现计数器
- 在较多的场景我们都可能会使用到这些原子类操作。一个典型应用就是计数了,在多线程的情况下需要考虑线程安全问题。
public class Counter {
private final AtomicInteger atomicInteger;
public Counter(int val) {
this.atomicInteger = new AtomicInteger(val);
}
private int getCount() {
return atomicInteger.get();
}
private void increase() {
atomicInteger.getAndIncrement();
}
private void set(int newVal) {
atomicInteger.set(newVal);
}
}
什么是AQS
- AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。
- 这个抽象类被设计为作为一些可用原子int值来表示状态的同步器的基类。
- 所以说他的内部主要就是维护了一个表示状态的int值和一个FIFO的阻塞队列
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
/**
* The synchronization state.
*/
private volatile int state;
AQS实现原理
- AQS的实现主要在于维护一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。队列中的每个节点是对线程的一个封装,包含线程基本信息,状态,等待的资源类型等。
- 此类支持默认排他模式或者共享模式或者都支持。
- 当以独占方式进行获取时,其他线程尝试进行的获取将无法成功.比如Condition由只持独占模式的子类用作实现。
- 由多个线程获取的共享模式, 当成功获取共享模式时,下一个等待线程(如果存在)也必须确定它是否也可以获取。
- 在不同模式下等待的线程共享相同的FIFO队列。通常,实现子类仅支持这些模式中的一种,但是这两种都可以在例如中使用 ReadWriteLock。仅支持独占模式或仅支持共享模式的子类无需定义支持未使用模式的方法。
- CLH结构如下:
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
- AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全部处于阻塞状态.
- 请求锁不成功的线程会被addWaiter方法负责把当前无法获得锁的线程包装为一个Node添加到队尾
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (RuntimeException ex) {
cancelAcquire(node);
throw ex;
}
}
- AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过CAS操作
AQS的使用
- AQS管理一个关于状态信息的单一整数,该整数可以表现任何状态。比如, Semaphore 用它来表现剩余的许可数,ReentrantLock 用它来表现拥有它的线程已经请求了多少次锁;FutureTask 用它来表现任务的状态(尚未开始、运行、完成和取消)
-
- 如果你有看过类似 CountDownLatch 类的源码实现,会发现其内部有一个继承了 AbstractQueuedSynchronizer 的内部类 Sync
private static final class Sync extends AbstractQueuedSynchronizer
-
使用AQS来实现一个同步器需要继承AbstractQueuedSynchronizer(AQS)类并且覆盖实现如下几个方法,并且使用getState,setState,compareAndSetState这几个方法来设置获取状态
**boolean tryAcquire(int arg) 尝试以独占模式进行获取。
boolean tryRelease(int arg) 尝试设置状态以反映排他模式下的发布。
int tryAcquireShared(int arg) 尝试以共享模式进行获取。
boolean tryReleaseShared(int arg) 尝试设置状态以反映共享模式下的发布。
boolean isHeldExclusively() 返回true是否仅相对于当前(调用)线程保持同步。 ** -
我们可以看一下CountdownLatch使用AQS的源码
public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void countDown() {
sync.releaseShared(1);
}
public long getCount() {
return sync.getCount();
}
public String toString() {
return super.toString() + "[Count = " + sync.getCount() + "]";
}
}
本文围绕Java并发编程展开,介绍了CAS和AQS。CAS是乐观锁,可解决多线程并行时重量级锁的性能损耗,存在ABA和耗能问题;AQS是同步框架,内部维护状态值和FIFO队列,支持排他和共享模式,使用时需继承并覆盖特定方法。

1230

被折叠的 条评论
为什么被折叠?



