1. CAS操作
概念: CAS(Compare And Swap)底层操作是一条汇编指令 lock cmpxchg
,是一种轻量级锁,也叫自旋锁:
- cas(num,3,4):我希望将3改成4:
- 将num获取到
- 判断是否为期望值3
- 如果3,将其改为4(这个过程是加锁的)。
- 如果不是3,说明有人动了这个值,重新获取num,或者放弃获取。
- 判断是否为期望值3
- ...
- 这个过程就是自旋
- ABA问题:
- 线程A将num获取到,判断为期望值3,但其实并不能一定说明中间没有其他线程动过这个num变量,因为有可能某个线程将其改为了5,又改为了93,又改回了3,如果num是基本类型变量,ABA问题影响不大,无论中间过程中num经历了什么,只要最后是3就行,但如果num是引用数据类型,存储的是0x9527,那么有可能0x9527中的某些属性值已经被偷偷改过了。
- 解决方案:添加版本号字段,只要被修改过,版本号自增,每个线程在判断期望值的同时,连带着版本号一同检查。
CAS底层都是基于UnSafe类的某些调用,UnSafe类是可以直接操作JVM内存的一个类,由JVM设计者使用,我们几乎用不到它。
2. synchronized锁升级
概念: 即使在多线程环境下,某些共享变量也有可能极少发生线程争抢,即大多数的情况下都是只有一个线程在访问这个共享资源,如果对这个共享资源添加OS锁,那么每次访问的时候都需要向OS申请,也会很浪费性能,早期的synchronized会直接对锁实例obj添加OS锁,后来随着JDK的升级而做了优化,变成了先添加偏向锁,然后视情况而定,是否需要向重量锁方向进行进化。
- 锁实例obj的mark-word的后两bit记录了锁类型的信息,其中:
- 刚new出来的实例,没有加锁,mark-work后两位为
01
,再辅助一个倒数第三位的0
同时描述。 - 获得了偏向锁后,mark-work后两位为
01
,再辅助一个倒数第三位的1
同时描述。 - 获得了轻量级锁-自旋锁后,mark-work后两位为
00
,不需要辅助位。 - 获得了重量级锁-OS锁后,mark-work后两位为
10
,不需要辅助位。 - 如果锁实例obj将要被GC回收,mark-work后两位为
11
,不需要辅助位。
- 刚new出来的实例,没有加锁,mark-work后两位为
锁升级流程图
2.1 偏向锁
概念:
- 偏向锁会偏向第一个获取锁的线程(假设为threadA),将获得了偏向锁的信息
101
记录在琐实例obj的mark-word中的后三位,再将threadA的线程ID记录mark-word其他位,然后每次有线程想要获取锁的时候,只需要一个简单的判断:- 如果新线程还是threadA,直接放行进入同步代码中。
- 如果其他线程如threadB,则表示发生了资源争抢,需要将偏向锁撤销并升级为轻量级锁。
- 如果同步代码发生了严重耗时的情况,如调用了
wait()
,则直接升级为重量级锁。
- 偏向锁在升级之前需要先撤销revoke,这个过程也会消耗一定的CPU资源,所以如果你明确知道会发生多线程争抢事件,就不要使用偏向锁,而直接使用自旋锁,以免不必要的性能开销。
- 偏向锁是需要启动的:因为JVM在启动的会执行很多sync代码,这些代码明确会有多线程竞争,所以偏向锁都是延迟4秒才启动的,以避免频繁的锁升级和锁撤销,浪费性能。
- 可以通过
-XX:biasdLockingStartupDelay=0
参数来调整匿名偏向锁的启动延迟时间。 - 偏向锁未启动时new出来的实例没有任何锁的信息,添加sync锁后,直接升级成为自旋锁。
- 偏向锁启动之后再new出来的实例直接会获得一个匿名偏向锁,指向0(null),锁信息为
101
,添加sync锁后仍为偏向锁,但mark-down存入了偏向的线程ID。
- 可以通过
源码: /javase-advanced/
- src:
c.y.thread.lock.LockUpgradeTest.biasLock()
/**
* @author yap
*/
public class LockUpgradeTest {
@Test
public void biasedLock() throws InterruptedException {
Object obj = new Object();
// 00000001 => 001: no lock
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
TimeUnit.SECONDS.sleep(5L);
obj = new Object();
// 000001(01): anonymous biased lock, and thread-recorded is null
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
// 00000(101): non-anonymous biased lock, and thread-recorded is main thread
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
2.2 轻量级锁-自旋锁
概念: 偏向锁撤销后,争抢资源的两个线程threadA和threadB,会各自在自己的线程栈中生成LR(Lock Record),并通过自旋的方式开始争抢资源,假设threadB争抢到了资源,则将threadB的LR记录在锁实例obj中,此时threadA开始在用户内存空间CAS自旋,此时锁实例obj中的偏向锁便升级为了自旋锁。
- 自旋可以理解为在同步代码块的"门口",建立一个死循环不停地尝试获取锁的过程。
- 自旋锁会消耗CPU资源,所以不适用于同步代码的执行时间长,或并发访问量高的情况。
自旋锁也叫无锁,但为了避免误导,尽量不要使用这个叫法。
源码: /javase-advanced/
- src:
c.y.thread.lock.LockUpgradeTest.selfRotatingLock()
/**
* @author yap
*/
public class LockUpgradeTest {
@Test
public void selfRotatingLock() {
Object obj = new Object();
// 00000001 => 001: no lock
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
// 11001000(00): self-rotating lock, and thread-recorded is the LR of main thread
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
2.3 重量级锁-OS锁
概念: 如果某个线程自旋次数超过10次,或所有自旋线程个数总和超过CPU核数的一半,那么自旋锁将升级为重量级锁,即OS锁。
- JDK6之前可以使用
-XX:PreBlockSpin
调整自旋最大次数,JDK6之后JVM引入了自适应自旋的概念,即JVM自动管理自旋最大次数,无需我们操心。 - OS锁需要向内核空间申请并得到返回,效率比自旋锁低,但它底层使用等待队列来存放和调度那些没能获取锁的线程,不消耗CPU资源,所以更适用于同步代码的执行时间长,或并发访问量高的情况。
源码: /javase-advanced/
- src:
c.y.thread.lock.LockUpgradeTest.osLock()
/**
* @author yap
*/
public class LockUpgradeTest {
@Test
public void osLock() throws InterruptedException {
Object obj = new Object();
// 00000001 => 001: no lock
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
Thread threadA = new Thread(()->{
synchronized (obj) {
try {
obj.wait();
System.out.println("notified...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadA.start();
Thread threadB = new Thread(()->{
synchronized (obj) {
try {
TimeUnit.SECONDS.sleep(1L);
obj.notify();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadB.start();
threadA.join();
threadB.join();
}
}
3. AQS类
概念: AbstractQueuedSynchronizer(AQS)是大部分锁的底层原理类,底层是CAS加volatile实现的。
- AQS和核心是必须保持可见性的
volatile int state
属性,state值所表示的意义是随子类而变的,如:- ReentrantLock拿state来记录线程重入次数。
- CountDownLatch拿state来记录线程倒数计数。
- Semaphore拿state来记录信号量个数。
- AQS中的state属性关联着一个双链表队列,每个节点都装着一个线程实例,由AQS类自己在类的内部维护。
- 双链表队列的入队和出队过程都是CAS操作的,比锁定整条链表效率高。
4. ReentrantLock
概念: ReentrantLock表示一种可重入锁,是可以替代synchronized锁的,且比synchronized更灵活。
- 构造:
- new ReentrantLock():构建一个可重入锁,
- new ReentrantLock(boolean fair):指定构建一个非公平(默认false)/公平锁。
- 非公平锁:新来的线程不进等待队列,直接进入就绪状态,直接有机会争抢CPU资源。
- 公平锁:新来的线程先入等待队列进行排队,等待线程调度器的调度。
- 方法:
void lock()
:添加可重入锁。void unlock()
:释放可重入锁,这个必须写在finally块中。boolean tryLock()
:尝试获取锁,无论是否成功都不阻塞,方法继续执行。- p1:指定多长时间内尝试获取锁。
- p2:时间单位。
- vs synchronized:
- ReentrantLock具有tryLock方法,可以自行指定获取不到锁时的处理方案,synchronized在获取不到锁的时候只能阻塞。
- ReentrantLock可以指定为公平锁或非公平锁,synchronized只有非公平锁。
源码: /javase-advanced/
- src:
c.y.thread.lock.ReentrantLockTest
/**
* @author yap
*/
public class ReentrantLockTest {
private static class LockDemo implements Runnable {
private Lock lock = new ReentrantLock();
private void method() {
lock.lock();
try {
System.out.println(Thread.currentThread() + ": over");
} finally {
lock.unlock();
}
}
@Override
public void run() {
lock.lock();
try {
for (int i = 0, j = 5; i < j; i++) {
TimeUnit.SECONDS.sleep(1L);
System.out.println(Thread.currentThread() + ": " + i);
}
method();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
private static class TryLockDemo {
private Lock lock = new ReentrantLock();
private void methodA() {
try {
lock.lock();
for (int i = 0, j = 5; i < j; i++) {
TimeUnit.SECONDS.sleep(1L);
System.out.println(Thread.currentThread() + ": " + i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private void methodB() {
boolean locked = false;
try {
// Try to acquire the lock within 3 seconds
locked = lock.tryLock(3, TimeUnit.SECONDS);
System.out.println(Thread.currentThread().getName() + ": " + locked);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (locked) {
lock.unlock();
}
}
}
}
@Test
public void lock() {
LockDemo lockDemo = new LockDemo();
new Thread(lockDemo).start();
new Thread(lockDemo).start();
}
@Test
public void tryLock() throws Exception {
TryLockDemo trylockDemo = new TryLockDemo();
new Thread(trylockDemo::methodA, "threadA").start();
TimeUnit.SECONDS.sleep(1L);
new Thread(trylockDemo::methodB, "threadB").start();
}
@SneakyThrows
@After
public void after() {
System.out.println(System.in.read());
}
}
4.1 lock()源码
流程: 以非公平锁为例:
-
lock()
底层流程:- 调用
Sync
类(AQS子类)的lock()
。 - 调用
NonfairSync
类(Sync子类)的lock()
。 - 通过CAS将
state
的值由0
改为1
。 - 如果成功,则令当前线程独占这把锁(使用
AbstractOwnableSynchronizer
中的exclusiveOwnerThread
变量记录当前线程)。 - 如果失败:调用
acquire(1)
。
- 调用
-
acquire(1)
底层流程:- 调用
tryAcquire()
尝试获取锁,如果成功,直接获取锁。 - 如果失败:
- 调用
addWaiter()
将当前线程加入等待队列。 - 调用
acquireQueued()
在队列中不断轮询申请获取锁。
- 调用
- 如果加入等待队列也失败,调用
selfInterrupt()
打断当前线程。
- 调用
-
tryAcquire()
底层流程:- 调用
NonfairSync
类中的tryAcquire()
。 - 调用
ReentrantLock
类中的nonfairTryAcquire()
。 - 如果当前
state
是0
- 通过CAS将
state
的值由0
改为1
。 - 令当前线程独占这把锁。
- 返回true。
- 通过CAS将
- 如果当前线程已经独占了锁。
state
自增1,表示重入一次。- 返回true。
- 如果条件均不满足,返回false。
- 调用
-
addWaiter()
底层流程:- 创建新节点,内容是当前线程。
- 将新节点通过CAS的方式追加到链表尾并返回。
-
acquireQueued()
底层流程:- 判断当前节点的前置节点是不是头节点(判断自己是不是脖子节点)。
- 头节点就是正在使用CPU资源的节点。
- 如果自己是脖子节点,尝试获取锁(和头节点竞争)。
- 如果竞争成功,获得锁,并将自己设置为新头节点。
- 如果条件均不满足,无限轮询,直至满足。
- 判断当前节点的前置节点是不是头节点(判断自己是不是脖子节点)。
4.2 unlock()源码
流程: 以非公平锁为例:
-
unlock()
底层流程:- 调用
Sync
类(AQS子类)的release(1)
。 - 调用AQS类的
release()
。 - 调用
Sync
类的tryRelease()
尝试释放锁。 - 如果释放成功,对当前头节点调用
unparkSuccessor()
并返回true。 - 如果释放失败,返回false。
- 调用
-
tryRelease()
底层流程:- 如果当前线程没有获得锁,直接抛出异常。
- 将
state
的值自减1,表示释放一把锁。 - 如果
state
的值减为0,表示所有锁释放完毕。 - 如果
state
为0,则将exclusiveOwnerThread
变量置空,并返回true。 - 否则返回false,表示尝试释放锁失败。
-
unparkSuccessor()
底层流程:- 对指定线程调用
unpark()
。
- 对指定线程调用
5. ReentrantReadWriteLock
概念: ReentrantReadWriteLock可以通过 Lock readLock()
获取读锁分支,或者通过 Lock writeLock()
获取写锁分支。
- 读读共享:我读的时候允许别人也来读。
- 读写互斥:我写的时候不允许别人读,我读的时候不允许别人写。
- 写写互斥:我写的时候不允许别人写。
源码: /javase-advanced/
- src:
c.y.thread.lock.ReentrantReadWriteLockTest
/**
* @author yap
*/
public class ReentrantReadWriteLockTest {
private static class ReadWriteLockDemo {
private ReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readLock = lock.readLock();
private Lock writeLock = lock.writeLock();
void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":reading...");
TimeUnit.SECONDS.sleep(5L);
System.out.println(Thread.currentThread().getName() + ":read over...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":writing...");
TimeUnit.SECONDS.sleep(2L);
System.out.println(Thread.currentThread().getName() + ":write over...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
}
@Test
public void readWriteLock() {
ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
new Thread(readWriteLockDemo::read, "reader-A").start();
new Thread(readWriteLockDemo::read, "reader-B").start();
new Thread(readWriteLockDemo::write, "writer-A").start();
new Thread(readWriteLockDemo::write, "writer-B").start();
}
@SneakyThrows
@After
public void after() {
System.out.println(System.in.read());
}
}