一、ReentrantLock可重入锁
什么是可重入锁,即一个线程获取锁之后那么她在后面调用同一个锁的lock方法是不会阻塞的,依然可以获得该锁,这样可以避免死锁的发生,synchronize就是可重入锁。
ReentantLock的UML类图大概是这样的
这个是不是似曾相识,上一章说的同步工具CountDownLatch、Semaphore的UML类图也是这样的,都是通过AQS来实现公平和非公平的锁。ReentrantLock利用AQS实现了几个核心要素
- state变量标记该锁的状态,0为没有锁,大于0表示有线程占用锁,大于1的时候表示该线程有锁重入。对state变量的操作采用CAS
- exclusiveOwnerThread记录是哪个线程占用锁
- 当线程抢夺锁失败之后需要阻塞,Unsafe类提供了阻塞或唤醒线程的一对操作原语,也就是park/unpark。
- 被阻塞的线程会放入AQS的阻塞队列中排队
接下来我们就从源码的角度看一下ReentrantLock的实现
默认创建一个 非公平锁
NofairSync继承Sync,里面有lock()方法和尝试获取锁的方法。我们来看一下ReentrantLock的lock方法
调用了 NofairSync的lock方法(非公平锁是NofairlSync公平锁是FairSync),继续跟进代码。
尝试加锁,即尝试将state变量从0通过Unsafe的CAS变成1。如果加锁成功则将ownerThread变成当前线程。加锁失败则进入acquire(1)方法。
调用tryAcquire(arg)再次尝试获取锁,这次会判断当前持有锁的线程是否就是该线程,如果是该线程就是锁重入,state变量的值加1,并且成功持有锁,返回true,否则返回false。当没有获取到锁时会继续执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。这个方法是否也是似曾相识,我们在分析Semaphore的源码的时候当线程在获取信号量失败的时候也会调用该方法,主要的就是将线程放入到阻塞队列中,并且判断是否为阻塞队列的头节点,如果是头节点则继续尝试获取锁,否则将park线程。这里acquireQueued方法中先调用了LockSupport的park方法,该方法是可以响应打断的,所以这里会返回打断标识,来判断是被打断的还是调用了unPark方法,如果是被打断的并且获得了锁,则出来之后调用selfInterrupt方法来响应打断。所以线程在调用lock方法获取锁的阻塞期间不能响应打断,但是获取锁之后是可以响应打断的。分析完Lock方法我们来看一下unLock方法
这里会调用AQS的release方法,进入方法具体看一下
这里应该是通过tryRelease(arg)去尝试解锁 ,进入方法
判断是否是持有锁的线程,如果不是持有锁的线程进行解锁会抛出异常,如果是当前线程为持有锁的线程则判断state变量是否是0,如果大于0则说明线程重入过,解锁失败,否则解锁成功将state赋值为0并将持有线程设置为null。然后调用unparkSuccessor(h)
方法唤醒等待队列的队头线程。
lockInterruptibly()方法,这个方法是在阻塞等待锁的时候也可以被打断,这个方法和lock方法唯一不同的地方就是这里parkAndCheckInterrup()方法返回打断标识为被打断的时候会直接抛出被打断标识,而不是更新被打断状态。
tryLock()方法,即每次尝试获取锁,如果没有获取到锁则判断是否可重入,如果当前的锁线程不是当前线程这直接返回false而不是阻塞等待,这就是tryLock方法。
说到了ReentrantLock就不得不说Condition,Condition对于ReentrantLock就相当于wait()、notify()对于synchronized锁。但是notify()会唤醒所有的wait()线程而Condition的signal()方法只会唤醒调用了某个Condition的await方法的线程。Condition相当于Lock的一个等候厅。打比方有一个Lock给一个房间上锁了,可以创建多个等候厅,也就是通过Lock的newCondition方法来创建的。所以一个等候厅调用signal方法只会唤醒当前等候厅里面的人。和wait()、notify()相同点是使用Condition的方法之前也需要获取锁。Condition具体的使用场景可以看之前我对于BlockingQueue的源码分析,里面就利用了Condition来实现队列空和队列满时的阻塞以及对应的唤醒。
二、读写锁
先来看一下读写锁的UML类图
可以看到所有的同步工具同步锁都离不开AQS。
可以看到这里的读写锁包括Sync类都是以静态内部类的形式存在ReentrantReadWriteLock里面的,并且提供了protect的构造方法。静态内部类的形式我想到了我们创建单例模式的时候使用的静态内部类的形式来创建。这就再获取读写锁的时候避免的构造方法溢出以及多次获得读写锁都是同一把锁的问题。
先来看看读锁的实现,从lock方法看起。
进入方法。
所有的锁都是先调用Sync的tryAcquireXXX模板方法去尝试修改state变量,修改成功就获取到锁,没获取到锁就进行阻塞。这里是返回小于0的数就是没有获取到锁,进入尝试获取锁的方法。
看一下exclusiveCount(int c)方法,这个方法是判断是否加有写锁的方法。这里注意上面的几个变量
1、SHARED_UNIT 将1左移16位得到的数为25536,用二进制表示就是10000000000000000
2、MAX_COUNT、EXCLUSIVE_MASK 这两个数都是将1向左移16位并减1,用二进制表示即1111111111111111
exclusiveCount(int c)将state变量和EXCLUSIVE_MASK做与运算,因为一个int类型标识32位,就是state换成32位的二进制和EXCLUSIVE_MASK做与运算。刚开始state为0所以会加锁成功。所以继续往下走,将state改成65536。这样看不出来什么效果,接下来就看一下先加读锁后面再添加一个读锁。通过exclusiveCount(int c)方法结果还是0,所以还会加锁成功。将state的值再加上25536。这就是读锁。在来看一下写锁是怎么实现的。
先判断是否加过锁,如果加过锁则判断是否是当前线程加了写锁,因为加读锁的时候没有将exclusiveOwnerThread变量赋值为当前线程的,所以这里的thread并不等于当前线程,所以加锁失败。
读写锁相对比较复杂,我这边理解的也不够透彻,所以语言组织的不够好。大概的意思就是将一个64位的int类型state变量分成高16位表示读锁,低16位标识写锁,使用一个变量的高低十六位来表示是因为CAS操作只能针对一个变量,无法分开操作。