ReentrantLock

735 阅读6分钟

ReentrantLock是JUC包下的一个类,和synchronized类似,可以用来保证多个线程并发的数据安全,下面对ReentrantLock和synchronized做一些比较:

  • ReentrantLock和synchronized都是可重入锁,可重入就是指当一个线程获得锁之后再次尝试获取同一个锁时不会被死锁。
  • ReentrantLock和synchronized阻塞式的互斥锁,都是要给资源上锁,但是前者是JDK1.5之后的api层面的实现,底层是调用的cas方法,而synchronized则是Java的一个关键字,实在JVM层面实现的。
  • ReentrantLock需要由加锁(lock)和解锁(unlock)两个步骤,为了避免由于执行中出现一些异常从而没有释放锁,一般建议将unlock操作放到finally语句块中。而synchronized实在JVM层面实现的,被synchronized包围的代码块被编译成字节码时,会在代码块前后加上monitorenter和monitorexit指令用于加锁和解锁,如果在执行期间同步代码块期间出现异常,JVM也会执行后续的解锁指令,确保解锁。
  • ReentrantLock可以设置多个条件变量(condition),当线程不满足的时候会进入等待,并且可以唤醒指定等待条件变量的线程,而synchronized在执行过程中只能随机唤醒或者全部唤醒等待的线程。

ReentrantLock底层实现:

由于ReentrantLock是api层面的实现,这里我们就先看一些加锁解锁相关的源码:

构造方法:

/* 
可以看到构造方法底层是创建了一个非公平锁(NonfairSync)对象,
NofairSync是ReentrantLock的一个内部类,继承自AQS同步框架,
ReentrantLock调用的某些方法底层会调用一些
非公平锁的方法,而这些方法有些是继承AQS
*/
public ReentrantLock() { sync = new NonfairSync(); }

加锁相关方法:

// 我们可以看到调用lock方法底层是调用的sync也就是上面构造方法创建的非公平锁对象的的lock方法
public void lock() {
    sync.lock();
}

// 下面是sycn的lock方法
final void lock() { 
    /* 
    首先用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了独占锁
    这里进行一个说明,AQS底层定义了一个state变量,用来记录加锁的状态
    当state为0时表示没有加锁,为1时则表示加锁,大于1时则表示有锁重入
    */
    if (compareAndSetState(0, 1)) 
        // 加锁成功则将ownerThread设为当前线程,ownerThread是用来记录获得锁的线程
        setExclusiveOwnerThread(Thread.currentThread()); 
    else 
    // 如果尝试失败,则调用acquire方法默认传参为1
        acquire(1); 
 }
 
 // 继承自AQS的方法
 public final void acquire(int arg) { 
     // (2) tryAcquire :尝试再次获取锁,成功获取返回true,失败则返回false
     if ( !tryAcquire(arg) && 
         // 当 tryAcquire 返回为 false 时(获取锁失败), 先调用(4)addWaiter, 接着调用(5)acquireQueued              
         acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ) { 
             selfInterrupt(); 
     } 
 }
 
 // (2)
 protected final boolean tryAcquire(int acquires) { 
     // 此方法调用(3)
     return nonfairTryAcquire(acquires); 
 }
 
 // (3) Sync 继承过来的方法
 final boolean nonfairTryAcquire(int acquires) { 
     final Thread current = Thread.currentThread(); 
     int c = getState(); 
     // 如果还没有获得锁 
     if (c == 0) { 
           // 尝试用 cas 获得, 这里体现了非公平性: 不去检查 AQS 队列 
           if (compareAndSetState(0, acquires)) { 
               // 成功获得锁,ownerThread为当前线程
               setExclusiveOwnerThread(current); 
               return true; 
           } 
     } 
     // 如果已经获得了锁, 并且ownerThread为当前线程, 表示发生了锁重入 
     else if (current == getExclusiveOwnerThread()) { 
         // 将state加一 
         int nextc = c + acquires; 
         if (nextc < 0) 
             // overflow 
             throw new Error("Maximum lock count exceeded"); 
         // 给state重新赋值
         setState(nextc); 
         return true;
     } 
     // 获取失败, 回到调用处 
     return false;
 }
 
 // (4) AQS 继承过来的方法
 private Node addWaiter(Node mode) {
     // 将当前线程关联到一个 Node 对象上, 模式为独占模式 
     Node node = new Node(Thread.currentThread(), mode); 
     // 如果 tail 不为 null, cas 尝试将 Node 对象加入 AQS 队列尾部 
     Node pred = tail; 
     if (pred != null) { 
         node.prev = pred; 
         if (compareAndSetTail(pred, node)) { 
             // 双向链表 
             pred.next = node; 
             return node; 
         } 
     } 
     // 尝试将 Node 加入 AQS, 进入 (6) 
     enq(node); 
     return node; 
  }
  
  // (6) AQS 继承过来的方法, 方便阅读, 放在此处 
  private Node enq(final Node node) { 
      for (;;) { 
          Node t = tail; 
          if (t == null) { 
              /* 
              还没有, 设置 head 为哨兵节点(不对应线程,状态为 0)
              */
              if (compareAndSetHead(new Node())) { 
                  // tail设为哨兵节点,下一次循环不为null会执行else中的代码
                  tail = head; 
              } 
          } else {
              // cas 尝试将 Node 对象加入 AQS 队列尾部 
              node.prev = t; 
              if (compareAndSetTail(t, node)) { 
                  t.next = node; 
                  return t; 
              } 
          } 
      } 
   }
   
   
   // (5) AQS 继承过来的方法
   final boolean acquireQueued(final Node node, int arg) { 
       boolean failed = true; 
       try { 
           boolean interrupted = false; 
           for (;;) { 
               final Node p = node.predecessor(); 
               // 上一个节点是 head, 表示轮到自己(当前线程对应的 node)了, 再次尝试获取 
               if (p == head && tryAcquire(arg)) { 
                   // 获取成功, 设置自己(当前线程对应的 node)为 head 
                   setHead(node); 
                   // 上一个节点 help GC 
                   p.next = null; 
                   failed = false; 
                   // 返回中断标记 false 
                   return interrupted;
                } 
                if ( // 上个节点不是head,判断是否应当 park, 进入 (7) 
                    shouldParkAfterFailedAcquire(p, node) && 
                    // park 等待, 此时 Node 的状态被置为 Node.SIGNAL (8)             
                    parkAndCheckInterrupt() ) { 
                    interrupted = true; 
                } 
            } 
        } finally { 
            if (failed) 
                cancelAcquire(node); 
        } 
   }
   
   // (7) AQS 继承过来的方法, 方便阅读, 放在此处 
   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { 
       // 获取上一个节点的状态 
       int ws = pred.waitStatus; 
       if (ws == Node.SIGNAL) { 
           // 上一个节点都在阻塞, 那么自己也阻塞好了 
           return true; 
       } // > 0 表示取消状态 
       if (ws > 0) { 
           // 上一个节点取消, 那么重构删除前面所有取消的节点, 返回到外层循环重试 
           do { 
               node.prev = pred = pred.prev; 
           } while (pred.waitStatus > 0); 
           pred.next = node; 
       } else { 
           // 这次还没有阻塞 
           // 但下次如果重试不成功, 则需要阻塞,这时需要设置上一个节点状态为 Node.SIGNAL 
           compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 
       } 
       return false; 
  } 
  
  // (8) 加锁失败,阻塞当前线程 
  private final boolean parkAndCheckInterrupt() { 
      LockSupport.park(this); 
      return Thread.interrupted(); 
  }
加锁大概流程
  1. 首先ReentrantLock的lock方法底层调用非公平锁的lock方法,尝试使用cas方法将state从0改为1,如果成功则将ownerThread设置为当前线程。
  2. 如果失败,则执行acquire方法,acquire方法会调用非公平锁的nofairTryAcquire再次尝试加锁,如果state等于0,则说明还没有加锁,调用cas尝试将state从0改成1,如果成功则将ownerThread设置为当前线程。如果state不等于0,则说明已有其他线程加了锁,则判断加锁的线程是否为当前线程,如果是当前线程,则说明发生了锁重入,将state加1.
  3. 如果上面的nofairTryAcquire尝试加锁失败并且也没有发生锁重入,则返回false执行后续的addWaiter和acquireQueued方法。
  4. addWaiter方法将当前线程关联一个node节点,状态为0,放到队列尾部返回线程关联的节点。
  5. acquireQueue接受到addWaiter返回的节点后,会进入循环判断此节点的上一个节点是否为头节点,如果是头节点,则再次尝试获取锁,如果获取成功,则设置当前节点为头节点,释放之前的头节点,返回false。如果不是头节点,则执行shouldParkAfterFailedAcquire判断是否应该park当前线程,第一次执行会将当前节点的前一个节点state置为Nodel.SIGNAL(-1),不会park节点关联的线程,然后进入下一个循环,如果再次执行到此方法,则会park节点关联的线程,至此线程就进入等待队列。

解锁相关的方法:

// unlock方法也是调用的非公平锁的release方法
public void unlock() { sync.release(1); }

// AQS 继承过来的方法, 方便阅读, 放在此处 
public final boolean release(int arg) { 
    // 尝试释放锁, 进入 (1)
    if (tryRelease(arg)) { 
        // 队列头节点 unpark 
        Node h = head; 
        if ( // 队列不为null 
            h != null && 
            // waitStatus == Node.SIGNAL 才需要 unpark 
            h.waitStatus != 0 ) { 
                // unpark AQS 中等待的线程, 进入 (3)
                unparkSuccessor(h); 
        } 
            return true;
     } 
        return false; 
}

// (1) Sync 继承过来的方法,尝试释放锁
protected final boolean tryRelease(int releases) { 
    // state减1
    int c = getState() - releases; 
    // 如果当前线程不是获得锁的线程,则直接抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread()) 
            throw new IllegalMonitorStateException(); 
    boolean free = false; 
    // 支持锁重入, 只有 state 减为 0, 才释放成功 
    if (c == 0) { 
         // 释放锁后,将ownerThread置为null
         free = true; 
         setExclusiveOwnerThread(null); 
    } 
    setState(c); 
    return free; 
}

// (2) AQS 继承过来的方法, 唤醒等待队列中的后续线程
private void unparkSuccessor(Node node) { 
    // 如果状态为 Node.SIGNAL 尝试重置状态为 0 
    // 不成功也可以 
    int ws = node.waitStatus; 
    if (ws < 0) { 
        compareAndSetWaitStatus(node, ws, 0); 
    } 
    // 找到需要 unpark 的节点, 但本节点从 AQS 队列中脱离, 是由唤醒节点完成的
    Node s = node.next; 
    // 不考虑已取消的节点, 从 AQS 队列从后至前找到队列最前面需要 unpark 的节点 
    if (s == null || s.waitStatus > 0) { 
        s = null; 
        for (Node t = tail; t != null && t != node; t = t.prev) 
            if (t.waitStatus <= 0) 
                s = t; 
    } 
    // 唤醒该节点关联的线程
    if (s != null) 
        LockSupport.unpark(s.thread); 
}
解锁大概流程
  1. 首先unlock方法调用非同步锁的release方法,release方法先调用tryRelease方法尝试释放锁,先判断拿到锁的线程是否为当前线程,如果不是则直接抛出异常。如果是,则将state减1,如果state等于0,则将ownerThread置为null,返回true,否则则说明是锁重入,返回false。
  2. 如果tryRelease返回true,那么判断等待队列的头节点,如果部位null且waitStatus为Node.SIGNAL,则说明有后续的节点需要被唤醒,执行unparkSuccessor方法。
  3. unparkSuccessor方法如果传入的头节点状态为Node.SIGNAL,则将其置为0。拿到下一个节点,如果下一个节点的没有被取消(waitStatus<=0),则唤醒该节点关联的线程。如果下个节点被取消,则直接从尾节点往前遍历,拿到最前面的没有取消的节点,唤醒关联的线程。