读ReentrantLock源码笔记

1.lock实现

public void lock() {    sync.lock();}

我们都知道reentrantlock分为非公平锁和公平锁,通过new ReentrantLock(true);可以变成公平锁

我们这里分析一下jdk1.8时候的lock的实现

公平锁机制下lock调用的源码如下:

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

流程图: www.processon.com/view/link/5e3102efe4b05b335ff6b35d

public final void acquire(int arg) {
    // 尝试获得锁
    if (!tryAcquire(arg) &&
        // 获得锁失败则将当前线程变成Node节点add进等待队列
        // Node.EXCLUSIVE互斥模式、Node.SHARED共享模式
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这里先看一下tryAcquire的实现

protected final boolean tryAcquire(int acquires) {
    // 获得当前线程
    final Thread current = Thread.currentThread();
    // 取出锁状态
    int c = getState();
    // 为0代表还无线程占用
    if (c == 0) {
        // hasQueuedPredecessors判断等待队列是否初始化,简单来说就是判断当前线程是否是队列中的第一个线程
        if (!hasQueuedPredecessors() &&
            // CAS,将状态+1
            compareAndSetState(0, acquires)) {
            // 设置当前线程为拥有者线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 判断当前线程是否为重入
    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源码:

case 1:当前等待队列未初始化,如果为未初始化则head和tail都为null,那么h != t肯定不成立

case 2:当前等待队列中等于1,则也直接h != t不成立

case 3:当前等待队列中大于1,则&&后的判断,先判断是否还有下一个节点,然后判断当前线程是否和队列中第一个排队的节点的thread相等,不相等则代表有比当前线程更早的获取锁的线程在等待,因为公平锁需要先来后到的执行。返回true,不会进入if。

public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

我们在看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 中addWaiter的实现

addWaiter将自己入队,看源码实现:

private Node addWaiter(Node mode) {
    // 将当前线程以指定的模式创建节点node
    Node node = new Node(Thread.currentThread(), mode);
    // 获取当前等待队列的尾节点
    Node pred = tail;
    // 队列不为空,将新的node加入等待队列中
    if (pred != null) {
        node.prev = pred;
         // CAS方式将当前节点尾插入队列中
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 当队列为empty或者CAS失败时会调用enq方法处理
    enq(node);
    return node;
}

其中,队列为empty,使用enq(node)处理,将当前节点插入等待队列,如果队列为空,则初始化当前队列。所有操作都是CAS自旋的方式进行,直到成功加入队尾为止。
private Node enq(final Node node) {
    // 不断自旋
    for (;;) {
        Node t = tail;
        // 当前队列为empty
        if (t == null) {
            // 完成队列初始化操作,头结点中不放数据,只是作为起始标记,lazy-load,在第一次用的时候new
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            // 不断将当前节点使用CAS尾插入队列中直到成功为止
            // compareAndSetTail(t, node) 判断尾部节点是不是t,是的话就将尾部指向node
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

acquireQueued用于已在队列中的线程以独占且不间断模式获取state状态,直到获取锁后返回。主要流程:

  • 结点node进入队列尾部后,检查状态;
  • 调用park()进入waiting状态,等待unpark()或interrupt()唤醒;
  • 被唤醒后,是否获取到锁。如果获取到,head指向当前结点,并返回从入队到获取锁的整个过程中是否被中断过;如果没获取到,继续流程1

Lock类对于锁的实现不会令线程进入阻塞状态,Lock底层调用LockSupport.park()方法,使线程进入的是等待状态。

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))的实现:

final boolean acquireQueued(final Node node, int arg) {
  // 是否已获取锁的标志,默认为true 即为尚未
  boolean failed = true;
  try {
      // 等待中是否被中断过的标记
      boolean interrupted = false;
      for (;;) {
          // 获取当前节点的前节点
          final Node p = node.predecessor();
          // 如果前节点是头结点,则意味着自己是第一个排队的节点,尝试获取锁
          if (p == head && tryAcquire(arg)) {
              // 获得锁成功后将当前节点设置为头结点
              setHead(node);
              p.next = null; // help GC
              failed = false;
              return interrupted;
          }
          // shouldParkAfterFailedAcquire翻译过来“在获取锁失败之后应该等待”
          // shouldParkAfterFailedAcquire根据对当前节点的前一个节点的状态进行判断,对当前节点做出不同的操作
          // parkAndCheckInterrupt让线程进入等待状态,并检查当前线程是否被可以被中断
          if (shouldParkAfterFailedAcquire(p, node) &&
              parkAndCheckInterrupt())
              interrupted = true;
      }
  } finally {
      // 将当前节点设置为取消状态(Node.CANCELLED),为1
      if (failed)
          cancelAcquire(node);
  }
}


private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // waitStatus默认为0
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 使用CAS将前一个节点状态由 INITIAL 设置成 SIGNAL(这里修改之后会自旋再次尝试获得锁)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

shouldParkAfterFailedAcquire 方法

  • 主要逻辑是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)使用CAS将前一个节点状态由 INITIAL 设置成 SIGNAL,表示当前线程。

    为什么要设置前一个线程的状态为SIGNAL而不是自己呢?

    打个比方,你自己在房间睡着的时候知道自己睡着了吗?当然是不知道,那么你睡着了能自己关门嘛?不能,那你关了门万一没睡呢?所以就需要别人来帮你把门关上。

    你也可以理解为修改状态的操作和使线程睡眠的操作不具有原子性,可能出现修改完状态之后却没睡着的情况。

  • shouldParkAfterFailedAcquire方法会在死循环中反复重试,直至compareAndSetWaitStatus 设置节点状态位为 SIGNAL 时 shouldParkAfterFailedAcquire 返回 true 时才会执行方法 parkAndCheckInterrupt 方法。

parkAndCheckInterrupt 该方法的关键是会调用 LookSupport.park 方法,该方法是用来当前线程。

waitStatus的各个值都是什么意思:

静态变量 描述
Node.CANCELLED 1 节点对应的线程已经被取消了(我们后边详细会说线程如何被取消)
Node.SIGNAL -1 表示后边的节点对应的线程处于等待状态
Node.CONDITION -2 表示节点在等待队列中(稍后会详细说什么是等待队列)
Node.PROPAGATE -3 表示下一次共享式同步状态获取将被无条件的传播下去(稍后再说共享式同步状态的获取与释放时详细唠叨)
0 初始状态

2.interrupt(),interrupted() 和 isInterrupted() 的区别

interrupt():将调用该方法的对象所表示的线程标记一个停止标记,并不是真的停止该线程。

interrupted():获取当前线程的中断状态,并且会清除线程的状态标记。是一个是静态方法。

isInterrupted():获取调用该方法的对象所表示的线程的中断状态,不会清除线程的状态标记。是一个实例方法。

简单的中断案例:t1先执行,但是sleep不释放锁资源,在这期间t2等候两秒钟还没拿到锁就中断

public class Test {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            testsync();
        },"t1");
        Thread t2 = new Thread(() -> {
            testsync();
        },"t2");
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t2.start();
        TimeUnit.SECONDS.sleep(2);
        System.out.println("main");
        /**
         * 如果t2两秒钟还拿不到就中断
         */
        t2.interrupt();
    }
    public static void testsync(){
        try {
            /**
             * lockInterruptibly 和 lock的区别,前者会直接抛出异常可以响应中断,后者则不可以
             */
            lock.lockInterruptibly();
            System.out.println(Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

lock和lockInterruptibly的区别就在如下:

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);
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 区别在这
                throw new InterruptedException();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

acquireQueued() 基本相同,唯一的区别是对中断信号的处理。

acquireQueued() 被中断后,将中断标志传给外界,外界再调用Thread的interrupt() 复现中断;而doAcquireInterruptibly() 则直接抛出InterruptedException。

二者本质上没什么不同。但**doAcquireInterruptibly()**显示抛出了InterruptedException,调用者必须处理或继续上抛该异常。


3.unlock实现

这里我们看release的实现,sync是reentrantlock的一个内部抽象类,继承了AbstractQueuedSynchronizer

reentrantlock的公平锁FairSync和非公平锁NonfairSync都实现了sync

public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    // 尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 当waitStatus不为0时,它会唤醒等待队列里的其他线程来获取资源。
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    // 将状态值减1(因为是可重入锁,所以释放锁的次数和加锁的次数要一一对应)
    int c = getState() - releases;
    // 判断当前线程是否是持有锁的线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是否成功释放锁标志,默认为尚未
    boolean free = false;
    // 当状态值为0就会进行解锁,清空锁持有线程。
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 设置可重入次数为原始值0
    setState(c);
    return free;
}

我们继续看unparkSuccessor():

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    // 如果waitStatus为-1,即表示后边的节点对应的线程处于等待状态
    if (ws < 0)
        // 将waitStatus改为0
        compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        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);
}

这里当unpark唤醒下一个线程的时候,我们之前阻塞的线程就会在acquireQueued这个地方被唤醒,且继续执行

// parkAndCheckInterrupt 这里被唤醒后会获取当前线程的中断状态,并且会清除线程的状态标记。
// 如果没有被中断就if不成立,重新进行for循环
if (shouldParkAfterFailedAcquire(p, node) &&
    parkAndCheckInterrupt())      


for (;;) {
          // 获取当前节点的前节点
          final Node p = node.predecessor();
          // 如果前节点是头结点,则意味着自己是第一个排队的节点,尝试获取锁
          if (p == head && tryAcquire(arg)) {
              // 成功则将当前节点设置为头结点
              setHead(node);
              p.next = null; // help GC
              failed = false;
              // 最后返回false
              return interrupted;
          }
          if (shouldParkAfterFailedAcquire(p, node) &&
              parkAndCheckInterrupt())
              interrupted = true;
      }
发布了27 篇原创文章 · 获赞 32 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_39809458/article/details/104117830
今日推荐