一、前言
在Java 5.0之前,在协调对共享对象的访问的时可以使用的机制只有synchronized 和 volatile。Java 5.0 增加了一种新的机制:ReentrantLock 。与之前提到过的机制相反,ReentrantLock 并不是一种替代内置加锁的方法,而是当内置解锁机制不适用时,作为一种可选择的高级功能。
二、简介
ReentrantLock 重入锁实现了 Lock和 java.io.Serializable接口,并提供了与synchronized相同的互斥性和内存可见性,ReentrantLock 提供了可重入的加锁语义,能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞,并且与synchronized相比,它还为处理锁的不可用性提供了更高的灵活性,与此同时,ReentrantLock 还支持公平锁和非公平锁两种方式。
ReentrantLock类层次结构
ReentrantLock实现了 Lock和 Serializable接口,内部有三个内部类,Sync、NonfairSync、FairSync
Sync 是一个抽象类型,它继承 AbstractQueuedSynchronizer,这个AbstractQueuedSynchronizer是一个模板类,它实现了许多和锁相关的功能,并提供了钩子方法供用户实现,比如 tryAcquire、tryRelease等。Sync实现了AbstractQueuedSynchronizer的tryRelease方法。
NonfairSync和FairSync两个类继承自Sync,实现了lock方法,然后分别公平抢占和非公平抢占针对tryAcquire有不同的实现。
三、可重入性
可重入锁,也叫做 递归锁,从名字上理解,字面意思就是再进入的锁,重入性是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,首先他需要具备两个条件:
线程再次获取锁:所需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次获取成功
锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其它线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前线程被重复获取的次数,而被释放时,计数自减,当计数为0时表示锁已经成功释放。
2.1 锁的实现
使用ReentrantLock 案例:
Lock lock = new ReentrantLock(); lock.lock(); try{ //更新对象状态 //捕获异常,并在必须时恢复不变性条件 }catch (Exception e){ e.printStackTrace(); } finally { lock.unlock(); }
上述代码中是使用Lock接口的标准使用方式,这种形式比使用内置锁(synchronized )复杂一些,必须要在 finally 块中释放锁,否则,如果在被保护的代码中抛出了异常,那么这个锁永远都无法释放。
四、ReentrantLock 源码分析
在简介中我们知道 ReentrantLock继承自 Lock接口,Lock提供了一些获取锁和释放锁的方法,以及条件判断的获取的方法,通过实现它来进行锁的控制,因为它是显示锁,所以需要显示指定起始位置和终止位置,下面就来介绍一下Lock接口的方法介绍:
ReentrantLock 也实现了上面接口的内容,同时 ReentrantLock 提供了 公平锁和 非公平锁两种模式,如果没有特别的去指定使用何种方式,那么 ReentrantLock 会默认为 非公平锁,首先我们来看一下 ReentrantLock 的构造函数:
/** * 无参的构造函数 */ public ReentrantLock() { sync = new NonfairSync(); } /** * 有参构造函数 * 参数为布尔类型 */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
从上述源码中我们可以看到:
ReentrantLock 优先使用的是无参构造函数,也就是非公平锁,但是当我们调用有参构造函数时,可以指定使用哪种锁来进行操作(公平锁还是非公平锁),参数为布尔类型,如果指定为 false 的话代表 非公平锁 ,如果指定为 true 的话代表的是 公平锁
Sync 类 是 ReentrantLock 自定义的同步组件,它是 ReentrantLock 里面的一个内部类,它继承自AQS(AbstractQueuedSynchronizer),Sync 有两个子类:公平锁 FairSync 和 非公平锁 NonfairSync
ReentrantLock 的获取与释放锁操作都是委托给该同步组件来实现的。下面我们来看一看非公平锁的 lock() 方法:
4.1 非公平锁 NonfairSync.lock()
1、NonfairSync.lock() 方法流程图
2、lock方法详解
在初始化 ReentrantLock 的时候,如果我们不传参,使用默认的构造函数,那么默认使用非公平锁,也就是 NonfairSync
当我们调用 ReentrantLock 的 lock() 方法的时候,实际上是调用了 NonfairSync 的 lock() 方法,代码如下:
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { //这个方法先用CAS操作,去尝试抢占该锁 // 快速尝试将state从0设置成1,如果state=0代表当前没有任何一个线程获得了锁 if (compareAndSetState(0, 1)) //state设置成1代表获得锁成功 //如果成功,就把当前线程设置在这个锁上,表示抢占成功,在重入锁的时候需要 setExclusiveOwnerThread(Thread.currentThread()); else //如果失败,则调用 AbstractQueuedSynchronizer.acquire() 模板方法,等待抢占。 acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
3.调用 acquire(1) 实际上使用的是 AbstractQueuedSynchronizer 的 acquire() 方法,它是一套锁抢占的模板,acquire() 代码比较简单:
public final void acquire(int arg) { //先去尝试获取锁,如果没有获取成功,就在CLH队列中增加一个当前线程的节点,表示等待抢占。 //然后进入CLH队列的抢占模式,进入的时候也会去执行一次获取锁的操作,如果还是获取不到, //就调用LockSupport.park() 将当前线程挂起。那么当前线程什么时候会被唤醒呢?当 //持有锁的那个线程调用 unlock() 的时候,会将CLH队列的头节点的下一个节点上的线程 //唤醒,调用的是 LockSupport.unpark() 方法。 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
- acquire() 会先调用 tryAcquire() 这个钩子方法去尝试获取锁,这个方法就是在 NonfairSync.tryAcquire()下的 nonfairTryAcquire(),源码如下:
//一个尝试插队的过程 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); //获取state值 int c = getState(); //比较锁的状态是否为 0,如果是0,当前没有任何一个线程获取锁 if (c == 0) { //则尝试去原子抢占这个锁(设置状态为1,然后把当前线程设置成独占线程) if (compareAndSetState(0, acquires)) { // 设置成功标识独占锁 setExclusiveOwnerThread(current); return true; } } //如果当前锁的状态不是0 state!=0,就去比较当前线程和占用锁的线程是不是一个线程 else if (current == getExclusiveOwnerThread()) { //如果是,增加状态变量的值,从这里看出可重入锁之所以可重入,就是同一个线程可以反复使用它占用的锁 int nextc = c + acquires; //重入次数太多,大过Integer.MAX if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } //如果以上两种情况都不通过,则返回失败false return false; }