04. 并发编程 - 同步器AQS

520 阅读13分钟

并发编程——同步器AQS

0.1. 内容概要

1. AQS的概念和原理

2. AQS的常用方法

3. AQS的数据结构

4. Lock框架再认识

0.2. 学习目标

  • 理解AQS的概念和原理
  • 能够描述出AQS的工作流程和工作原理
  • 能够自定义同步器并正确使用
  • 能够描述出常用同步器的底层实现及彼此的区别
  • 理解AQS的数据结构
  • 深入理解Lock框架,能够熟练使用Lock框架的常用类

1. AQS的概念和原理

1.0. Java并发核心组件

00.Java并发核心组件.png

1.1. AQS的概念

AQS(AbstractQueuedSynchronizer),字面意思:抽象队列同步器。

  • abstract:抽象的,要使用这个类,需要创建它的子类对象;
  • queued:队列,说明该类是基于队列这样的数据结构来实现的;
  • synchronizer:同步器,即用于实现线程同步的对象。

AQS是Java并发包中的一个框架,用于实现阻塞锁和与之相关的依赖于先进先出(FIFO,First Input First Output)等待队列的同步器(比如Semaphore、CountDownLatch)。说白了,就是Java中除了synchronized关键字之外另一种实现锁的方式。JSR166基于AQS类建立了一个框架,这个框架为构造同步器提供一种通用的实现机制,并且被java.util.concurrent包中大部分类使用,同时,我们也可以用它来定义自己的同步器,这就是作者Doug Lea大神的初衷。我们会对比两种锁机制的实现方式来学习AQS,同时也让你加深对synchronized关键字以及锁机制的认识。

1.2. AQS的工作原理

在JDK5之前,我们使用synchronized关键字实现同步,由于在语言层面提供了内部的支持,我们通常将这种锁称为内部对象锁。在JDK5之后,我们有了新的选择:同步器AQS。要注意的是,AQS并不是作为内部对象锁的替代,而是当内部锁被证明受到局限时,提供可选择的高级特性。

此类默认支持两种模式:

  • 排他模式(Exclusive mode):也叫独占模式,相当于互斥锁。当一个线程以独占模式成功获取锁,其它线程获取锁的尝试都将失败,就像synchronized那样。

  • 共享模式(Shared mode):多个线程可同时获取锁,用于控制一定量的线程并发执行。设计者建议共享模式下的同步状态支持0,小于0和大于0三种情况,以便在某种情况下和独占模式兼容。在此模式下,同步状态大于、等于0都代表获取锁成功。

接下来,我们回顾并对比synchronized关键字实现同步的流程,来分析一下AQS的工作原理、数据结构和它的设计模式,然后尝试自己定义一个同步器。

1.2.1. 对象锁的工作流程

先花一点时间来回顾下synchronized关键字实现同步的工作流程。

前面提到过,使用synchronized关键字实现同步,我们使用对象(类锁也是对象锁)作为监视器,而锁的本质是内存中对象头部的一部分数据。线程能否通过锁对象访问监视区域,取决于这部分数据所代表的锁的状态,当状态发生改变,别的线程无法通过锁,直到锁被完全释放。同样的,使用AQS来实现锁的思路也大体相同,区别在于,synchronized关键字是一种Java原生语法,而AQS只是普通的Java类。

锁机制的核心操作是:获取锁、释放锁。来看下面这段代码:

public void method() {
    synchronized (this) {
        System.out.println("synchronized block");
    }
}

通过javap命令反编译之后,得到的结果是是这样的:

public void method();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter						// 进入监视器
       4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #3                  // String synchronized block
       9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: aload_1
      13: monitorexit						// 退出监视器
      14: goto          22
      17: astore_2
      18: aload_1
      19: monitorexit						// 程序发生异常,确保退出监视器
      20: aload_2
      21: athrow
      22: return
    Exception table:
       from    to  target type
           4    14    17   any
          17    20    17   any

请重点关注序号为3、13和19的三行指令。synchronized关键字基于monitorentermonitorexit两个指令来实现锁的获取和释放过程。还记得下面这张图吗:

01.监视器.png

  1. 进入并持有监视器:线程到达监视区域,并进入监视器处于入口区。它会尝试获取锁,由于现在没有其它线程持有监视器,所以该线程会立即持有监视器,并执行监视区域中的代码。线程进入监视区域,执行的就是JVM的monitorenter指令。
  2. 其它线程阻塞:新的线程到达监视区域后,也会尝试获取监视器,但它此时必须在入口区等待,因为第一个线程还在执行监视区域的代码。后续的线程也会在入口区等待(准确地说是阻塞状态),直到该监视器当前的持有者(第一个线程)完全释放。
  3. 关于等待区和退出:监视器的持有者,即活动线程会通过两种途径释放监视器:完成监视区域的代码或者执行一个等待命令。如果它执行完监视区域,会从最右边的出口处释放并退出监视器,进入监视器的JVM指令是 monitorenter,退出的指令是 monitorexit(第二次退出的含义:在程序发生异常时也要确保退出监视器)。如果活动线程执行了等待(wait)命令,它就会释放监视器并进入等待区(wait set),注意,此时它并没有退出监视区域。
  4. 条件对象:synchronized关键字有个内置的条件对象,它是有JVM原生支持的,我们不需要/也不能对它进行操作,但是你要知道,它确实是存在的。
  5. 同步状态与可重入性:对象锁是可重入的,持有该监视器的线程每进入一次由该监视器锁保护的同步块/方法,锁的状态(计数器)就会+1,同样的,每退出一次,锁的状态就会-1,直到锁状态变成默认状态0,锁被完全释放,该线程就退出了监视器。

使用synchronized关键字实现同步,Java程序员不需要自己手动加锁、释放锁,也不需要关心同步状态被加/减了多少次,因为对象锁是在JVM内部使用的,这一切操作都在JVM内部完成——被JVM指令集支持。我们只需要编写同步语句或者同步方法来保护一个代码片段,程序运行时,JVM每一次进入监视区域都会自动锁上对象或者类。

1.2.2. AQS的工作流程

同步器AQS的工作流程与内部对象锁大致相同,它的核心操作也是获取锁、释放锁,但是现在,需要我们自己操作线程的同步状态了。为了实现与synchronized关键字同样的功能,需要考虑三个基本组件的相互协作:

  1. 同步状态的原子性管理;
  2. 锁的获取与释放(线程的阻塞与激活);
  3. 入口区和等待区线程的管理——队列;

接下来梳理一下AQS类的结构,摸清楚这些组件在独占模式和共享模式下分别是怎么实现的。

  1. 同步状态(state属性):

    /**
     * The synchronization state.
     */
    private volatile int state;
    
    protected final int getState() {
        return state;
    }
    
    protected final void setState(int newState) {
        state = newState;
    }
    
    protected final boolean compareAndSetState(int expect, int update) {
        return STATE.compareAndSet(this, expect, update);
    }
    

    在AQS类中定义了 private volatile 修饰的int型属性state,代表同步状态,加锁与释放锁的操作,本质上是操作state的结果,这与内部对象锁的实现如出一辙。为了保证同步,对state的操作必须是原子的,而且该属性不能被继承,所有同步机制的实现均依赖于对该变量的原子操作。volatile和CAS操作确保了同步状态的原子性管理

  2. 获取锁(acquire方法):

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    /**
     * 尝试获取独占锁,需要子类重写
     */
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    /**
     * 获取独占锁,支持响应线程中断
     */
    public final void acquireInterruptibly(int arg)
        throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
    /**
     * 执行获取独占锁之后的工作
     * 1.将线程添加到同步队列,并标记为独占模式
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(mode);
    
        for (;;) {
            Node oldTail = tail;
            if (oldTail != null) {
                node.setPrevRelaxed(oldTail);
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
                initializeSyncQueue();
            }
        }
    }
    /**
     * 执行获取独占锁之后的工作
     * 2.阻塞添加到队列中的线程,并在该线程被激活时尝试获取锁
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean interrupted = false;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node))
                    interrupted |= parkAndCheckInterrupt();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            if (interrupted)
                selfInterrupt();
            throw t;
        }
    }
    

    acquire方法用于以独占模式获取锁,如果获取失败,则将此线程添加到队列(如果不存在则创建)并阻塞此线程——底层调用工具类java.util.concurrent.locks.LockSupport 的静态方法 park()来实现。 此方法是独占模式下获取锁(state)的顶层入口,它的调用流程如下:

    1. tryAcquire:尝试获取共享资源(state),获取成功后,线程就会进入临界区执行相关的代码并导致acquire方法直接返回。该方法的默认实现只是单纯的抛出UnsupportedOperationException 异常,设计者这样做的目的,就是要让我们自己去重写该方法,并定义获取共享资源的具体算法。
    2. addWaiter:尝试获取共享资源失败,才会执行这个方法。它的作用是将此线程加入同步队列(尾部),并标记为“独占模式”。
    3. acquireQueued:线程进入等待队列之后,会执行此方法。该方法的主要作用就是调用 LockSupport.park()方法阻塞当前线程。为了提高效率,线程进入阻塞队列之后,会检查前一个节点是否为头节点,若是,则再进行一次尝试获取锁,一旦成功则直接返回。当阻塞的线程被激活(unpark),会再次尝试获取资源,获取成功则返回,如果失败,则以自旋的方式执行上述操作,直到成功后返回。在此过程中如果发生异常,则取消获取共享资源的动作——这与内置锁是不同的;如果该线程接收到中断请求,并不会立即响应,而是在获取锁成功之后,将中断状态返回,再由acquire方法响应中断。如果想要在线程获取锁的过程中支持响应中断,可以调用acquire的兄弟方法:acquireInterruptibly,这是与内置锁另一个不同之处。
  3. 释放锁(release方法):

    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();
    }
    

    release方法用于释放独占模式 的同步锁,底层调用工具类java.util.concurrent.locks.LockSupport 的静态方法 unpark()来实现,如果释放失败,则直接返回。此方法是独占模式下释放共享资源的顶层入口。类似的,此方法中调用了tryRelease并根据其返回值来判断是否完成了释放锁的操作——一个默认实现也是抛出 UnsupportedOperationException 异常的方法,目的也是为了让我们自己去重写。

    在独占模式下,线程释放资源之前,必定已经预先拿到了资源,所以重写的方法中只需要减掉相应的资源量即可,不需要过多考虑线程安全问题。当前线程释放完成后,会通过 unparkSuccessor方法激活队列中等待的下一个线程。一般被激活的线程就是当前线程的“next”节点,但如果该线程由于超时或者被中断而已经被激活,则(从队列尾部倒序)查找距离当前线程最近的可激活线程,用 LockSupport.unpark()方法激活它。

    private void unparkSuccessor(Node node) {
      
        int ws = node.waitStatus;
        if (ws < 0)
            node.compareAndSetWaitStatus(ws, 0);
    
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }
    
  4. 共享模式获取锁、释放锁的顶层方法命名规则和实现逻辑与独占模式几乎相同,请看下面的方法列表:

    /**
     * 获取共享锁,共享模式获取锁的顶层入口
     */
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    /**
     * 获取共享锁,支持响应中断
     */
    public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
    /**
     * 释放共享锁,共享模式释放锁的顶层入口
     */
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    
    /**
     * 尝试获取共享锁,需要子类重写
     */
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    /**
     * 尝试释放共享锁,需要子类重写
     */
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    /**
     * 执行获取共享锁之后的工作
     * 1.将线程添加到同步队列,并标记为共享模式
     * 2.阻塞此线程,并在它被激活时尝试获取锁
     */
    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean interrupted = false;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node))
                    interrupted |= parkAndCheckInterrupt();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        } finally {
            if (interrupted)
                selfInterrupt();
        }
    }
    

    相对于独占模式,共享模式获取锁、释放锁的基本流程没有任何变化。对锁的获取操作都应该被子类重写:独占模式:tryAcquire方法,共享模式:tryAcquireShared方法;同样的,释放锁的具体操作也是在子类中重新定义:独占模式:tryRelease方法,共享模式:tryReleaseShared方法。

    不同的地方是,获取锁操作,在独占模式下,队列中的线程获取锁成功之后,将当前节点设置为头结点,仅仅是将节点对象关联的前后节点重新赋值:

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

    而共享模式下,第一个线程获取锁后,如果共享资源仍然可以使用(state >= 0),则会唤醒当前节点的后续节点——多个线程是可以同时获取锁。所以,队列中的线程重新被激活之后,除了设置当前节点为头节点,还 需要/可能需要 进行后续节点的唤醒:

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
    
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared(); // 继续唤醒后续节点
        }
    }
    

    释放锁操作,共享模式下,释放掉共享资源(state)之后,唤醒后续节点。独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程(可重入性);而共享模式下的releaseShared 则没有这种要求,共享模式的实质是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点:

     private void doReleaseShared() {
         for (;;) {
             Node h = head;
             if (h != null && h != tail) {
                 int ws = h.waitStatus;
                 if (ws == Node.SIGNAL) {
                     if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                         continue;            // loop to recheck cases
                     unparkSuccessor(h);		
                 }
                 else if (ws == 0 &&
                          !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                     continue;                // loop on failed CAS
             }
             if (h == head)                   // loop if head changed
                 break;
         }
     }
    
  5. 关于等待队列

    当线程进入临界区,发现必须满足某个/些 条件才能继续,则该线程将在该条件对象上等待,并进入等待区(wait set)。为什么需要Condition对象呢?这是因为在多线程并发的环境中,我们能确定哪个线程先执行,哪个后执行吗?不能。通过设置Condition对象让进入临界区却不满足条件的线程等待,并在条件满足时继续执行,这样可以确保程序以设计的顺序执行。这就是条件对象的本质。一个锁可以管理多个条件对象,一个条件对象上可能会有多个线程处于等待状态。

    AQS类中定义了一个ConditionObject类,它实现了 java.util.concurrent.locks.Condition 接口,并提供如await、signal和signalAll操作,还扩展了带有超时、检测和监控的方法。ConditionObject类有效地将条件与其它同步操作结合到了一起。这里要注意,当且仅当一个线程持有锁且要操作的条件对象属于该锁时,条件操作才是合法的。这样,一个条件对象(ConditionObject)关联到一个锁对象(同步器的实例)上就表现出跟synchronized对象锁一样的行为了。

  6. AQS的工作流程

02.AQS的工作流程.png

2. AQS的数据结构

AQS使用队列来管理入口区和等待区的线程:

入口区:同步队列

等待区:等待队列

两个队列的本质相同,它们都是先进先出的链表结构,而且使用的是同一个内部类Node的实例最为节点。不同的是,同步队列是一个双向队列,而等待队列是一个单向队列。

2.1. AQS的节点和队列

2.1.1. AQS的节点

AQS使用Node内部类作为节点,封装线程及相关同步信息,从而构成一个链式结构的队列。

同步队列使用:prev、next两个指针分别指向前一个、后一个节点,使用waitStatus表示节点的同步状态,同时封装了当前thread对象;

Node节点:同步队列.png

  • volatile Tread thread 线程对象

  • volatile Node next 下一个节点

  • volatile Node prev 前一个节点

等待队列使用:nextWaiter一个指针指向后一个节点,使用waitStatus表示节点的同步状态,同时封装了当前thread对象;

Node节点:等待队列.png

  • volatile Node nextWaiter 等待队列中的下一个节点

  • volatile int waitStatus **:**节点等待状态,默认0。当前节点的值表示后续节点的状态。

    CANCELLED = 1 取消状态,唯一大于0的值,当线程等待超时、发生异常或中断,节点会变成取消状态

    SIGNAL = -1 一般正常的节点状态,代表后一个节点需要被唤醒

    CONDITION = -2 在条件对象上等待的节点处于此状态,与SIGNAL唯一的区别就是数值不同,从而表示处于同步队列还是条件队列

    PROPAGATE = -3 传播,共享模式的节点处于此状态,表示当前节点的行为会向后续节点传播,即后续节点与当前节点行为相同。事实上,很少/很短时间有节点会处于此状态。

2.1.2. AQS的同步队列

获取锁失败的线程,被封装成Node节点加入同步队列的尾部。同步队列有个头指示器(head)和尾指示器(tail)分别指向首节点和尾节点。值得注意的是,头节点的状态为-1,即代表后续节点需要被唤醒。头结点(head)中并未封装线程实例,而且,前一个节点的状态其实代表的时候后一个节点的行为,而最后一个节点的状态,是默认值0

AQS的同步队列.png

2.1.2. AQS的等待(条件)队列

获取锁成功后,线程进入监视区域,但是如果线程不满足某些条件,就不得不再次进行等待。不满足条件对象的线程会被封装成节点,进入等待状态并加入等待队列的尾部。等待队列有个首节点指示器firstWaiter和尾节点指示器lastWaiter,与同步队列不同的是,firstWaiter指向的首节点是封装了线程实例的,同步队列的首节点是一个标志位,是一个具有特殊功能的火车头,而等待队列只需要封装线程和它的同步信息即可。

AQS的等待(条件)队列.png

2.2. 同步队列节点的入队和出队

2.2.1. 同步队列节点的入队

04.同步队列节点的入队.gif

2.2.2. 同步队列节点的出队

05.同步队列节点的出队.gif

2.3. 等待队列节点的入队和出队

2.3.1. 等待队列节点的入队

06.等待队列节点的入队.gif

2.3.2. 等待队列节点的出队

07.等待队列节点的出队.gif

值得注意的一点,等待队列的节点出队之后,并不是直接将线程从节点中移除,因为条件对象满足之后,就一定可以立即执行吗?很明显,答案是否定的,线程必须再次检测并尝试获取锁,才能确定资源是否还存在。所以,等待队列的节点移出之后,被转移到了同步队列,直到下次排队到当前线程被唤醒。

2.4. 共享模式下节点的入队与出队

08.共享模式下节点的出队与入队.gif

2.5. 关于节点的取消状态

当线程等待超时、被中断或者发生异常时,会执行取消获取锁的方法:cancelAcquire,并且将节点状态置为1。

  • cancelAcquire(Node node):取消获取锁

    node.thread = null;

    node.waitStatus = Node.CANCELLED;

    node == tail,node脱钩;

    node为中间节点,将前驱的next指向后继节点

    否则,唤醒后继节点

  • Ø中断

    ①acquireQueued(Node node, int arg)

    ②doAcquireShared(int arg)

    ③doAcquireInterruptibly(int arg)

    ④doAcquireSharedInterruptibly(int arg)

  • 超时

    ①doAcquireNanos(int arg, long nanosTimeout)

    ②doAcquireSharedNanos(int arg, long nanosTimeout)

节点的取消.gif

3. AQS的使用方式

3.1. AQS的设计模式——模板方法

模板方法设计模式代码结构.png

  • 概述:定义一个算法的骨架,而将一些步骤延迟到子类中实现

  • 好处:子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤

  • 适用性

    ①一次性实现算法不变的部分,将可变的部分交给子类来实现

    ②将子类公共部分提取出来以避免代码重复,将代码不同之处分离为新的操作,最后,用一个调用这些新

    操作的模板方法替换这些不同的代码

    ③控制子类扩展

/**
 * 抽象父类:模板类
 * 实现一个模板方法,定义算法的骨架
 *
 * 好处:
 *  1.一次性实现算法不变的部分,将可变的部分交给子类来实现
 *  2.将子类公共的部分提取出来,避免代码重复,将代码不同之处分离成新的操作
 *      最后,用一个调用这些新操作的模板方法来替换这些不同的代码
 *  3.控制子类扩展
 */
public abstract class Template {

    /**
     * 模板方法,执行数据更新操作,并打印相关信息
     */
    public void update() {
        System.out.println("开始打印");
        for (int i = 0; i < 10; i++) {
            print();
        }
        System.out.println("打印结束");
    }

    /**
     * 钩子方法
     */
    public abstract void print();
}
/**
 * 具体子类,实现算法的一部分
 */
public class TemplateConcrete extends Template {
    @Override
    public void print() {
        System.out.println("在控制台打印信息");
    }
}
public class Test {
    public static void main(String[] args) {
        Template temp = new TemplateConcrete();
        temp.update();
    }
}

3.2. 自定义同步器的实现步骤

  1. 继承AQS
  2. 重写钩子方法
  3. 独占锁实现
  4. 共享锁实现
package com.boxuegu.aqs.selflock;

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

/**
 * 自定义一个互斥锁
 *
 * 1.继承自AQS类
 * 2.重写钩子方法
 *  获取锁
 *  释放锁
 *
 *  设置状态1为获取锁:
 *      线程尝试获取锁时,如果state的值为0,则可以获取锁,并设置为1
 *  设置状态0为释放锁:
 *      把同步状态设置为0
 *
 */
public class Mutex {

    class Sync extends AbstractQueuedSynchronizer {
        /**
         * 重写钩子方法:尝试获取锁
         * @param arg
         * @return
         */
        public boolean tryAcquire(int arg) {
            return compareAndSetState(0, 1);
        }

        /**
         * 重写钩子方法:尝试释放锁
         * @param arg
         * @return
         */
        protected boolean tryRelease(int arg) {
            setState(0);
            return true;
        }
    }

    private final Sync sync = new Sync();

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

    public void unlock() {
        sync.release(0);
    }

}
package com.boxuegu.aqs.selflock;

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

/**
 * 支持可重入的自定义同步器
 * 1.获取锁
 *      判断当前线程是否为锁的持有者
 *          是:允许进入监视区域
 *          否:返回;或者判断是否为第一次进入锁,若是,则设置当前线程为锁的持有者
 * 2.释放锁
 *      设置新的同步状态
 *      当前锁的持有者,置空
 *
 */
public class ReentrantMutex {
    class Sync extends AbstractQueuedSynchronizer {
        /**
         * 重写钩子方法:尝试获取锁
         * @param arg
         * @return
         */
        public boolean tryAcquire(int arg) {
            if (getState() == 0) { // 当前锁没有被任何线程持有
                if (compareAndSetState(0, 1)) {
                    setExclusiveOwnerThread(Thread.currentThread()); // 设置锁的持有者为当前线程
                    return true;
                }
            } else if (Thread.currentThread() == getExclusiveOwnerThread()) {
                return true;
            }
            return false;
        }

        /**
         * 重写钩子方法:尝试释放锁
         * @param arg
         * @return
         */
        protected boolean tryRelease(int arg) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
    }

    private final Sync sync = new Sync();

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

    public void unlock() {
        sync.release(0);
    }
}
package com.boxuegu.aqs.selflock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 使用Lock框架实现同步机制的标准格式
 * Lock lock = new 子类(); // 锁对象
 * lock.lock(); // 在监视区域开始的时候,加锁
 * try{
 *     // 监视区域,临界区
 * } finally {
 *     lock.unlock(); // 在监视区域结束时,释放锁
 * }
 */
public class TicketSellerReentrantLock {
    static int tickets = 100; // 某趟列出的剩余车票数
    public static void main(String[] args) {
//        Lock lock =  new ReentrantLock(); // 锁对象
//        Mutex lock =  new Mutex(); // 锁对象
        ReentrantMutex lock = new ReentrantMutex(); // 锁对象
        Runnable seller = () -> {
            while (true) {
                lock.lock(); // 加锁
                try {
                    if (tickets <= 0 )
                        break;
                    Thread.sleep(10); // 不会释放锁
                    System.out.println(Thread.currentThread().getName() +
                            "正在售卖第" + tickets-- + "张票");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                     lock.unlock(); // 释放锁
                }
            }
        };

        for (int i = 0; i < 4; i++) {
            new Thread(seller, "窗口" + (i + 1)).start();
        }
    }
}

4. Lock框架再认识

00.AQS原理图.png

J.U.C包中并没有对同步器的API做一个统一的定义。因此,有一些类定义了通用的接口(如Lock),而另外一些则定义了其专有的版本。因此在不同的类中,acquire和release操作的名字和形式会各有不同。例如:Lock.lock,Semaphore.acquire,CountDownLatch.await和FutureTask.get,在这个框架里,这些方法都是acquire操作。但是,J.U.C为支持一系列常见的使用选项,在类间都有个一致约定。在有意义的情况下,每一个同步器都支持下面的操作:

  • 阻塞和非阻塞(例如tryLock)同步。
  • 可选的超时设置,让调用者可以放弃等待
  • 通过中断实现的任务取消,通常是分为两个版本,一个acquire可取消,而另一个不可以。

同步器的实现根据其状态是否独占而有所不同。独占状态的同步器,在同一时间只有一个线程可以通过阻塞点,而共享状态的同步器可以同时有多个线程在执行。一般锁的实现类往往只维护独占状态,但是,例如计数信号量在数量许可的情况下,允许多个线程同时执行。为了使框架能得到广泛应用,这两种模式都要支持。

j.u.c包里还定义了Condition接口,用于支持管程形式的await/signal操作,这些操作与独占模式的Lock类有关,且Condition的实现天生就和与其关联的Lock类紧密相关。

4.1. Lock接口的核心方法

4.1.1. 方法列表。。。

void lock(); // 加锁

void lockInterruptibly() throws InterruptedException; // 加锁,支持响应中断

boolean tryLock(); // 尝试加锁

// 尝试加锁,支持响应中断
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

void unlock(); // 释放锁

Condition newCondition(); // 条件对象

4.1.2. Lock接口与synchronized的比较

1.优先选择synchronized关键字,因为效率已经与Lock相当

2.Lock支持获取锁被中断、超时等情况

3.Lock支持尝试获取锁(tryLock),支持闯入策略

4.支持公平和非公平策略

4.2. 读写锁ReadWriteLock

实现类:ReentrantReadWriteLock

同一时刻,支持任意数量的线程读数据 但是,如果写线程访问是,有且只有一个写线程可以执行(也没有读线程) ReadLock --> lock,用于读操作 lock():共享模式 WriteLock --> lock,用于写操作 lock():独占模式

适用性:读多写少

注意事项:

1.与互斥锁相比,读写锁效率提升在于: 读与写的相对频率,读越多,写越少,效率越高,反之越低(生产者-消费者模式) 读与写的持续时间 数据的争用 同时尝试读、写的线程数量

2.阅读源码要注意的点: 写入器释放锁之后,此时,写线程和读线程都在等待获取锁,把锁给谁? 锁的重入性:读锁本身是否可重入;写锁本身是否可重入;持有写锁的线程能够再持有写锁? 写锁是否可以降级为读锁,反之如何?如何实现?

public class TestRW {

    // 读商品数据线程
    private static class ReadThread implements Runnable {
        private GoodsService goodsService;

        public ReadThread(GoodsService goodsService) {
            this.goodsService = goodsService;
        }

        @Override
        public void run() {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100; i++) {
                goodsService.getNum();
            }
            System.out.println(Thread.currentThread().getName() + "读取商品数据耗时:"
                    + (System.currentTimeMillis() - start) + "ms");

        }
    }

    // 写商品数据线程
    private static class WriteThread implements Runnable {
        private GoodsService goodsService;

        public WriteThread(GoodsService goodsService) {
            this.goodsService = goodsService;
        }

        @Override
        public void run() {
            long start = System.currentTimeMillis();
            Random r = new Random();
            for (int i = 0; i < 10; i++) {
                goodsService.setNum(r.nextInt(10));
                Sleep.millis(50);
            }
            System.err.println(Thread.currentThread().getName()
                    + "写商品数据耗时:" + (System.currentTimeMillis() - start) + "ms");

        }
    }

    public static void main(String[] args) throws InterruptedException {
        Goods goods = new Goods(10);
//        GoodsService goodsService = new SyncLock(goods); // synchronized实现
        GoodsService goodsService = new RWLock(goods); // 读写锁实现
        for (int i = 0; i < 3; i++) { // 写线程数量
            Thread setT = new Thread(new WriteThread(goodsService));
            for (int j = 0; j < 10; j++) { // 读线程数量
                Thread getT = new Thread(new ReadThread(goodsService));
                getT.start();
            }
            Sleep.millis(100);
            setT.start();
        }
    }
}
/**
 * 用内置锁来实现商品服务接口
 */
public class SyncLock implements GoodsService {
    private Goods goods;

    public SyncLock(Goods goods) {
        this.goods = goods;
    }

    @Override
    public synchronized int getNum() {
        Sleep.millis(5);
        return 10;
    }

    @Override
    public synchronized void setNum(int number) {
        Sleep.millis(5);
        goods.setStoreNumber(number);
    }

}
/**
 * 读写锁实现读多写少的效果
 *
 */
public class RWLock implements GoodsService {

    private Goods goods;

    public RWLock(Goods goods) {
        this.goods = goods;
    }

    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock getLock = lock.readLock(); // 读锁
    private final Lock setLock = lock.writeLock(); // 写锁

    @Override
    public int getNum() {
        getLock.lock();
        try {
            Sleep.millis(5);
            return this.goods.getStoreNumber();
        } finally {
            getLock.unlock();
        }
    }

    @Override
    public void setNum(int number) {
        setLock.lock();
        try {
            Sleep.millis(5);
            goods.setStoreNumber(number);
        } finally {
            setLock.unlock();
        }
    }
}
/**
 * 商品服务接口
 */
public interface GoodsService {

    int getNum(); // 获得商品库存数量

    void setNum(int number);// 设置商品库存数量
}
/**
 *
 * 商品实体类
 */
public class Goods {
    private int storeNumber; // 库存数量

    public Goods(int storeNumber) {
        this.storeNumber = storeNumber;
    }

    public int getStoreNumber() { // 读库存:上货
        return storeNumber;
    }

    public void setStoreNumber(int storeNumber) { // 写库存:进货
        this.storeNumber = storeNumber;
    }
}

4.3. 回顾Callable、Future和FutureTask

略。

4.4. Fork-Join框架

Fork-Join框架.png

4.4.1. 什么是Fork/Join

规模为N的问题,N<阈值,直接解决,N>阈值,将N分解为K个小规模子问题,子问题互相对立,与原问题形式相同,将子问题的解合并得到原问题的解

与动态规范的区别:

分割的子任务之间是否有联系

4.4.2 工作秘取

当一个线程去完成自己的任务队列后,为了防止CPU空转,让线程去别的任务队列的尾部“偷取(steeling)”一个任务去执行,执行完成再将结果放回对应的位置,即工作秘取:work steeling。