1、什么是锁
A lock is a tool for controlling access to a shared resource by
multiple threads. Commonly, a lock provides exclusive access to a
shared resource引用自JDK的java.util.concurrent.locks.Lock接口说明
锁即是对共享资源的独占访问的一种机制,当然也有共享资源的锁,如ReadWriteLock中的Read锁。
2、聊聊J.U.C中的锁
JUC中的locks包,包含了多种锁,如可重入锁、读写锁及带戳的版本锁等等,下面我们看一下这几个锁的用途及区别。
2.1 ReentrantLock
ReentrantLock
提供了公平和非公平锁,默认非公平。
分别由FairSync
NonfairSync
实现,其父类为Sync
,而Sync
父类为AbstractQueuedSynchronizer
,AQS的核心代码实现了ReentrantLock的锁的逻辑。
这一章节从源码角度说明公平和非公平锁的区别以及AQS是如何实现lock和unlock逻辑的:
java.util.concurrent.locks.ReentrantLock#lock
public void lock() {
// 调用lock,根据构造的是FairSync还是NonfairSync,调用其lock方法
sync.lock();
}
// 先看FairSync.lock();
final void lock() {
// AQS的acquire()
acquire(1);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
// 这里分为3步:
// 第一步:tryAcquire(arg),如果成功获取到锁,则lock()方法取锁完成,否则继续下一步
// 第二步:addWaiter(),构造Node并加入到队尾(AQS包含了head、tail指针,用于记录等待获取锁的线程队列)
// 第三步:acquireQueued(),获取队列里的Node,判断其是否真正获取到锁,如果没有获取到锁且需要被打断则执行selfInterrupt()将当前线程打断
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// state是AQS中的成员变量,记录锁的状态,大于0说明锁被某个线程锁住了
int c = getState();
// state == 0, 说明锁没有被占
if (c == 0) {
// 判断前面是否还要线程再等待(公平锁就是要等前面先排队的)
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程已经加过锁了,则把state再往上加,这里就有可重入锁的味道
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 非公平锁调用的加锁代码是其父类Sync的nonfairTryAcquire方法
// java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 看到锁没被其他线程占住,二话不说直接抢占
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 这里和公平锁一样的逻辑, state往上加
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;
}
以上就是ReenTrantLock的加锁逻辑。
下面分析下unlock释放锁:
java.util.concurrent.locks.ReentrantLock#unlock
public void unlock() {
sync.release(1);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#release
public final boolean release(int arg) {
// 先释放锁
if (tryRelease(arg)) {
// AQS中的head指针,head不为空,说明加锁等待队列不为空,需要唤醒head指针中记录的thread,并将其唤醒
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
// 释放锁其实就是修改state的值,如果为0,说明锁释放成功
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 这里判断了释放锁的线程和持有锁的线程是不是同一个线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
2.2 ReentrantReadWriteLock
我们知道,给共享资源加锁导致了线程独占资源,目的是为了保护共享资源的正确性。但线程访问资源,有可能只是去读取数据,而不会修改数据,这时候其他线程独占共享资源,导致读操作要一直等待,降低了系统性能。
读写锁就是为了解决这个问题:读锁和写锁的加锁关系如下
能否同时加锁 | 读 | 写 |
---|---|---|
读 | Y | N |
写 | N | N |
只有多个线程都是读操作,才能共同加“读”锁。其他情况下,只有一个线程能加锁成功。
2.3 StampedLock
JDK1.8及以后版本提供了带时间戳的锁,包含了读写锁的功能。这是一个带版本号的锁,每次给对象加锁,会返回一个stamp,之后的释放锁需要用到这个stamp,如果和锁当前的stamp不匹配则释放锁失败。