Lock 与 Synchronized 一样用于控制多个线程访问共享资源。Synchronized 关键字隐式地获取锁和释放锁,同时也将锁的获取释放流程固定化了。Lock 则不同,它支持用户去手动地获取和释放锁。
基本方法
Lock 是一个接口,定义了锁的获取与释放的基本操作:
- void lock():使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待,在锁获得后从该方法返回
- void lockInterruptibly() throws InterruptedException:与lock()方法类似,但是在等待过程中可以中断当前线程;
- boolean tryLock():尝试非阻塞地获取锁,调用该方法后立即返回,如果能够获取则返回true,否则返回false;
- boolean tryLock(long time, TimeUnit unit) throws InterruptedException:超时地获取锁,超时的时间内获取到锁,返回true,超时时间结束尚未获取到锁,返货false,超时时间内当前线程被中断,则会结束获取锁;
- void unlock():释放锁;
- Condition newCondition():获取等待通知组件,当前线程获取到锁之后可调用该组件的wait()方法,调用后,将释放锁,进入等待。
队列同步器
队列同步器 AbstractQueuedSynchronizer 是用来构建锁或者其他同步组件的基础框架,使用了一个 int 成员变量表示同步状态,通过内置的FIFO队列争来实现资源获取线程的排队工作。
队列同步器提供三个方法支持重写,用于对当前的同步状态进行更改:
- getState(): 获取当前的同步状态
- setState(int newState): 设置当前同步状态;
- compareAndSetState(int expect, int update): 使用 CAS 设置当前状态,保证状态设置的原子性。
同时,同步器还提供了多个方法可被复写:
- boolean tryAcquire(int arg): 独占式地获取同步状态;
- boolean tryRelease(int arg): 独占式释放同步状态;
- int tryAcquireShared(int arg): 共享式获取同步状态;
- boolean tryReleaseShared(int arg): 共享式释放同步状态;
- boolean isHeldExclusively(): 判断是否被当前线程所独占。
独占表示在同一时刻只可以有一个线程获取到锁,而其他线程只能在队列中等待,共享表示在同一时刻可以有多个线程共享式获取到锁,其他企图独占式获取锁的线程只能等待。
同步队列
队列同步器的实现依赖内部的同步队列来完成同步状态的管理。它是一个FIFO的双向队列,当线程获取同步状态失败时,同步器会将当前线程和等待状态等信息包装成一个节点并将其加入同步队列,同时会阻塞当前线程。当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态
节点是构成同步队列的基础,同步器拥有首节点和尾节点,没有成功获取同步状态的线程会成为节点加入该队列的尾部,其结构如下图所示
独占式同步状态的获取与释放
独占式获取同步状态的代码如下
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
主要逻辑:
首先调用同步器实现的tryAcquire(int arg)
方法,保证线程安全地获取同步状态,如果同步状态获取失败则构造同步节点(同步队列中的等待节点),通过addWaiter方法将其天尊驾到队列的尾部,最后调用acquireQueued使其自旋获取同步状态。流程主要如下图所示:
独占式获取了同步状态并执行相应的逻辑后,需要释放同步状态,通过调用release(int arg)
方法可以释放同步状态。该方法代码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
该方法中 unparkSuccessor 用户唤醒处于等待状态的线程。
共享式同步状态的获取与释放
通过调用acquireShared(int arg)方法可以共享式地获取同步状态,代码如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
同步器通过调用 tryAcquireShared 方法尝试获取同步状态,若返回值大于等于0表示能回去到同步状态,否则进入同步队列开始自旋,在自旋过程中如果当前节点的钱去节点为头节点则尝试获取同步状态,获取成功则从自旋中退出。
与独占式相同,共享式获取同步状态也需要释放,通过调用releaseShared(int arg)方法释放同步状态,代码如下
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
该方法在释放同步状态之后,将会唤醒处于等待状态的节点。
重入锁
重入锁 ReentrantLock 就是支持重进入的锁,该锁能够支持一个线程对资源的重复加锁。
实现方式:
- 线程再次获取锁时,锁需要识别获取锁的线程是否是当前占据锁的线程,如果是则再次成功获取;
- 线程重复获取了n次锁,则需要重复释放n次。
读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
可以通过readLock()获取读锁,通过writeLock()获取写锁