什么是可重入锁
可重入锁,也叫做递归锁,指的是同一线程获得锁之后,又去获得同一把锁,如果能够成功,就是可重入锁。如果不举例,这个概念可能会有点抽象。当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
看下这段代码:
public class Demo {
public synchronized void method1() {
method2();
}
private synchronized void method2() {
}
}
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁,导致死锁发生。
幸好synchronized是可重入的,所以不会因为这种情况发生死锁。至于synchronized可重入是怎么实现的,涉及到JVM实现,暂时不知道,以后会去了解。但是,熟悉Java并发包的同学应该知道ReentrantLock类,这个类就叫可重入锁,我们可以从源码角度去探索下这个类是怎么实现可重入的。
ReentrantLock——可重入锁
ReentrantLock,可重入锁,是一种可以完全替代synchronized的递归无阻塞同步机制。在JDK5的早期版本中,可重入锁的性能远比synchronized好,从JDK6开始synchronized进行了大量的优化,使得两者性能相差不大。但是,ReentrantLock提供了比synchronized更强大、更灵活的锁机制。JDK源码中可以看到大量ReentrantLock的使用。
我们通过源码来看下ReentrantLock是怎么实现可重入的,ReentrantLock的lock()方法用来加锁。
lock方法首先调用compareAndSetState()方法判断锁是否被线程占有,如果没有被线程占有,通过setExclusiveOwnerThread()方法记录当前线程为占有锁的线程,以便后续进行可重入判断。如果锁被线程占有的,调用acquire()申请锁。acquire()中最重要的一个方法是nonfairTryAcquire(),这个方法用来判断线程最终是否能够获得锁。代码如下:
一行行来分析下这段代码:
- 第一行,获取当前线程,用来跟之前lock()方法中调用的setExclusiveOwnerThread()的线程对比。
- 第二行,调用getState()方法,这个state变量,就是用来记录锁是否被占有和锁被占有的次数,0表示没有线程占有,1表示被线程占有,2表示被同一个线程占有两次,需要释放锁两次,以此类推。
- 第三行,根据state的值判断锁是否被占有。
- 第四行,如果没有占有,调用compareAndSetState()方法上锁。这compareAndSetState()方法也很重要,ReentrantLock锁机制就靠它实现的。
- 第五行,记录当前占有锁的线程信息。
- 第九行,这是实现可重入的关键代码,在第三行判断锁已经被占有后,如果没有第九行这段代码判断,那么同一线程就无法获取该锁,从而导致死锁的发生。
- 接下来几行代码就是记录线程占有锁的次数,并设置到state变量上。
再看下unlock()方法,看看可重入锁是如何释放锁的,代码如下:
unlock()方法调用ReentrantLock的内部类Sync的release()方法:
tryRelease()方法做了最终的释放锁操作:
这段代码不难理解,先判断当前线程是否是持有锁的线程,是的话,state变量减去相应的数值,再判断锁是否完全释放。现在去了解下更关键的compareAndSetState()方法,它是如何保证数据同步的:
只有一行代码,很明显又做了封装,不过这次使用Unsafe这个类的native方法。unsafe.compareAndSwapInt(this, stateOffset, expect, update);
这行代码实现的功能是原子性的判断this指向的类,stateOffset所代表的属性的值,是否等于expect值,如果等于就将该值更新为update值。这句话有点绕,需要仔细体会下。this表示当前类,这个是肯定的,那么stateOffset所代表的是哪个属性呢?下面这段代码证明,stateOffset代表的是state属性。
总结
synchronized和ReentrantLock都具有可重入性,就是为了避免线程多次访问同一个锁时,出现死锁的情况。但是,synchronized和ReentrantLock的代码实现是不同的,synchronized是基于JVM层面实现的,ReentrantLock是基于底层CPU指令实现。尽管ReentrantLock的功能比synchronized更强大,但还是强烈推荐在多线程应用程序中使用synchronized关键字,因为实现方便,后续工作由JVM来完成,可靠性高。只有在确定锁机制是当前多线程程序的性能瓶颈时,才考虑使用其他机制,如ReentrantLock等。