同一个线程再次进入同步代码的时候.可以使用自己已经获取到的锁,这就是可重入锁
java里面内置锁(synchronized)和Lock(ReentrantLock)都是可重入的
比如一个方法是synchronized,递归调用自己,那么第一次已经获得了锁,第二次调用的时候还能进入吗? 直观上当然需要能进入.这就要求必须是可重入的.可重入锁又叫做递归锁
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething"); super.doSomething();//若内置锁是不可重入的,则发生死锁
}
}
当然,可重入锁,也是针对同一个线程而言的。同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行。在多线程就不可以了。
土方法实现可重入锁
将获得锁的线程记录下来,如果进入方法的线程和获得锁的线程是同一个线程,那么我们就可以直接获得锁,不需要等待
public class MyLock implements Lock{
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;
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
// TODO Auto-generated method stu
}
@Override
public boolean tryLock() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// TODO Auto-generated method stub
return false;
}
@Override
public Condition newCondition() {
// TODO Auto-generated method stub
return null;
}
}
当我们使用上面介绍重入锁的测试方式来测验代码时,将会打印出a、b,因为锁是可以重入的,不会出现一直等待的情况。
使用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类的源码解析)
注:这个方法是忽略中断的,不忽略中断的方法,这里不做介绍
当使用AQS释放锁时,会调用方法release(int)方法中会调用,而release(int)方法中会调用tryRelease(int)来尝试释放锁,如果释放锁成功后,如果FIFO链表中有线程,那么,会唤醒所有等待状态的线程。
利用AQS实现可重入锁
实际上利用AQS实现一个可重入锁是非常容易的,首先给出代码。
实际上利用AQS实现锁和用土方法实现锁的思路大体上是相同的,只是,我们不需要关注线程的唤醒和等待,这些会有AQS帮助我们实现,我们只需要实现方法tryAcquire(int)和tryRelease(int)就可以了。
这里我们实际上是应用AQS中的int值保存当前线程的重入次数。
加锁思路:
如果第一个线程进入可以拿到锁,可以返回true,
如果第二个线程进入,拿不到锁,返回false,
有一种特例(实现可重入),如果当前进入线程和当前保存线程为同一个,允许拿到锁,但是有代价,更新状态值,也就是记录线程的重入次数
public class MyLock 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();
}
}