锁的分类#
可重入 / 不可重入锁#
参考链接: 究竟什么是可重入锁
广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者 class)
在 Java 中ReentrantLock
和synchronized
都是可重入锁,区别在于:
Synchronized
是依赖于 JVM 实现的,而ReentrantLock
是 JDK 实现的ReentrantLock
可以指定是公平锁还是非公平锁。而Synchronized
只能是非公平锁
ReentrantLock 中可重入锁实现#
实现原理:在 AQS 中维护了一个 private volatile int state 来计数重入次数,避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 实现原理
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// setState是AbstractQueuedSynchronizer中final定义的不可变方法
setState(nextc);
return true;
}
return false;
}
公平锁 / 非公平锁#
公平锁就是保障了多线程下各线程获取锁的顺序,先到的线程优先获取锁 (通过维护一个FIFO队列实现
),而非公平锁则无法提供这个保障,ReentrantLock
、ReadWriteLock
默认都是非公平模式,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。 结合代码来看:
//非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//区别重点看这里
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//hasQueuedPredecessors这个方法就是最大区别所在
// 在获取锁之前会先判断等待队列是否为空或者自己是否位于队列头部,该条件通过才能继续获取锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
读写锁#
参考链接: 深入理解读写锁 ReentrantReadWriteLock
读写锁是怎样实现分别记录读写状态的#
同步状态变量 state 的高 16 位用来表示读锁被获取的次数,低 16 位用来表示写锁的获取次数
/** Returns the number of shared holds represented in count */
// 读锁是共享锁
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
// 写锁是独占锁
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
写锁是怎样获取和释放的#
写锁获取#
写锁是独占式锁,在同一时刻写锁是不能被多个线程所获取,实现写锁的同步语义是通过重写 AQS 中的 tryAcquire 方法实现的:
其主要逻辑为:当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态
写锁释放#
写锁释放通过重写 AQS 的 tryRelease 方法
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//1. 同步状态减去写状态
int nextc = getState() - releases;
//2. 当前写状态是否为0,为0则释放写锁
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
//3. 不为0则更新同步状态
setState(nextc);
return free;
}
读锁是怎样获取和释放的#
读锁的获取#
当写锁被其他线程获取后,读锁获取失败,否则获取成功利用 CAS 更新同步状态。另外,当前同步状态需要加上 SHARED_UNIT, 原因是同步状态的高 16 位用来表示读锁被获取的次数。如果 CAS 失败或者已经获取读锁的线程再次获取读锁时,是靠 fullTryAcquireShared 方法实现的
读锁的释放#
读锁释放的实现主要通过方法 tryReleaseShared,源码如下:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 前面还是为了实现getReadHoldCount等新功能
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
// 读锁释放 将同步状态减去读状态即可
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
乐观锁 / 悲观锁#
参考链接: 面试必备之乐观锁与悲观锁
乐观锁#
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法
实现
乐观锁适用于多读的应用类型,这样可以提高吞吐量
乐观锁的缺点 (CAS 算法的缺陷)#
- ABA 问题 (JDK 1.5 以后的
AtomicStampedReference
类解决) - 循环时间长开销大:自旋 CAS, 也就是不成功就一直循环执行直到成功,如果长时间不成功,会给 CPU 带来非常大的执行开销
- 只能保证一个共享变量的原子操作: CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效,从 JDK 1.5 开始,提供了
AtomicReference
类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作
悲观锁#
每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)
- 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
- Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现