网上关于锁的资料非常多,关于源代码分析的也非常多。但是这里我觉得依然有必要记录下自己的理解,或者从另一个角度再看看这个问题。本文是结合网上的参考资料以及jdk1.6的源代码进行的分析。
1、预定义的一些问题
(1) ReentrantLock的lock流程是什么样子的?
(2) ReentrantLock的unlock流程是什么样子的?
(3) ReentrantLock中的公平与非公平是什么意思,以及怎么实现的?
(4) 重入锁是什么概念?
下面开始进入正文,来直接分析下:
2、ReentrantLock的加锁流程图
这里没有明显区分(只有开始的地方有标注)ReentrantLock的公平与非公平的方式,先给出整体的流程图如下:
图1 锁流程图(图中的自旋阶段不确切,是有阻塞逻辑的)
从图1中,可以看到当一个线程抢锁,它有2种走向:成功,图中左侧的绿色部分的流向;失败,图中右侧部分的流向,最终进入CLH队列。从图中可以看到进入左侧成功部分,非公平锁有3次机会,而公平锁有2次机会(这里机会的多少并不重要):也就是图中的标粗的黄框的判断条件。这些判断条件里都是通过对锁占用标志位进行CAS操作实现的,因此只能有1个线程是成功的,其他线程都会失败,进入CLH队列里等待。这就保证了只有1个线程抢到了锁。
成功的线程流程是比较简单的,线程会抢到锁继续执行。那么对于失败的线程,这里就有一些值得研究的问题:我的一个疑问就是,一个线程在失败的流程里执行时,这个时候锁被释放了怎么处理的呢?毕竟是多线程环境下。这里不妨把失败流程植入几个切点:如图所示,在第1个切点,其他线程unlock了锁,那么接下来的流程就会进入tryAcquire,有获得锁的机会。如果这个时候依然失败,那么进入了第2个切点处,接下来会进入队列,不过进入自旋时依然可以有tryAcquire的机会。这个时候如果进入切点3,那么会判断是否需要阻塞,如果不需要阻塞,那么依然有可以循环回到tryAcquire的机会。这里看下切点4,如果这个时候其他线程unlock了锁,该线程依然会走到LockSupport.park(this)流程。那么这个线程会被锁住吗?答案是不会被锁住的。原因在于:lock方法实质是调用了LockSupport.park(this),而unlock方法实质是调用了LockSupport.unpark(s.thread)。park和unpark方法有个特性:unpark函数可以先于park调用。比如线程B调用unpark函数,给线程A发了一个“许可”,那么当线程A调用park时,它发现已经有“许可”了,那么它会马上再继续运行。这个特性,也给了在切点4处的线程一个回到tryAcquire的机会。线程回到tryAcquire就有机会成功,有机会获得锁。
针对这个我们可以做个实验,如下表所示:
public static void main(String[] args) {
Thread thread = Thread.currentThread();
LockSupport.unpark(thread);//释放许可
LockSupport.park();// 获取许可
System.out.println("aaaa");
}
3、ReentrantLock的解锁流程
因为释放锁的逻辑一定是在得到锁的线程里调用的(其他线程调用会抛异常),其实就是个单线程操作,不涉及到多线程。主要有以下2步操作:
(1) 更改锁的占用状态(state、ownerthread)。
(2) unpark等待队列的head线程。
4、ReentrantLock中的公平与非公平是什么意思,以及怎么实现的?
这里的公平性是指请求锁的线程按照FIFO的顺序获得锁。为了维护顺序性,在公平锁和非公平锁中,获得锁的顺序会有细微差别。主要体现为:在公平锁的时候会判断是否是CLH队列的head,如果不是,要入队,然后进入acquireQueued阶段,再获得锁。而非公平锁要方便得多,类似于插队一样,直接修改锁状态获得锁。
5、重入锁是什么概念?
直接给个例子:
public static void main(String[] args)
{
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
System.out.println( "1111" );
lock.lock();//重入(如果不是重入锁,那么就死锁了)
try {
System.out.println( "2222" );
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}
上面就是重入锁,下面为了对比给个非重入锁的例子:
public class TestPark {
public static void main(String[] args)
{
SpinLock lock = new SpinLock();
lock.lock();
try {
System.out.println( "1111" );
lock.lock();//不能重入,会死锁在这里
try {
System.out.println( "2222" );
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}
}
class SpinLock {
private AtomicReference<Thread> sign =new AtomicReference<Thread>();
public void lock(){
Thread current = Thread.currentThread();
while(!sign .compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
sign .compareAndSet(current, null);
}
}
通过上面的代码中的注释,应该比较明白重入的含义。ReentrantLock的重入逻辑体现在:tryAcquire方法中,就是对持有锁的线程再请求锁的时候,只是进行计数加1的操作。并且这里面体现了偏向锁的概念,没有进行CAS操作,只是简单的单线程操作(具体可参考代码)。
6、参考资料
(2)Java的LockSupport.park()实现分析