JUC
CAS(compareAndSet)
JDK1.6后synchronized优化,效率和CAS操作差不多了,而且可读性更高。
无锁算法,但看起来是乐观锁的思想,原子操作指令。CAS 操作包含三个操作数:内存中的旧值(V)、预期值(A)和新值(B),旧值是volatile修饰的,一个线程修改,保证所有其他线程可见。比如compareAndSet(0, 1),0是预期值,1是新值,若当前内存值等于预期值则替换成新值。整个比较替换的过程是由硬件(比如Pentium锁住总线不让多核中其他处理器访问)保证原子性的,只需要调用相应指令即可。
java底层是调用unsafe.compareAndSwapInt,compareAndSwapInt是native本地方法(在Atomic类中),由c++编写,这个c++执行的核心汇编指令是cmpxchg。
优点是无锁,所以高效,非阻塞,JUC就是基于CAS操作的。缺点是CPU消耗大,调试起来复杂,而且会出现ABA问题。
ABA问题:线程1取到A,此时线程2取A,改成B,完后又改成A写入内存,线程1进行CAS发现还是A,于是操作成功。虽然CAS成功,但不代表A没没发生过变化。解决的办法是为变量引入版本号。
AQS(AbstractQueuedSynchronizer)
- J.U.C是基于AQS实现的,AQS是一个同步器,设计模式是模板模式。
- 核心数据结构:双向链表(即一个FIFO队列) + state(锁状态,volatile int类型)
- 底层操作:CAS
- AQS加锁解锁流程:
- state是加锁解锁的核心状态,为0代表:没人占锁,可以竞争,>0代表已经有人占锁了,若Sync是公平锁,则state只有0和1两个状态;若Sync是非公平锁,则state可重入,加锁就是每次+1,解锁就是每次-1,直到state又变为0。
- 通过自旋+CAS,尝试占锁,若占锁失败,将线程加入同步队列尾部,并阻塞当前线程,当同步锁释放后,会唤醒队列首节点来尝试占锁。当CAS(占锁)成功,修改state状态,更新同步队列。
AQS两种资源共享方式:独占锁 和 共享锁
-
Exclusive(独占锁,只有一个线程能执行,如ReentrantLock)
/* tryAcquire:去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。 addWaiter:如果tryAcquire返回FALSE(获取同步状态失败),则调用该方法将当前线程加入到同步队列(CLH)尾部。 acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。 selfInterrupt:产生一个中断。 */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
-
Share(共享锁,多个线程可同时执行,如Semaphore/CountDownLatch)。
public final void acquireShared(int arg) { // 大于0表示获取成功,否则进行doAcquireShared自旋 if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }