把学习当成一种习惯
选择往往大于努力,越努力越幸运

本文对源码的学习是基于JDK1.8版本.

前言

  本文主要角色 : ReentrantLock 、 AbstractQueuedSynchronizer(简称AQS).
  ReentrantLock 是一种独占式、可重入的互斥锁,即写-写、写-读、读-写、读-读都是互斥,只能一条线程占用临界资源,且默认为非公平锁,可通过构造器设置为公平锁.ReentrantLock的底层实现是基于AQS独占式去获取锁和释放锁,所以对于理解ReentrantLock我们是有必要学习AQS同步器.
【PS : AQS有两种对同步状态的获取方式 : 独占式(Exclusive,只能一条线程执行) 和 共享式(Share,多个线程可同时执行) , 本文基于ReentrantLock的基础上在独占模型下进行源码分析 】

AbstractQueuedSynchronizer --- 队列同步器

  AQS主要维护了一条带有头节点(head)和尾结点(tail)的双向链表的同步队列、一个volatile int state(代表着同步状态)、一个静态内部Node结点类,接下来看看AQS提供的这三种资源的各个作用 :

  • state : 代表着同步状态,默认初始化为0,即没有被线程占用状态.该属性AQS提供了三个函数来访问和设置 :
    • getState() : 获取state;
    • setState(int newState) : 设置state
    • compareAndSetState(int expect, int update) : CAS设置state,线程是通过该方法来获取锁
  • 静态内部Node结点类 如下图 :

代码如下 :


static final class Node {

// 共享式
static final Node SHARED = new Node();
// 独占式
static final Node EXCLUSIVE = null;

// ------ 结点的四种状态(对应下面waitStatus属性) ------

static final int CANCELLED =  1;

static final int SIGNAL    = -1;

static final int CONDITION = -2;

static final int PROPAGATE = -3;

// ------ 同步队列中存放结点(Node)的四种核心属性 ------

// 对应上面四种结点状态
volatile int waitStatus;
// 双向链表的上一结点指针,即前驱结点
volatile Node prev;
// 双向链表的下一结点指针,即后继结点
volatile Node next;
// 线程信息
volatile Thread thread;

// 条件队列的属性,即用到Condition的时候会使用到,这里可以反应出条件队列是一条单向链表
Node nextWaiter;

// 该结点是否为共享式
isShared();

// 获取前驱结点
predecessor()

/** 接下来是三个构造函数 **/

}

结点的几种状态 :

  • CANCELLED(1) : 表示线程已取消.
  • SIGNAL(-1): 表示后继结点在等待当前结点唤醒.后继结点入队时,会将前继结点的状态更新为SIGNAL(结点的状态是存放在前驱结点的).
  • CONDITION(-2) : 表示结点在Condition对象的条件队列上,当其他线程调用了Condition的signal()函数后,CONDITION状态的结点将从条件队列转移到同步队列中,等待获取同步锁.
  • PROPAGATE(-3) : 共享模式下,前驱节点不仅会唤醒其后继节点,同时也可能会唤醒后继的后继节点,即传播式的唤醒后继结点
  • 0 : 默认初始化的值为0

  nextWaiter实现的是单向链表的条件队列,与Condition有关,当调用Condition的await()才会进入对应的Condition对象的条件队列(一个Lock可以有多个Condition);当调用Condition的signal()会进入对应的Condition对象的条件队列进行唤醒操作,而基于独占式和共享式有不同的唤醒操作 :

nextWaiter 说明
独占式 只唤醒一条线程
共享式 可能唤醒多条线程
  • 双向链表实现的同步队列,包含一下特点 :
    • 同步队列的头结点是持有锁的.
    • 同步队列拥有head结点和tail结点,可快速访问到头尾结点,即出队、入队操作
    • 同步队列中存储的结点都是Node结点,一个Node结点对应的就是一条线程信息
    • 在独占式模式下,只能一条线程竞争同步状态(state)成功,其他竞争失败的线程会被封装成Node结点从尾部中加入同步队列
  • 双向链表实现的同步队列,如下图 :

AQS数据结构 --- 小结

  • AQS维护了一条双向链表的同步队列、volatile int state的同步状态、一个静态内部类Node结点,并且同步队列中存放的都是封装线程信息的Node结点,只有head结点是持有锁的;
  • AQS对于同步状态的访问操作拥有独占式、共享式两种方式,本文只重点围绕独占式模型,共享式会独立一篇文章;
  • Condition知识点没了解过的可暂时跳过(Node结点的nextWaiter属性与Condition有关联,也可跳过该属性).

ReentrantLock --- 源码分析

  ReentrantLock可分为非公平、公平的重入锁,默认为非公平锁;ReentrantLock的底层是基于AQS的独占式实现,结合上面AQS知识点的理解,现在我们来深入理解ReentrantLock,接下来我们看看非公平、公平模式下的不同实现思路.
ReentrantLock的抽象接口 Lock , 代码如下:


public interface Lock {

// 获取锁 : 该方法如果锁已被其他线程获取,则进行等待
void lock();

// 获取锁 : 该方法如果锁已被其他线程获取,则会返回false,否则返回true,无需等待
boolean tryLock();

// 超时获取锁 : 同上面的tryLock类似,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false;如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

// 可中断获取锁 : 如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态
void lockInterruptibly() throws InterruptedException;

// 释放锁 : 持有锁线程释放锁
void unlock();

// 创建一个条件对象,即条件队列
Condition newCondition();

}

本文核心学习常用的 获取锁(即lock()函数) 和 释放锁(即unlock()) 这两个函数 :


    // 通过sync同步器获取锁,syn就是AQS了,往下继续看
    public void lock() {
        sync.lock();
    }
    
    // 通过sync同步器释放锁
    public void unlock() {
        sync.release(1);
    }

ReentrantLock的两种锁类型,即非公平锁和公平锁,都是通过构造器来创建,代码如下 :


// 同步器,有两种方式,即非公平和公平
private final Sync sync;

public ReentrantLock() {
   // 默认使用非公平同步器
   sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
   sync = fair ? new FairSync() : new NonfairSync();
}

// NonfairSync 为非公平锁实现类
static final class NonfairSync extends Sync {
......
}

// FairSync 为公平锁实现类
static final class FairSync extends Sync {
......
}
    

具体看看Sync , 代码如下 :


// 继承了我们熟悉的AQS同步器
abstract static class Sync extends AbstractQueuedSynchronizer {

   // 抽象方法 : 获取锁,具体实现是公平锁实现类和非公平锁实现类
   abstract void lock(); 
   
   // 非公平方式获取锁
   final boolean nonfairTryAcquire(int acquires) {
   ......
   }
   
   // 释放锁
   protected final boolean tryRelease(int releases){
   ......
   }
   
   // 当前线程是否持有锁
   protected final boolean isHeldExclusively(){
   ......
   }

}

  ReentrantLock的实现,是通过Sync同步器来维护同步状态(state),我们只需要实现同步状态(state)的获取与释放方式即可,至于具体线程在同步队列、条件队列的维护(如获取锁失败入队、条件队列唤醒出队等操作),AQS已经在低层实现好了,不用我们关心.

非公平锁 --- NonfairSync

非公平独占式获取锁 --- tryAcquire()函数

非公平实现类 代码如下:


    static final class NonfairSync extends Sync {
    
        private static final long serialVersionUID = 7316153563782823691L;

        // 1. 获取锁
        final void lock() {
            // 2. CAS成功,表示成功获取锁
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
            // 3. 否则,失败,这个函数是AQS提供的
                acquire(1);
        }

        // 最终是执行这个函数 : 这个才是真正的独占式获取锁实现函数
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

接下来看看AQS提供的acquire()函数 ,代码如下 :


    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 竞争锁失败,加入同步队列并且挂起,竞争锁期间只要被中断过就需要执行这个函数,该函数表示真正的中断线程
            selfInterrupt();
    }
    

总共有四个函数,第一个就是独占式获取锁函数 --- tryAcquire() ,代码如下 :


    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

  AQS 提供的是一个抛出异常???上面我们说过,AQS只负责底层同步队列、条件队列的维护,至于同步状态state的获取和释放锁是我们自己实现,所以可以推断出这个函数真正实现是在非公平锁实现类 , 如下图 :

点击非公平锁,将跳转到非公平锁实现类的 tryAcquire() 函数,代码如下 :


    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
    
    // 真正的非公平锁独占式获取锁函数,是我们自己代码层面实现
    final boolean nonfairTryAcquire(int acquires) {
        
        // 1. 获取当前线程
        final Thread current = Thread.currentThread();
        // 2. 获取当前同步状态state
        int c = getState();
        // 3. 当前没有线程占用锁
        if (c == 0) {
            // 4. CAS尝试获取锁
            if (compareAndSetState(0, acquires)) {
                // 4.1 CAS设置成功,即获取锁成功
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            // 5. 持有锁线程为当前线程
            
            // 5.1 可重入,+1
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            // 5.2 由于是独占式,所以这里设置同步资源state无需CAS设置
            setState(nextc);
            return true;
        }
        // 6. 尝试获取锁失败,返回false
        return false;
    }

  如果上面的独占式获取锁失败,则会执行addWaiter(Node.EXCLUSIVE) 以独占式结点加入到同步队列中, 代码如下 :


    private Node addWaiter(Node mode) {
        // 1. 为当前线程封装成独占式Node结点
        Node node = new Node(Thread.currentThread(), mode);
        // 2. 获取同步队列尾结点
        Node pred = tail;
        // 3.尾结点不为null
        if (pred != null) {
            // 3.1 当前结点与尾结点prev接上
            node.prev = pred;
            // 3.2 CAS 设置当前结点为尾结点
            if (compareAndSetTail(pred, node)) {
                // 3.3 CAS设置当前结点为尾结点成功 设置next字段
                pred.next = node;
                // 3.4 入队成功
                return node;
            }
        }
        // 4. 上面CAS入队失败,继续尝试入队
        enq(node);
        return node;
    }
    
    // 自旋的方式入队,不入队誓不罢休 重点是2 ~ 4 步骤
    private Node enq(final Node node) {
        // 1. 自旋
        for (;;) {
            // 1.1 获取tail结点
            Node t = tail;
            if (t == null) {
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 重点是这里
                // 2. 当前结点于尾结点连接起来(prev)
                node.prev = t;
                // 3. CAS设置 当前结点为tail,即tail指向当前结点
                if (compareAndSetTail(t, node)) {
                    // 4. CAS设置成功,设置next属性
                    t.next = node;
                    return t;
                }
                // 5. CAS失败,自旋继续入队操作
            }
        }
    }

  这里重点聊聊enq()函数的 2 ~ 4步骤中 CAS操作的前后操作(prev和next)会导致同步队列可能出现的问题 :
  第 2、3步骤中结合使用并非原子操作,所以如果同时出现两个结点入队,即出现多个结点并发入队,会出现如下图情况 : 总是可以保证成功入队的结点通过prev与队列连接上,换句话说就是通过尾结点的前驱结点向前遍历,总是可以遍历查询整条链表的结点数据.(prev 引用是要可靠的)

  第3、4步骤中结合使用并非原子操作,所以如果出现两个结点CAS入队成功,由于设置next属性是在CAS操作之后,所以可能存在tail指针变更,导致有可能存在有一些结点的next属性没设置的情况,如下图 :

【PS : 从上面的图可以得出结论 : prev 是可靠的, 而 next 有时会为 null, 但并不一定真的就没有后继结点,即next是不可靠的;所以通过从 tail 开始反向查找, 借助可靠的 prev 引用来定位到指定的结点!!!】
【PS : 参考链接 : Java AQS unparkSuccessor 方法中for循环从tail开始而不是head的疑问?

  上面的addWaiter() 入队成功后,开始执行 acquireQueued()函数 , 代码如下 :


    final boolean acquireQueued(final Node node, int arg) {
        // 是否成功竞争到锁
        boolean failed = true;
        try {
            // 等待过程中是否被中断过,竞争锁期间不响应中断,只是一个标识
            boolean interrupted = false;
            // 自旋
            for (;;) {
                // 1. 获取前驱结点
                final Node p = node.predecessor();
                // 2. 如果前驱结点为头节点,执行tryAcquire竞争锁
                if (p == head && tryAcquire(arg)) {
                    // 2.1 竞争锁成功,设置为头结点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false; // 成功竞争到锁
                    return interrupted; // 返回等待过程中是否被中断过
                }
                // 3. 如果前驱结点不是头结点,则走这里
                // 第一个函数为设置结点状态
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 如果等待过程中被中断过,就将interrupted标记为true
                    // 从这里也可以看出在竞争锁期间是不响应中断的,即并不会抛出中断异常,只是标识为true
                    interrupted = true; 
            }
        } finally {
            // 竞争锁成功是无需执行这个函数的
            if (failed)
              // 竞争锁失败,被中断取消竞争锁才会执行这个函数,取消同步队列同步队列中的等待结点
              cancelAcquire(node);
        }
    }
    
    // 设置结点状态,把当前结点状态保存在前驱结点中
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 获取前驱结点状态
        int ws = pred.waitStatus;
        
        // 1. 如果ws状态为SIGNAL 执行parkAndCheckInterrupt()函数挂起当前线程
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
        // 2. ws>0 即为取消结点,向前遍历寻找状态不大于0的结点,这个过程会回收这些已取消的结点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        // 3. ws 既不大于0也不等于-1 CAS设置前驱结点状态为SIGNAL,即当前结点状态保存在前驱结点中
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 4. 执行到这里将回到上面自旋中
        return false;
    }
    
    // 挂起当前线程
    private final boolean parkAndCheckInterrupt() {
        // 挂起线程
        LockSupport.park(this);
        // 被唤醒,继续执行,并且查看自己是不是被中断的
        return Thread.interrupted();
    }

非公平独占式获取锁小结,并且如下图

  • 头结点持有同步资源,且next结点会自旋的方式去尝试获取锁(第一次尝试还是失败的话,会被设置为SIGNAL状态,第二次还是尝试失败则挂起了)
  • 结点的状态保存在前驱结点中(为什么这样设计?这里先保留这个疑问)
  • 链表入队操作是在尾结点中进行,并且链表的next指针不一定是有效的,而prev指针是一定有效的
  • 每个结点代表一条线程信息,且结点的nextWaiter指针是要结合Conidtion使用,即条件队列,本文对这个知识点暂不叙述
  • 竞争锁失败 --> 入队并且进入阻塞状态 --> 竞争锁成功 ,前两个步骤在竞争锁过程中,是不响应中断的,只是一个标识,而到第三个步骤竞争锁成功,发现是有被中断过,才真正执行中断
  • 如果线程1调用lock(),而此时持有锁线程刚好把state设置为0,那么线程1刚好竞争到锁,导致线程1这种后来的线程比存在于同步队列的先来的线程优先获取到锁,这就是不公平性.

【PS : 上图是state 基于非可重入状态】

非公平独占式释放锁 --- tryRelease()函数

ReentrantLock释放锁函数unlock(),直接看代码 :

 
 // 调用同步器的release()
 public void unlock() {
    sync.release(1);
 }   
 
  
 public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
       if (h != null && h.waitStatus != 0)
         unparkSuccessor(h);
         return true;
       }
       return false;
  }
 
 // 抛出一个异常,同获取锁函数是一个道理的
 protected boolean tryRelease(int arg) {
     throw new UnsupportedOperationException();
 }
 

非公平和公平方式的释放锁操作都是执行的是Sync的同一个函数 : tryRelease()


   // 释放锁真正执行的函数
   protected final boolean tryRelease(int releases) {
        // 由于是可重入锁,所以释放一次锁state减少1 (参数传送的是1)
        int c = getState() - releases;
        
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        
        // state为0 说明真正释放锁了
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        // 设置state
        setState(c);
        return free;
    }

  回到release(),当tryRelease()返回true的时候,则执行unparkSuccessor(h) : 该函数的作用就是执行唤醒后继结点


    public final boolean release(int arg) {
     // 返回true的话 说明释放锁成功了,看看需不需要唤醒后继结点
     if (tryRelease(arg)) {
        Node h = head;
        // 头结点不为null
        // 重点是h.waitStatus != 0这个条件,这个属性记录了下一结点的状态
        // 即下一结点如果状态为0的话,是无需执行unparkSuccessor()来唤醒后继结点参与竞争锁的
        if (h != null && h.waitStatus != 0)
          unparkSuccessor(h);
          return true;
        }
       return false;
   }

    // 寻找最靠前可唤醒的结点
    private void unparkSuccessor(Node node) {
        
        // 1. 获取头结点状态 ws
        int ws = node.waitStatus;
        // 2. 如果ws小于0 CAS设置为0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

       
        // 3. 获取下一结点 s
        Node s = node.next;
        // 4. s为空 (next是不可靠的) 或者 s结点是无效状态 需要重新寻找一个结点来唤醒
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 4.1 从尾结点开始遍历(prev是有效的),找到最靠前的waitStatus<=0且不是头结点的结点,如果存在该节点则唤醒该结点 
            for (Node t = tail; t != null && t != node; t = t.prev)
                // 4.2 waitStatus <= 0 赋值给s
                if (t.waitStatus <= 0)
                    s = t;
        }
        
        if (s != null)
        // 5. 执行唤醒
            LockSupport.unpark(s.thread);
    }


非公平独占式释放锁小结,并且如下图

非公平独占式释放锁的实现逻辑挺简单的,主要就是设置state为0,然后判断是否需要唤醒后继结点.

  • 由于ReentrantLock是一个可重入锁,即state大于1时,我们需要多次(state次)调用unlock()来释放锁,而不是一次,因为通过上面的代码分析,一次的调用只对state减1,并不是真正释放锁,而其他线程通过CAS竞争锁的话总是失败的,最终的结果就是会出现死锁.
  • unparkSuccessor() 的4.2步骤的判断条件为 waitStatus <= 0 ,个人一开始认为独占式模式下,该条件为waitStatus < 0 就行了,因为waitStatus == 0 的情况下,是addWaiter()入队操作时候才有的,即初始化状态,所以此时并没有阻塞状态;再者,acquireQueued()函数的第3步骤成功设置为SIGNAL状态才会进行挂起阻塞操作.我自己大概猜测就是 : 独占式和共享式调用的释放锁函数都是同一个函数 : tryRelease() , 所以在共享式模式下 waitStatus == 0 的情况下 是可能处于阻塞状态的,具体的到时候分析共享式模式再做分析.
  • 为什么是非公平性的,因为每个线程调用lock()后可以参与竞争锁,这样就会出现虽然同步队列中的结点是先来的,但是后来的线程却能比先来的线程优先竞争到锁的可能,导致了不公平性.

【PS : 上图是state 基于非可重入状态】

公平锁 --- FairSync

公平独占式获取锁 --- tryAcquire()函数


    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

       
        // 真正执行的是这个函数
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // 1. state == 0 ,尝试获取锁
            if (c == 0) {
                // 1.1 具体看看hasQueuedPredecessors()
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 2. 可重入
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

  我们对于非公平性获取锁已经知道,后来的线程可以参与到竞争锁中,导致后来的线程可能会比先来的线程优先竞争到锁,这就是不公平性;所以,公平性必须保证不能出现这种情况,而 hasQueuedPredecessors() 函数的作用就是体现在这里.
  只有当 hasQueuedPredecessors() 返回false时才有资格去竞争锁 ,即当链表为空时(即head和tail都等于null),或者链表只存在一个结点(即head和tail指向同一个结点),你才可以去竞争锁,否则只能乖乖的加入到同步队列;换句话说就是判断队列中有没有相关线程的节点已经在排队了,有则返回true表示线程需要排队,没有则返回false则表示线程无需排队 :


    // 第一个条件 : h !=t 
    // 第二个条件 : s = h.next
    // 第三个条件 : s.thread != Thread.currentThread()
    public final boolean hasQueuedPredecessors() {
        
        Node t = tail;
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

    // 入队操作
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

具体分析 hasQueuedPredecessors() 的两个判断条件 :

  • h != t , 要么返回false或者true
    • false : 将短路后面第二个判断条件;即当head 、tail 都为null或者指向同一个结点时,不需要排队,可直接参与竞争锁.
    • true : 表示同步队列存在至少两个结点以上,需要判断head的后继结点是否为当前结点
      • (s = h.next) == null : 如果head的后继结点为空,则表示当前同步队列至少存在两个结点以上,为什么?结合next不可靠原因,是多个结点并发入队导致的,又因为head!=tail,所以可以确认同步队列至少两个结点以上,此时后面的判断条件将短路.
      • 还有一种情况 : 初始化的时候头节点(head),尾节点(tail)都为null,此时头节点指向新的节点,但是还没来得及执行tail=head.这个时候hasQueuedPredecessors(xx)被另一个线程执行了,然后判断h!=t(hNode,tnull),结果为true.若是此时h.next==null,说明同步队列正在初始化,进一步说明有节点正在准备入队,此时整体判断就是 :同步队列里有节点在等待,也是不能参与竞争锁的
      • s.thread != Thread.currentThread() : 来到这个判断条件,说明 h.next是不为空的,所以判断是否为当前线程,如果不是,也是不能参与竞争锁的

公平独占式 --- 小结

  公平独占锁是与不公平独占锁的区别就是获取锁的方式不同,公平锁保证公平的方式在于 hasQueuedPredecessors() ,即先判断有没有节点(线程)先于当前线程排队等候锁的,若有则当前线程需要排队等候.而其他的都是一样的执行代码.

一图胜千言

扩展 --- 响应中断

  基于上面的公平锁和非公平锁的lock()函数在获取锁期间的是不响应中断的,只是对一个布尔类型的变量做标识,等真正获取到锁后,根据这个标识判断是否需要真正中断.Lock接口还提供了一个响应中断的函数 : lockInterruptibly(), 如下 :


    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    
    
    // AQS 提供的函数
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        // 是否被中断过
        if (Thread.interrupted())
            throw new InterruptedException();
        // 获取锁
        if (!tryAcquire(arg))
            // 真正要看的代码是这个函数
            doAcquireInterruptibly(arg);
    }

响应中断的函数 : doAcquireInterruptibly() :


    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 唯一区别就是这里,lock()是设置标志,而lockInterruptibly()是直接中断了
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

总结

  • ReentrantLock 是一个可重入、可公平、非公平的独占锁,底层是基于AQS同步器来维护.
  • AQS维护着一条双向链表的同步队列、一个int volatile state(同步资源);head结点是持有锁的,而head.next会以自旋的方式去尝试获取锁(第一次尝试还是失败的话,会被设置为SIGNAL状态,自旋第二次还是尝试失败则挂起了),后面的结点处于挂起阻塞状态,等待被前驱结点唤醒.
  • 公平锁和非公平锁的主要区别就是 hasQueuedPredecessors() 保证了先判断同步队列有没有结点(线程)先于当前线程排队等候锁的,若有则当前线程需要加入同步队列排队等候,而不是非公平锁可以直接竞争锁导致了不公平.

结束语

  • 原创不易
  • 希望看完这篇文章的你有所收获!

相关参考资料

  • JDK1.8
  • Java并发编程实战【书籍】

目录