本篇文章将介绍两种自己动手实现可重入锁的方法。
我们都知道JDK中提供了一个类ReentrantLock
,利用这个类我们可以实现一个可重入锁,这种锁相对于synchronized
来说是一种轻量级锁。
重入锁的概念
重入锁实际上指的就是一个线程在没有释放锁的情况下,可以多次进入加锁的代码块。
public void a() {
lock2.lock();
System.out.println("a");
b();
lock2.unlock();
}
public void b() {
lock2.lock();
System.out.println("b");
lock2.unlock();
}
new Thread(() -> {
m.a();
}).start();
这种情况下,如果我们加的锁不是支持可重入的锁,那么b方法中的代码块不会执行,如果我们的锁是一个重入锁,那么b方法中的打印代码块也会被执行。
土方法实现重入锁
首先我们先实现一个没有实现可重入的锁,这个锁实现接口Lock
,代码如下:
public class MyLock implements Lock {
//锁标记
private boolean isLocked = false;
@Override
public synchronized void lock() {
//如果已经有一个线程获得了锁,那么线程就一直等待
while (isLocked)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
isLocked = true;
}
@Override
public synchronized void unlock() {
//可以进入的一定使已经获得锁的线程,那么直接改变标志,唤醒其他等待的线程
isLocked = false;
notify();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
代码中使用sychronized
关键字,来控制只有一个线程可以获得锁。
由于sychronized
关键字加在类的方法上的时候,内置锁对象实际使当前类的对象,因此,我们需要使用同一个对象来调用加锁、解锁方法。
这样我们就可以保证只有一个线程可以进入加解锁方法内部,然后通过isLocked
来标记是否已经有线程获得了锁。
当我们使用上面介绍重入锁的测试方式来测验代码时,只会打印出a,之后将一直等待,无法打印b。
下面,我们将修改方法,让其实现可重入。
实际上,所谓可重入的方法就是将获得锁的线程记录下来,如果进入方法的线程可获得锁的线程是同一个线程,那么我们就可以直接获得锁,不需要等待。
实现方法如下:
private boolean isLocked = false;
//记录获得锁的线程
private Thread lockBy = null;
//记录获得锁的线程的重入次数
private int count = 0;
@Override
public synchronized void lock() {
//获取当前线程
Thread currentThread = Thread.currentThread();
//已经有线程获得锁,并且获得锁的线程不是当前线程,那么不满足获得锁,线程需要等待
while (isLocked && currentThread != lockBy)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//没有线程获得锁,或者获得锁的线程就是当前线程
isLocked = true;
lockBy = currentThread;
//记录当前线程的重入次数
count++;
}
@Override
public synchronized void unlock() {
//释放锁时,只有当获得锁的线程和当前线程是同一线程时才是正确的
if (lockBy == Thread.currentThread()) {
//线程重入次数减一
count--;
//只有当count变为0也就是所有获取锁的地方都已经释放了,才能够真正释放锁,修改标志位,唤醒其他线程
if (count == 0) {
notify();
isLocked = false;
}
}
}
当我们使用上面介绍重入锁的测试方式来测验代码时,将会打印出a、b,因为锁是可以重入的,不会出现一直等待的情况。
使用AQS类实现重入锁
AQS类简单介绍
AbstractQueuedSynchronizer
类时JDK在1.5版本开始提供的一个可以用来实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)框架,在这里我们不关注它的其他功能,重点介绍一下如何利用AQS实现阻塞锁。
当我们要使用AQS实现一个锁时,我们需要在我们的锁类的内部声明一个非公共的内部帮助类,让这个类集成AbstractQueuedSynchronizer
类,并实现其某些方法。
利用AQS类,我们可以实现两种模式的锁,一种是独占锁,一种是共享锁。这两种锁的帮助类需要实现的方法是不同,如果是独占锁,那么需要实现tryAcquire(int)
和tryRelease(int)
方法;如果是共享锁,那么需要实现tryAcquireShared(int)
和tryReleaseShared(int)
方法。
AQS类内部维护了一个FIFO的双向链表,用来保存所有争夺锁的线程,AQS源码中的Node
类就是双向链表中节点的数据结构。
当使用AQS类加锁时,会调用方法acquire(int)
方法中会调用,而acquire(int)
方法中会调用tryAcquire(int)
来尝试获得锁,如果获得锁成功,方法结束,如果获得锁失败,那么需要将线程维护到FIFO链表中,并且让新增加的线程进入等待状态,并且维护链表中线程的状态。(这部分代码比较复杂,可以参考网上对AQS的讲解,之后会再写一篇专门介绍AQS类的源码解析)
注:这个方法是忽略中断的,不忽略中断的方法,这里不做介绍
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
当使用AQS释放锁时,会调用方法release(int)
方法中会调用,而release(int)
方法中会调用tryRelease(int)
来尝试释放锁,如果释放锁成功后,如果FIFO链表中有线程,那么,会唤醒所有等待状态的线程。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
利用AQS实现可重入锁
实际上利用AQS实现一个可重入锁是非常容易的,首先给出代码。
实际上利用AQS实现锁和用土方法实现锁的思路大体上是相同的,只是,我们不需要关注线程的唤醒和等待,这些会有AQS帮助我们实现,我们只需要实现方法tryAcquire(int)
和tryRelease(int)
就可以了。
这里我们实际上是应用AQS中的int值保存当前线程的重入次数。
加锁思路:
如果第一个线程进入可以拿到锁,可以返回true,
如果第二个线程进入,拿不到锁,返回false,
有一种特例(实现可重入),如果当前进入线程和当前保存线程为同一个,允许拿到锁,但是有代价,更新状态值,也就是记录线程的重入次数
public class MyLock2 implements Lock {
private Helper helper = new Helper();
//实现一个私有的帮助类,继承AQS类
private class Helper extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
//AQS中的int值,当没有线程获得锁时为0
int state = getState();
Thread t = Thread.currentThread();
//第一个线程进入
if (state == 0) {
//由于可能有多个线程同时进入这里,所以需要使用CAS操作保证原子性,这里不会出现线程安全性问题
if (compareAndSetState(0, 1)) {
//设置获得独占锁的线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
} else if (getExclusiveOwnerThread() == t) {
//已经获得锁的线程和当前线程是同一个,那么state加一,由于不会有多个线程同时进入这段代码块,所以没有线程安全性问题,可以直接使用setState方法
setState(state + 1);
return true;
}
//其他情况均无法获得锁
return false;
}
@Override
protected boolean tryRelease(int arg) {
//锁的获取和释放使一一对应的,那么调用此方法的一定是当前线程,如果不是,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new RuntimeException();
}
int state = getState() - arg;
boolean flag = false;
//如果state减一后的值为0了,那么表示线程重入次数已经降低为0,可以释放锁了。
if (state == 0) {
setExclusiveOwnerThread(null);
flag = true;
}
//无论是否释放锁,都需要更改state的值
setState(state);
//只有state的值为0了,才真正释放了锁,返回true
return flag;
}
Condition newCondition() {
return new ConditionObject();
}
}
@Override
public void lock() {
helper.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
helper.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return helper.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return helper.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
helper.release(1);
}
@Override
public Condition newCondition() {
return helper.newCondition();
}
}
至此,我们已经使用两种方式实现了一个重入锁。