synchronized是托管给JVM执行的,Lock的锁定是通过代码实现的。所以Lock比较灵活,可以便于开发人员根据合适的场景进行操作,Lock是一个接口,需要实现它来进行使用,ReetrantLock是Lock的主要实现类,ReetrantLock是一个可重入锁,同时可以指定公平锁和非公平锁,我们来具体看一下他的实现方式。
ReentrantLock lock = new ReentrantLock(); //如果被其它线程占用锁,会阻塞在此等待锁释放 lock.lock(); try { //操作 } catch (Exception e) { e.printStackTrace(); } finally { //释放锁 lock.unlock(); } 复制代码
上面只是其中一种方式,可以看到ReentrantLock的使用方式比较简单,创建出一个ReentrantLock对象,通过lock()方法进行加锁,使用unlock()方法进行释放锁操作。
他的加锁方式有三种,使用lock、trylock、trylock(long,TimeUnit)指定时间参数。使用lock来获取锁的话,如果锁被其他线程持有,那么就会处于等待状态。另外需要我们去主动的调用unlock方法去释放锁,即使发生异常,他也不会主动释放锁,需要我们显式的释放。使用trylock方法获取锁,是有返回值的,获取成功返回true,获取失败返回false,不会一直处于等待状态。使用trylock(long,TimeUnit)指定时间参数来获取锁,在等待时间内获取到锁返回true,超时返回false。还可以调用lockInterruptibly方法去中断锁,如果线程正在等待获取锁,可以中断线程的等待状态。
上面我们说ReentrantLock是一个可重入锁,那么,什么是可重入锁呢?可重入锁是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生。我们先来看下验证可重入锁的代码:
public static class TestReentrantLock { private Lock lock = new ReentrantLock(); public void method() { lock.lock(); try { System.out.println("方法1获得ReentrantLock锁"); method2(); } finally { lock.unlock(); } } public void method2() { lock.lock(); try { System.out.println("方法2重入ReentrantLock锁"); } finally { lock.unlock(); } } public static void main(String[] args) { new TestReentrantLock().method(); } } 复制代码
由上面的代码我们可以得知,ReentrantLock是具有可重入性的。那么,这种可重入的底层实现方式是什么呢?
ReentrantLock是使用AQS中的state的值,线程可以不停地lock来增加state的值,对应地需要unlock来解锁,直到state为零。并且state的值是用volatile进行修饰,以下是具体源码实现。
//java.util.concurrent.locks.ReentrantLock.FairSync protected final boolean tryAcquire(int acquires) { //获取当前线程 final Thread current = Thread.currentThread(); int c = getState(); //当前锁没被占用 if (c == 0) { //1.判断同步队列中是否有节点在等待 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {//2.如果上面!1成立,修改state值(表明当前锁已被占用) //3.如果2成立,修改当前占用锁的线程为当前线程 setExclusiveOwnerThread(current); return true; } } //占用锁线程==当前线程(重入) else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires;// if (nextc < 0) throw new Error("Maximum lock count exceeded"); //修改status setState(nextc); return true; } //直接获取锁失败 return false; } 复制代码
我们前面说了ReentrantLock可以实现公平锁和非公平锁,那么什么是公平锁?什么是非公平锁呢? 所谓的公平锁,就是在多个线程请求获取同一个资源的时候,能够保证依照线程请求的顺序,依次执行,来保证公平竞争的效果。反之,非公平锁就是不按照线程请求顺序,每个线程一起去争抢锁,谁抢到是谁的。
上面我们说到ReentrantLock中的公平锁是通过继承AQS来实现的,我们来看下他的具体实现方式。
AQS是一个抽象类,主要是通过继承的方式来使用。AQS的功能分为两种:独占和共享。AQS的实现依赖内部的同步队列,也就是FIFO的双向队列,如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。
当有锁竞争的时候,当有新的线程加入进来,会将此线程封装成Node节点追加到同步队列中,并将新线程的前置指针指向上一个节点,将上一个节点的后置指针指向新线程的节点。是通过CAS来进行指针指向的这个修改的。
当头结点在释放锁时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,设置头节点不需要用CAS,原因是设置头节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要CAS保证。
加入同步队列(当同步队列为空时会直接获得锁),等待锁
//java.util.concurrent.locks.ReentrantLock.FairSync final void lock() { acquire(1); } //java.util.concurrent.locks.AbstractQueuedSynchronizer public final void acquire(int arg) { if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } 复制代码
tryAcquire():模板方法,获取锁
//java.util.concurrent.locks.AbstractQueuedSynchronizer //1 private Node addWaiter(Node mode) { //生成node Node node = new Node(Thread.currentThread(), mode); Node pred = tail; if (pred != null) { //将node加到队列尾部 node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //如果加入失败(多线程竞争或者tail指针为null) enq(node); return node; } //1.1 private Node enq(final Node node) { //死循环加入节点(cas会失败) for (;;) { Node t = tail; if (t == null) { //tail为null,同步队列初始化 //设置head指针 if (compareAndSetHead(new Node()))//注意这里是个空节点!! tail = head;//将tail也指向head } else { node.prev = t;//将当前node加到队尾 if (compareAndSetTail(t, node)) { t.next = node; return t;//注意这里才返回 } } } } //2 final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { //表示是否被打断 boolean interrupted = false; for (;;) { //获取node.pre节点 final Node p = node.predecessor(); if (p == head //当前节点是否是同步队列中的第二个节点 && tryAcquire(arg)) {//获取锁,head指向当前节点 setHead(node);//head=head.next p.next = null;//置空 failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && //是否空转(因为空转唤醒是个耗时操作,进入空转前判断pre节点状态.如果pre节点即将释放锁,则不进入空转) parkAndCheckInterrupt())//利用unsafe.park()进行空转(阻塞) interrupted = true;//如果Thread.interrupt()被调用,(不会真的被打断,会继续循环空转直到获取到锁) } } finally { if (failed)//tryAcquire()过程出现异常导致获取锁失败,则移除当前节点 cancelAcquire(node); } } 复制代码
acquireQueued(addWaiter(Node.EXCLUSIVE), arg):加入同步队列
//java.util.concurrent.locks.AbstractQueuedSynchronizer //1 private Node addWaiter(Node mode) { //生成node Node node = new Node(Thread.currentThread(), mode); Node pred = tail; if (pred != null) { //将node加到队列尾部 node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //如果加入失败(多线程竞争或者tail指针为null) enq(node); return node; } //1.1 private Node enq(final Node node) { //死循环加入节点(cas会失败) for (;;) { Node t = tail; if (t == null) { //tail为null,同步队列初始化 //设置head指针 if (compareAndSetHead(new Node()))//注意这里是个空节点!! tail = head;//将tail也指向head } else { node.prev = t;//将当前node加到队尾 if (compareAndSetTail(t, node)) { t.next = node; return t;//注意这里才返回 } } } } //2 final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { //表示是否被打断 boolean interrupted = false; for (;;) { //获取node.pre节点 final Node p = node.predecessor(); if (p == head //当前节点是否是同步队列中的第二个节点 && tryAcquire(arg)) {//获取锁,head指向当前节点 setHead(node);//head=head.next p.next = null;//置空 failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && //是否空转(因为空转唤醒是个耗时操作,进入空转前判断pre节点状态.如果pre节点即将释放锁,则不进入空转) parkAndCheckInterrupt())//利用unsafe.park()进行空转(阻塞) interrupted = true;//如果Thread.interrupt()被调用,(不会真的被打断,会继续循环空转直到获取到锁) } } finally { if (failed)//tryAcquire()过程出现异常导致获取锁失败,则移除当前节点 cancelAcquire(node); } } 复制代码
selfInterrupt(): 唤醒当前线程
static void selfInterrupt() {//在获取锁之后 响应intterpt()请求 Thread.currentThread().interrupt(); } 复制代码
以上就是ReadWriteLock原理及源码的解读,希望大家能有收获。
部分内容参考: juejin.im/post/5ae1b4…