• ReentrantLock原理


    AQS:AbstractQueuedSynchronizer,是一个依赖状态的同步器,定义了一套多线程访问共享资源的同步器框架。对于等待队列、条件队列、独占共享等行为进行一系列抽象。
    ReentrantLock是一种基于AQS框架的实现,作用类似于synchronized,是一种互斥锁,且它具有比synchronized更多的特性,支持手动加锁和解锁,支持公平锁和非公平锁。
    先看ReentrantLock的简单应用:
    // false为非公平锁,true为公平锁,默认是公平锁
    ReentrantLock lock = new ReentrantLock(true);
    // 加锁
    lock.lock() 
    // 具体的业务逻辑
    // 解锁
    lock.unlock() 

    我们进入到这个ReentrantLock这个类,ReentrantLock作为AQS的应用实现,通过内部类Sync继承AbstractQueuedSynchronizer,这个类尤其重要,是整个juc抽象的核心。

    // 构造器,默认创建的是公平锁
    public ReentrantLock(boolean fair) {
      sync = fair ? new FairSync() : new NonfairSync();
    }

    FairSync和NonfairSync都是Sync的子类,而Sync继承了AbstractQueuedSynchronizer,先简单来看AbstractQueuedSynchronizer中的重要属性:

    // 头节点
    private transient volatile Node head;
    
    // 尾节点
    private transient volatile Node tail;
    
    // 状态,AQS是基于状态的同步器,用的就是这个状态值
    private volatile int state;  
    
    // 同步队列使用双向链表来构建,使用内部类Node创建节点
    static final class Node {
      // 标记共享模式
      static final Node SHARED = new Node();
      // 标记独占模式 
      static final Node EXCLUSIVE = null;
      // 表示出现异常,可能是中断,也可能是执行出现错误,需要取消等待
      static final int CANCELLED =  1;
      // 表示可被唤醒
      static final int SIGNAL    = -1;
      // 节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中
      static final int CONDITION = -2;
      // 表示下一次共享式同步状态获取将会被无条件地传播下去
      static final int PROPAGATE = -3;
      // 节点状态
      volatile int waitStatus;
      // 前驱节点
      volatile Node prev;
      // 后继节点  
      volatile Node next;
      // 节点对应的线程
      volatile Thread thread;
      // 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量
      Node nextWaiter;
      // 空节点,用于标记共享模式
      Node() {
      }
      // 用于同步队列
      Node(Thread thread, Node mode) {
        this.nextWaiter = mode;
        this.thread = thread;
      }
      // 用于条件队列
      Node(Thread thread, int waitStatus) { 
        this.waitStatus = waitStatus;
        this.thread = thread;
      }
    }

    再回到ReentrantLock中,加锁的方法为Lock,调用的具体实现为FairSync的Lock():

    final void lock() {
      // 调用AQS中的acquire方法
      acquire(1);
    }
    
    // AQS中的acquire方法
    public final void acquire(int arg) {
      // 先尝试获得锁,如果不能成功,进行队列入队
      if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();
    }
    
    // tryAcquire调用的是子类具体实现:FairSync.tryAcquire()
    protected final boolean tryAcquire(int acquires) {
      // 获取当前线程
      final Thread current = Thread.currentThread();
      // 获取状态值
      int c = getState();
      // 如果为0,说明现在没有线程获得锁,尝试加锁
      if (c == 0) {
        // hasQueuedPrecessors是判断是否需要等待,如果不需要,对state进行cas
        if (!hasQueuedPredecessors() && 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");
          // 这里就是可重入的表现,不需要进行cas,因为当前线程已经获取过一次,不可能有其他线程拿到当前锁。多次加锁未来就会多次解锁。
          setState(nextc);
          return true;
      }
      // 未能成功加锁
      return false;
    }
    /**
     * 入队
     * @param mode  表示的是锁的模式:独占或者共享,用Node节点表示,在Node注释中有提到。
     */
    private Node addWaiter(Node mode) {
      // 构造一个节点,构造参数中传入当前线程和锁模式,
      Node node = new Node(Thread.currentThread(), mode);
      // 把尾节点赋值给pred,当然,此时的tail有可能是为空的,因为之前有可能还没有初始化等待队列
      Node pred = tail;
      if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
          pred.next = node;
          return node;
        }
      }
      enq(node);
      return node;
    }
    // 节点入队
    private Node enq(final Node node) {
      // 进入自旋,目的是保证入队一定能成功。因为此时可能有多个节点来入队,一个节点一次入队不一定成功。
      for (;;) {
        Node t = tail;
        if (t == null) {
          // 在AQS的设计思想中,构建队列之前一定保证了队列初始化过。头部放一个空节点,内部的线程属性不指向任何线程。队头和队尾同时指定这个空节点,这样队列初始化完成。
          if (compareAndSetHead(new Node()))
            tail = head;
        } else {
          node.prev = t;
          // 将当前节点入队,当前节点就变成了队列的尾节点。此处也存在竞争,为了保证所有阻塞线程对象能被唤醒,所以要保证安全入队。
          if (compareAndSetTail(t, node)) {
            // 之前的尾部指针的next指向新节点
            t.next = node;
            return t;
          }
        }
      }
    }
    // 节点入队之后的处理。主要进行节点阻塞
    final boolean acquireQueued(final Node node, int arg) {
      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;
          }
          // 尝试获取锁失败,判断是否需要阻塞
          // 在这个方法中会调用LockSupport.park(this),进行线程阻塞
          if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
            interrupted = true;
        }
      } finally {
        if (failed)
          cancelAcquire(node);
      }
    }
    // 获取锁失败,是否需要阻塞
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
       // 前驱节点的状态
      int ws = pred.waitStatus;
      // 前驱节点的状态是SIGNAL,表示当前节点是可被唤醒的。这个地方的设计比较奇特,使用前驱节点的状态来标记唤醒当前节点。A->B->C,A节点的状态表示B能否被唤醒
      if (ws == Node.SIGNAL)
        return true;
      // 前驱节点的状态大于0,我们认为出现错误,将当前节点的前驱节点,和前驱节点都进行前移,直到前驱节点的状态正常
      if (ws > 0) {
        do {
          node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
      } else {
        // 将前驱节点的状态修改为SIGNAL,下次可唤醒
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
      }
      return false;
    }

    到此,加锁的代码处理完毕。未获得锁的线程是阻塞的,如何被唤醒?当然是在unlock方法,当前获取锁的线程释放锁之后,就会去唤醒下一个线程,unlock代码跟下去:

    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 final boolean tryRelease(int releases) {   // 状态减   int c = getState() - releases;   if (Thread.currentThread() != getExclusiveOwnerThread())     throw new IllegalMonitorStateException();   boolean free = false;   if (c == 0) {     free = true;     // 把当前AQS独占的线程置空     setExclusiveOwnerThread(null);   }   setState(c);   return free; } // 线程唤醒 private void unparkSuccessor(Node node) {   int ws = node.waitStatus;   if (ws < 0)     // 再次将节点状态改为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); }

    因为Doug Lea将太多的逻辑集成到AQS中,这个代码深入到每一行几乎时读不下去的,重点是学习他的设计思路,多线程之间互相阻塞和唤醒。 

     
     
     

  • 相关阅读:
    Delphi中SQL语句配置参数代码示例
    Delphi中treeview的使用部分
    Delphi listview使用部分总结代码
    如何用Delphi编写自己的可视化控件
    关于treeview节点图标的帖子
    Delphi调用存储过程
    DELPHI的开源控件集(转自http://xieyunc.blog.163.com/)
    雨巷(A Lane in the Rain)
    五一过去了,新的开始
    好好的学习,做个有本事的人:),好好的玩,做个快乐的人!
  • 原文地址:https://www.cnblogs.com/dlcode/p/14026603.html
Copyright © 2020-2023  润新知