Java并发 ReentrantLock和AQS

LockSupport

  1. LockSupport是什么?

    LockSupport是一种线程等待唤醒机制,用来创建锁和其他同步类的基本线程阻塞原语。LockSupport中的park()和 unpark()的作用分别是阻塞线程和解除阻塞线程。

  2. 让线程等待和唤醒的三种方式?

    • 方式1:使用object中的wait()方法让线程等待,使用object中的notify()方法唤醒线程
    • 方式2:使用uc包中condition的await()方法让线程等待,使用signal()方法唤醒线程
    • 方式3: LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
      在这里插入图片描述3. wait,notify机制例子
      代码:
      public class Demo01 {
              
              
          private static Object objectLock = new Object();
          public static void main(String[] args) {
              
              
              new Thread(() -> {
              
              
                  synchronized (objectLock) {
              
              
                      System.out.println(Thread.currentThread().getName() + "---come in----");
                      try {
              
              
                          objectLock.wait();
                      } catch (Exception e) {
              
              
                          e.printStackTrace();
                      }
                      System.out.println(Thread.currentThread().getName() + "---被唤醒---");
                  }
              }, "t1").start();
      
              new Thread(() -> {
              
              
                  synchronized (objectLock) {
              
              
                      objectLock.notify();
                      System.out.println(Thread.currentThread().getName() + "---com in---");
      
                  }
              }, "t2").start();
          }
      }
      
      输出结果:
      在这里插入图片描述
      那如果把代码改成下面的样子呢?
      public class Demo01 {
              
              
          private static Object objectLock = new Object();
          public static void main(String[] args) {
              
              
              new Thread(() -> {
              
              
      //            synchronized (objectLock) {
              
              
                      System.out.println(Thread.currentThread().getName() + "---come in----");
                      try {
              
              
                          objectLock.wait();
                      } catch (Exception e) {
              
              
                          e.printStackTrace();
                      }
                      System.out.println(Thread.currentThread().getName() + "---被唤醒---");
      //            }
              }, "t1").start();
      
              new Thread(() -> {
              
              
      //            synchronized (objectLock) {
              
              
                      objectLock.notify();
                      System.out.println(Thread.currentThread().getName() + "---com in---");
      
      //            }
              }, "t2").start();
          }
      }
      
      输出结果:
      在这里插入图片描述
      两个线程都去掉代码块会报错,也就是说,wait,notify机制是不能脱离synchronized的!!
      我们也可以尝试将notify放在wait前面将导致线程永远不会唤醒!!!所以要先wait后notify!!!
    1. Condition接口中的await后signal方法实现线程的等待和唤醒
      代码:

      public class Demo02 {
              
              
          private static Lock lock = new ReentrantLock();
          static Condition condition = lock.newCondition();
      
          public static void main(String[] args) {
              
              
              new Thread(() -> {
              
              
                  lock.lock();
                  try {
              
              
                      System.out.println(Thread.currentThread().getName() + " com in");
                      try {
              
              
                          condition.await();
                      } catch (InterruptedException e) {
              
              
                          e.printStackTrace();
                      }
                      System.out.println(Thread.currentThread().getName() + " 被唤醒");
                  } finally {
              
              
                      lock.unlock();
                  }
              }, "t1").start();
      
              new Thread(() -> {
              
              
                  lock.lock();
                  try {
              
              
                      condition.signal();
                      System.out.println(Thread.currentThread().getName() + " 通知");
                  } catch (Exception e) {
              
              
                      e.printStackTrace();
                  } finally {
              
              
                      lock.unlock();
                  }
              }, "t2").start();
          }
      }
      

      输出结果:
      在这里插入图片描述
      将两个线程的lock与unlock去掉,同样会报错:
      在这里插入图片描述

    2. LockSupport案例
      LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零,默认是零。可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
      代码:

      public class Demo03 {
              
              
          public static void main(String[] args) {
              
              
              Thread a = new Thread(() -> {
              
              
                  System.out.println(Thread.currentThread().getName() + "\t come in");
                  LockSupport.park();
                  System.out.println(Thread.currentThread().getName() + "\t 被唤醒");
              });
              a.start();
      
              try {
              
              
                  TimeUnit.SECONDS.sleep(3L);
              } catch (Exception e) {
              
              
                  e.printStackTrace();
              }
      
              Thread b = new Thread(() -> {
              
              
                 LockSupport.unpark(a);
                  System.out.println(Thread.currentThread().getName() + "\t 通知了");
              });
              b.start();
          }
      }
      
      

      输出结果:
      在这里插入图片描述

      那如果先unpark后park呢?输出结果:
      在这里插入图片描述
      LockSupport的重要说明:

      1. LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
        LockSupport是一个线程阻寨工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
      2. LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
        LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成o,同时park立即返回。如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。
      3. 形象的理解
        线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park方法时,*如果有凭证,则会直接消耗掉这个凭证然后正常退出;*如果无凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
    3. LockSupport的面试题

      • 为什么可以先唤醒线程后阻塞线程?
        因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。
      • 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
        因为凭证的数量最多为1,连续调用两次unpark和调用一次 unpark 效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。

AQS

AQS是什么

是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。抢到资源的线程直接使用处理业务逻辑,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。

在这里插入图片描述

AQS在其他同步器组件中的应用

在这里插入图片描述进一步理解锁和同步器的关系:

  • 锁,面向锁的使用者
    定义了程序员和锁交互的使用层APl,隐藏了实现细节,你调用即可。
  • 同步器,面向锁的实现者
    比如Java并发大神DougLee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等

而AQS则属于后者。

AQS内部体系结构

首先看一看AQS的类图(此类图只显示出相关的类和各类的变量,主要介绍红框里的变量):
在这里插入图片描述
AbstractQueuedSynchronizer类:

  • head:双端队列的头指针
  • tail:双端队列的尾指针
  • state: 当前锁的持有状态(0表示空闲,1表示被占有)

注意:state是用voliate修饰的变量,目的是为了保证内存可见性,使得state变量被修改后,其他线程能够立即感知到。

Node类:
Node类是AbstractQueuedSynchronizer的一个内部类,如果有多个线程竞争锁,其中的一个线程获取到了锁,那么其他的线程会被封装成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唤醒
    static final int CONDITION = -2;
	// 共享式同步状态获取将会无条件的传播下去
    static final int PROPAGATE = -3;
	// 初始为0,状态是上面的几种
    volatile int waitStatus;
	// 前置结点
    volatile Node prev;
	// 后继结点
 	volatile Node next;

	volatile Thread thread;
}
	  

AQS源码

以ReentrantLock为例,深入理解AQS源码。

在这里插入图片描述从类的关系图中可以看到:

  • ReentrantLock实现了Lock接口
  • ReentrantLock内有NonfairLock、FairSync和Sync内部类,且NonfairLock、FairSync又继承了Sync类
  • Sync类继承了AbstractQueuedSynchronizer抽象类
  • AbstractQueuedSynchronizer内有Node和ConditionObject两个内部类

以下述代码为入口一步一步的进入源码,看一看AbstractQueuedSynchronizer内部是如何运行的:

public class Demo02 {
    
    
    public static void main(String[] args) {
    
    
        ReentrantLock lock = new ReentrantLock();

        new Thread(() -> {
    
    
            lock.lock();
            try {
    
    
                System.out.println("A获取了锁");
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }, "A").start();

        new Thread(() -> {
    
    
            lock.lock();
            try {
    
    
                System.out.println("B获取了锁");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }, "B").start();

        new Thread(() -> {
    
    
            lock.lock();
            try {
    
    
                System.out.println("C获取了锁");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }, "C").start();
    }
}

代码执行流程分析

ReentrantLock无参构造函数

public ReentrantLock() {
    
    
    sync = new NonfairSync();
}

ReentrantLock默认是非公平锁,以lock.lock();A线程获取锁为入口,看一下执行大致的流程图:
在这里插入图片描述

A线程获得锁

A线程获得锁,代码的执行流程为 1 =》 2,对应的方法为lock() => compareAndSetState()
可以从图中看到,A获得锁的时候由于没有其他线程竞争,所以A线程通过自旋的方式将state设置为1,自己为锁的持有者

B线程竞争锁(A线程未释放锁)

A线程获得锁后,由于执行任务需要占用锁2s,此时B线程过来竞争锁,对应的执行流程为1 =》 2 =》 3 =》 4 =》 5 =》 6 =》7 =》 8 =》 9 =》 10,对应的方法为 lock() =》 compareAndSetState() =》 acquire() =》 tryAcquire() =》 addWaiter() =》 nonfairTryAcquire() =》 addWaiter() =》 enq() =》 acquireQueued() =》 tryAcquire() =》setHead()

执行lock()方法尝试获取锁,要先调用compareAndSetState()将state置为1,但是由于此时线程A已获得锁且state变量的值为1,所以自旋失败,执行acquire()方法尝试以非公平方式去获取锁(也就是调用nonfairTryAcquire()方法)。

以注释的形式描述nonfairTryAcquire()内部具体做了些什么:

final boolean nonfairTryAcquire(int acquires) {
    
    
	// 获取当前请求锁的线程
    final Thread current = Thread.currentThread();
    // 获取锁的状态
    int c = getState();
    // 如果锁处于空闲状态
    if (c == 0) {
    
    
    	// 自旋尝试设置state=1
        if (compareAndSetState(0, acquires)) {
    
    
        	// 自旋成功,设置当前线程未持有锁的线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 判断当前线程和已持有锁的线程是否为同一个线程
    else if (current == getExclusiveOwnerThread()) {
    
    
    	// 如果是同一个线程,则state = state + 1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 重新设置state的值
        setState(nextc);
        return true;
    }
    return false;
}

由于线程A已占有锁,所以nonfairTryAcquire()方法执行结果为false,所以tryAcquire()的结果为false,接着执行 addWaiter()方法将B线程封装进一个Node对象并添加进双端队列中。我们可以看一看addWaiter()内部做了什么(addWaiter(Node.EXCLUSIVE)一个空结点 )。

addWaiter()源码:

private Node addWaiter(Node mode) {
    
    
	// 创建一个新Node对象,
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // 定义一个指针指向尾节点
    Node pred = tail;
    if (pred != null) {
    
    
    	// 尾节点不是null,自旋设置新的结点为尾节点
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
    
    
            pred.next = node;
            return node;
        }
    }
    // 入队
    enq(node);
    return node;
}

enq(node)源码:

private Node enq(final Node node) {
    
    
	// 死循环
    for (;;) {
    
    
    	// 获取尾节点
        Node t = tail;
        if (t == null) {
    
     // Must initialize
        	// 尾节点为NULL,自旋设置一个新的结点为头结点
            if (compareAndSetHead(new Node()))
            	// 尾节点指向头结点
                tail = head;
        } else {
    
    
        	// 尾节点补位null,则将尾节点设置为新结点的前置结点,
            node.prev = t;
            if (compareAndSetTail(t, node)) {
    
    
            	// 新的结点成为尾节点
                t.next = node;
                return t;
            }
        }
    }
}

在addWaiter(Node.EXCLUSIVE)方法中由于tail为null,所以直接调用了enq(Node)方法,在enq()方法内部是一个死循环,第一次循环时由于尾节点为空,所以就新建一个结点使得head、tail指针都指向这个结点(这个结点被成为哨兵结点),执行完第一次循环队列图如下:
在这里插入图片描述

第一次循环结束后,多了个结点,但是此时此结点并不是我们传入的结点,内部的值是因为Node初始化(new Node())而得到的(Thread=null,waitStatus=0)。在第二次循环后,由于tail不为空,所以第二次循环后队列图为:

在这里插入图片描述

此时addWaiter(Node.EXCLUSIVE)方法执行完后,B线程已被封装进Node结点内放进双端队列中,接下来会调用acquireQueued()去竞争锁,进到源码里看一看acquireQueued()是如何工作的。
acquireQueued():

final boolean acquireQueued(final Node node, int arg) {
    
    
    boolean failed = true;
    try {
    
    
        boolean interrupted = false;
        // 死循环,直至获取到锁
        for (;;) {
    
    
        	// 获取node的前驱结点
            final Node p = node.predecessor();
            // 如果前驱结点是头结点且获取锁成功
            if (p == head && tryAcquire(arg)) {
    
    
            	// 一下代码是将获取锁后的此结点从队列中删除
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 如果前驱结点不是头结点或者获取锁失败,则挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
    
    
        if (failed)
            cancelAcquire(node);
    }
}

从代码中我们可以知道Thread前驱结点为头结点,当tryAcquire(arg)返回为false时(获取锁失败)则再次尝试,在第一次获取失败后,代码则会执行到shouldParkAfterFailedAcquire()方法。

shouldParkAfterFailedAcquire()源码:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    
    
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
    
    
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
    
    
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
    
    
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

shouldParkAfterFailedAcquire()执行完后,队列变成:
在这里插入图片描述在第二次循环后,由于Thread又为获得锁,所以又执行到shouldParkAfterFailedAcquire()方法,而此方法由于第一次执行waitStatus=1会返回true,接下来会执行parkAndCheckInterrupt()

parkAndCheckInterrupt()源码:

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

可以看到ThreadB将会被挂起,等待唤醒。

在ThreadA执行完毕后,调用unlock()释放锁后,ThreadB被唤醒尝试获取锁,获取锁成功后则将此结点从队列中删除,注意:并不是直接将ThreadB结点删除,而是将ThreadB结点置为新的哨兵结点
setHead()的源码:

private void setHead(Node node) {
    
    
    head = node;
    node.thread = null;
    node.prev = null;
}

重置头结点后队列图:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_44533129/article/details/113475189