一 前言
java蕴涵丰富锁的种类,使用不同的锁会带来不同的性能开销,理解各种主流锁在不同的场景中使用有助于提高我们系统的性能。总体的分类我帮读者们做了个总结请看下面的类目;
二 乐观锁VS悲观锁
2.1 悲观锁
悲观锁是指对数据修改保持悲观的态度,在处理和访问数据之前就已经上锁;我们常见的悲观锁有synchronized和Lock;其工作方式也很简单,就是在同一时期只有一个线程能获取到锁,只有等该线程释放锁,其他线程才有机会获取到锁;
2.2 乐观锁
乐观锁是指认为一般情况数据不会造成冲突,在数据进行提交时才对数据进行检查更新,其是直接通过操作同步资源实现;
乐观锁的实现方式是CAS(compare and swap); 在 java.util.concurrent包下的原子类就是经过CAS实现;更底层CAS实现是功能强大的Unsafe类;
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
上面是Unsafe类中的CAS方法源码之一,var1 代表对象内存位置,var2代表对象中变量的偏移,var4代表期望值,var6代表新值;操作含义是如果var1对象在内存中的偏移量为var2的变量值是var4,那么就会用var6更新原始值;
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
看上面的源码, this.getLongVolatile(var1, var2);
是获取变量的值,循环体中是指 var1对象的偏移是var2,如果期望值是var6,那么就会用var6+var4代替原来的值(即this.getLongVolatile(var1, var2);
的值);如果期望值不是var6那么会继续循环直到设置成功为止;
CAS问题概述
- ABA问题:线程1使用CAS将A变量改为B,变量值不一定就是B;可能线程1在获取变量后执行CAS之前,线程2使用CAS将变量A改为B,又将B改为A;经过线程2修改后的变量A就有可能不是原来线程1获取变量时的A(对象的地址偏移等可能已经发生改变);ABA问题其实就是变量的状态值经过形态转换造成的,那么通常的解决方式就是加版本号(1A2B3A),或者加上时间戳;java中也提供了
AtomicStampedReference
来解决ABA问题; - 时间问题:我们分析源码看见是返回false那么会一值循环直到设置原始值是期望值为止,会造cpu成长等待,性能开销大;
- 多共享变量问题:对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性,javase1.5也提供了
AtomicReference
类来解决这个问题;
三 公平锁VS非公平锁
3.1 公平锁
公平锁是指按照队列中的顺序取锁也就是时间顺序取锁,越早请求获取锁的线程将越快获得锁;常见的公平锁就是synchronized和 new ReentrantLock(true);
下面的源码可以看见当传入的值是true就是公平锁,传入的值是false就是非公平锁;
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
FairSync()与 NonfairSync()最大的区别其实FairSync()有进行hasQueuedPredecessors()判断,然后在将当前线程设置进setExclusiveOwnerThread里面;
// FairSync()
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 设置当前线程占用锁
setExclusiveOwnerThread(current);
return true;
}
}
// NonfairSync()
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
hasQueuedPredecessors源码如下其就是判断当前线程是否是队列中的第一个线程;
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// 当head不等于tail,并且head下一个为空或者当前线程不是下一个线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
3.2 非公平锁
非公平锁就是不按照队列中的线程顺序获取锁,常见就是 new ReentrantLock(false);
源码已经在3.1分析过;
四 可重入锁VS不可重入锁
4.1可重入锁
可重入锁是指线程在外层方法中已经获得过一个锁,后面此线程还能再次获取锁而不会陷入阻塞状态;常见的可重入锁就是synchronized
和 ReentranLock
;
synchronized
锁内部拥有一个线程标识标明持有锁的线程,线程标识关联维护着一个计数器,当同一线程获得锁,会将计数器加1;当计数器的值大于1的时候其他线程是无法获得该锁的拥有权,直到拥有锁的线程释放相应次数的锁使计数器为0,其他线程才有机会获得锁;
ReentranLock
支持被同一个线程2147483647 次数递归拥有(可重入锁又称递归锁);当前线程第一次进入nonfairTryAcquire
方法时会先判断state(默认值为0)的状态,获得锁会将state状态设置为1,后面如果还是该线程再次获取锁,就会将state的状态再次加1;如果是其他线程(非占用锁的线程)进入该方法判断state的状态大于1,会直接返回false,表示尝试获取失败;tryRelease
释放锁也是等state为0的时候才释放;详细的源码分析看如下:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// state初始值为0 表示没有其他线程获得锁
int c = getState();
if (c == 0) {
// acquires的默认传入是1
if (compareAndSetState(0, acquires)) {
// 设置当前线程拥有锁
setExclusiveOwnerThread(current);
// 获取成功
return true;
}
}
// 当前线程就是占用锁的线程,可重入
else if (current == getExclusiveOwnerThread()) {
// 每次进入都会将 nextc 的值加1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 将 nextc 计数后的值设置为新的state
setState(nextc);
// 获取成功
return true;
}
// state的初始值不为0表示该锁已经被其他线程持有,获取失败
return false;
}
protected final boolean tryRelease(int releases) {
// 状态值减去释放的次数
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 设置 free的标志位
boolean free = false;
// 当state为0时才真正释放,并且设置占有锁的线程为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 每次都会设置计算后的c 赋值给state
setState(c);
// 返回 free的值
return free;
}
4.2 非可重入锁
非可重入锁是指当前线程获得锁,执行完同步后释放锁,二次进入需要重新获取锁,设置当前线占锁;在ThreadPoolExecutor的内部类Worker中有一段代码就是获得非可重入锁;
protected boolean tryAcquire(int unused) {
// 如果期望值是0,直接将state设置为1
if (compareAndSetState(0, 1)) {
// 设置当前线程占有锁
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
// 直接设置占有线程为空,表示释放锁资源
setExclusiveOwnerThread(null);
// 直接设置state为0
setState(0);
return true;
}
五 独占锁VS共享锁
5.1 独占锁
独占锁又称排它锁,即一个锁只能同时被一个线程所拥有;常见的排它锁有synchronized
,和ReentrantLock
,ReentrantReadWriteLock中的WriteLock
;
以ReentrantReadWriteLock中写锁的获取锁和释放锁都是独占并且可重入,其具体的源码分析如下
// 获取锁
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 获得state
int c = getState();
// 表示占有锁的次数,内部是将c 与上 65535(2的16次方减1即0xffff)
int w = exclusiveCount(c);
// state状态非0,表示已经有线程占有锁
if (c != 0) {
// 如果当前线程不是占有锁的线程,或者锁被当前线程占有0次,获取写锁失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 占有的次数超过锁允许的最大次数抛出异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 重新获取锁,设置state
setState(c + acquires);
return true;
}
// state为0,写锁处于阻塞状态或者,CAS操作state的期望值不是0,获取写锁失败
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 获取锁成功
setExclusiveOwnerThread(current);
return true;
}
// 释放锁
protected final boolean tryRelease(int releases) {
// 当前线程非占有锁的线程会抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 表示剩余的占有锁的次数
int nextc = getState() - releases;
// 占有锁的次数是否为0
boolean free = exclusiveCount(nextc) == 0;
// 占有锁的次数为0就释放锁
if (free)
setExclusiveOwnerThread(null);
// 设置state为计算过后的nextc
setState(nextc);
return free;
}
5.2 共享锁
共享锁表示锁可以被多个线程同时拥有;常见的共享锁就是ReentrantReadWriteLock中的readLock
;共享锁的线程和次数是由LocalThread所维护,state的每次设置都是以(1>>>16)为单位,从而实现共享安全,具体的源码分析如下;
// 获得共享锁
protected final int tryAcquireShared(int unused) {
// 当前线程
Thread current = Thread.currentThread();
// 获得state
int c = getState();
// 占有锁的次数非0,并且当前线程不是锁的占有者,返回-1表示获取锁失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 表示共享锁的次数(其内部是无符号右移16位)
int r = sharedCount(c);
// 读锁处于非阻塞状态,共享锁次数小于锁的最大次数,CAS操作的期望值是c,会设置state为 c + 65536(2的16次方)
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 共享锁的次数为0,设置当前线程是第一个获取读锁的线程,持有数为1,返回1表示获取共享锁成功
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 当前线程就是第一个获取读锁的线程,持有数累加1,返回1
} else if (firstReader == current) {
firstReaderHoldCount++;
// 当前线程非第一个读取锁的线程
} else {
// HoldCounter内部维护着线程的id和线程的持有读锁count;
HoldCounter rh = cachedHoldCounter;
// HoldCounter为null或者线程id不是当前线程id
if (rh == null || rh.tid != getThreadId(current))
// 设置当前线程持有的读锁的次数。readHolds由ThreadLocal维护,保证每个线程是独立
cachedHoldCounter = rh = readHolds.get();
// 如果线程持有读锁次数为0,设置HoldCounter,并且将 count累加1
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
// 获取成功
return 1;
}
// 处理CAS失效的情况和读锁是否获取问题
return fullTryAcquireShared(current);
}
// 释放共享锁
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 当前线程是第一个获取读锁的线程,
if (firstReader == current) {
if (firstReaderHoldCount == 1)
// 设置 第一个读锁的线程为null
firstReader = null;
else
// 读锁次数持有减1
firstReaderHoldCount--;
// LocalThread维护的非第一个持有读锁的线程次数减1
} 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;
}
// CAS操作,当state正好减少65536表示释放一次锁,返回true
for (;;) {
int c = getState();
// 以 65536 为单位减少
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
六 自旋锁和自适应锁
6.1 自旋锁
自旋锁指当前线程尝试去获取锁的时候发现,锁已被其他线程占用,当前线程不会马上陷入阻塞状态,而是会在锁外自旋尝试获取锁(默认是自旋10次,可以使用-XX:PreBlockSpin来设置),其实现原理也是CAS操作,Unsafe类中的CAS设置long值如下,会在while中循环尝试获取;
public final long getAndSetLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var4));
return var6;
}
6.2 自适应锁
自适应锁表示自旋的时间不再固定,如果上次该线程成功自旋获取到锁,那么jvm会认为这次该线程也能自旋获取相同的锁成功,从而加长自旋时间;如果该线程上次自旋获取锁失败,那么jvm会认为这次该线程自旋获取相同的锁会失败,从而减少自旋时间;
七 锁粗化VS锁消除
7.1 锁消除
锁消除是指在运行时期,同步代码块中没有共享数据安全问题,就会消除锁;
示例:
public class EliminateLock {
private void lock() {
synchronized(this){
// do nothing
}
}
}
7.2 锁粗化
锁粗化是指在一串连续的同步锁中,同步锁直接扩展至最外部;
粗化前:
public class ExtendLock {
private int v1 = 0;
private int v2 = 1;
public void lock(){
synchronized (this){
v1++;
}
synchronized (this){
v2++;
}
}
}
粗化后:
public class ExtendLock {
private int v1 = 0;
private int v2 = 1;
public void lock(){
synchronized (this){
v1++;
v2++;
}
}
}
八 无锁 vs 偏向锁 vs 轻量级锁 vs 重量级锁
8.1 mark word
mark word 是 java头对象中的一部分,其存储对象的HashCode,分代年龄和锁标志位信息和运行时数据;
8.2 无锁
无锁即对同步资源没进行加锁,所有线程都可以修改数据,当同一时期只有一个线程能修改数据成功,上面提到的CAS操作就是无锁的实现;
8.3 偏向锁
偏向锁是指偏向于第一个获取到锁的线程;在后续的同步中,如果没有其他线程竞争锁,偏向锁则会消除同步;当使用:
-XX:-UseBiasedLocking=true 开启(默认开启)偏向锁,第一个获取到锁的线程会在对象头的标志位设置为01;
8.4 轻量级锁
轻量级锁是指当前锁是偏向锁,被其他线程锁获取,偏向锁就会升级为轻量级锁;加锁过程中,虚拟机会在帧栈中建立锁记录(Lock Record),会将 mark word拷贝至 Lock Record ;然后jvm会使用CAS将mark world 的指针指向 Lock Record 中的指针,并将对象头中的标志位就会修改为00;
8.5 重量级锁
当超过2个线程同时竞争一个锁时,轻量级锁失效会导致轻量级锁膨胀为重量级锁,锁的标志位会设置为10,其他等待的线程将不会自旋而是陷入阻塞状态;
九参考文档
《java并发编程的艺术》
《java并发编程之美》
《深入理解java虚拟机 jvm高级特性与最佳实践》
转载:https://blog.csdn.net/youku1327/article/details/103152479