从流程图的角度分析ReentrantLock

        网上关于锁的资料非常多,关于源代码分析的也非常多。但是这里我觉得依然有必要记录下自己的理解,或者从另一个角度再看看这个问题。本文是结合网上的参考资料以及jdk1.6的源代码进行的分析。

1、预定义的一些问题

(1)      ReentrantLock的lock流程是什么样子的?

(2)      ReentrantLock的unlock流程是什么样子的?

(3)      ReentrantLock中的公平与非公平是什么意思,以及怎么实现的?

(4)      重入锁是什么概念?

下面开始进入正文,来直接分析下:

2、ReentrantLock的加锁流程图

         这里没有明显区分(只有开始的地方有标注)ReentrantLock的公平与非公平的方式,先给出整体的流程图如下:

从流程图的角度分析ReentrantLock_第1张图片

图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 sign =new AtomicReference();

	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、参考资料

(1)《深入浅出 Java Concurrency》目录

(2)Java的LockSupport.park()实现分析

(3)深入JVM锁机制2-Lock




你可能感兴趣的:(jdk源码分析)