JavaSE——多线程:Lock体系

1.初识Lock与AbstractQueuedSynchronizer(AQS)

1.1.Lock简介

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。在Lock接口出现之前,java程序主要是靠synchronized关键字实现锁功能的,而JDK5之后,并发包中增加了lock接口,它提供了与synchronized一样的锁功能。虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性,通常使用显示使用lock的形式如下:

Lock lock = new ReentrantLock();
lock.lock();
try{
    .......
}finally{
    lock.unlock();
}

需要注意的是synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁,因此在finally块中释放锁

1.2 Lock接口API

  • void lock():获取锁
  • void lockInterruptibly() throws InterruptedException:获取锁的过程能够响应中断(Lock独有
  • boolean tryLock():非阻塞式响应中断,能立即返回,获取锁返回true,反之为false
  • boolean tryLock(long time,TimeUnit unit):超时获取锁,规定时间内未获取到锁,线程直接返回(Lock独有
  • Condition newCondition():获取与lock绑定的等待通知组件,当前线程必须先获得了锁才能等待,等待会释放锁,再次获取到锁才能从等待中返回
  • void unlock():解锁

1.3.初识AQS(AbstractQueuedSynchronizer,同步器)——Lock体系最核心存在

AQS(同步器)作用:用来构建锁与其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列形成同步队列它的子类必须重写AQS的几个protected修饰的用来改变同步状态的方法,其他方法主要是实现排队与阻塞机制,状态更新使用getState()、setState()、compareAndSetState()这三个方法
子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以方便的实现不同类型的同步组件
:锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作,锁和同步器很好的隔离了使用者和实现者所需关注的领域

1.4.AQS的模板方法设计模式

AQS的设计是使用模板方法设计模式,它将一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法。比如:

//AQS中需要重写的方法tryAcquire
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

ReentrantLock中NonfairSync(继承AQS)会重写该方法为:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

而AQS中的模板方法acquire():

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

会调用tryAcquire方法,而此时当继承AQS的NonfairSync调用模板方法acquire时就会调用已经被NonfairSync重写的tryAcquire方法。这就是使用AQS的方式,可以归纳总结为这么几点:

  • 同步组件(这里不仅仅值锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内存类
  • AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类方法时就会调用被重写的方法
  • AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义
  • 在重写AQS的方式时,使用AQS提供的getState(),setState(),compareAndSetState() 方法进行修改同步状态

AQS提供的模板方法可以分为3类:

  • 独占式获取与释放同步状态
  • 共享式获取与释放同步状态
  • 查询同步队列中等待线程情况

自己实现一个简易Lock锁:

package xpu.edu;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

class Mutex implements Lock {
    private Sync sync = new Sync();

    static class Sync extends AbstractQueuedSynchronizer{
        @Override
        protected boolean tryAcquire(int arg) {
            if (arg != 1) {
                throw new RuntimeException("信号量不为1!");
            }
            if (compareAndSetState(0,1)) {
            // 当前线程成功获取锁
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        @Override
        protected boolean isHeldExclusively() {
            return getState()==1;
        }
        Condition newCondition() {
            return new ConditionObject();
        }
    }
// Lock接口实现方法
    @Override
    public void lock() {
        sync.acquire(1);
    }
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }
    @Override
    public void unlock() {
        sync.release(1);
    }
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

public class Test{
    private static Mutex mutex = new Mutex();
    public static void main(String[] args){
        for (int i = 0; i < 10;i++){
            Thread thread = new Thread(()-> {
                mutex.lock();
                try {
                    Thread.sleep(3000);
                }catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    mutex.unlock();
                }
            });
            thread.start();
        }
    }
}

2深入理解AbstractQueuedSynchronizer(AQS)

2.1.AQS简介

在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义,AQS则实现了对同步状态的管理,以及对阻塞线程进行排队,等待通知等等一些底层的实现处理。AQS的核心也包括了这些方面:同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现,而这些实际上则是AQS提供出来的模板方法,归纳整理如下:

  • 独占式锁
    1.void acquire(int arg) :独占式获取同步状态,如果获取失败则将当前线程插入同步队列进行等待
    2.void acquireInterruptibly(int arg) :与acquire方法相同,但在同步队列中等待时可以响应中断
    3.boolean tryAcquireNanos(int arg,long nanosTimeout):在2的基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false
    4.boolean tryAcquire(int arg) : 获取锁成功返回true,否则返回false
    5.boolean release(int arg) : 释放同步状态,该方法会唤醒在同步队列中的下一个节点
  • 共享式锁
    1.void acquireShared(int arg) :共享式获取同步状态,与独占锁的区别在于同一时刻有多个线程获取同步
    状态
    2.void acquireSharedInterruptibly(int arg):增加了响应中断的功能
    3.boolean tryAcquireSharedNanos(int arg,lone nanosTimeout):在2的基础上增加了超时等待功能
    4.boolean releaseShared(int arg):共享锁释放同步状态

2.2.同步队列

当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。就数据结构而言,队列的实现方式无外乎两者一是通过数组的形式,另外一种则是链表的形式,AQS中的同步队列则是通过链式方式进行实现
在AQS有一个静态内部类Node,这是我们同步队列的每个具体节点,在这个类中有如下属性:

  • volatile int waitStatus:节点状态
  • volatile Node prev:当前节点的前驱节点
  • volatile Node next:当前节点的后继节点
  • volatile Thread thread:当前节点所包装的线程对象
  • Node nextWaiter:等待队列中的下一个节点

节点的状态如下:

  • int INITIAL = 0:初始状态
  • int CANCELLED = 1:当前节点从同步队列中取消
  • int SIGNAL = -1:后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程继续运行
  • int CONDITION = -2:节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中
  • int PROPAGATE = -3:表示下一次共享式同步状态获取将会无条件地被传播下去

很显然AQS中的同步队列是一个双向链表,将线程封装为Node结点后进行入队与出队处理,节点如何进行入队和出队操作?实际上这对应着锁的获取和释放两个操作:获取锁失败进行入队操作,获取锁成功进行出队操作

2.3.独占锁

2.3.1.独占锁的获取

调用lock()方法是获取独占锁,获取失败就将当前线程加入同步队列,成功则线程执行。
ReentrantLock源码:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

lock方法使用CAS来尝试将同步状态改为1,如果成功则将同步状态持有线程置为当前线程,否则将调用AQS提供的acquire()方法:

public final void acquire(int arg) {
	// 再次尝试获取同步状态,如果成功则方法直接返回
	// 如果失败则先调用addWaiter()方法再调用acquireQueued()方法
    if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

当线程获取独占式锁失败后就会将当前线程加入同步队列,那么加入队列的方式是怎样的了?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) {
    // 将当前线程以尾插的方式插入同步队列中
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 当前尾节点为空或CAS尾插失败
    enq(node);
    return node;
}

程序的逻辑主要分为两个部分:

  • 当前同步队列的尾节点为null,调用方法enq()插入
  • 当前队列的尾节点不为null,则采用尾插(compareAndSetTail())的方式入队

另外还会有另外一个问题:如果 if(compareAndSetTail(pred, node))为false怎么办?会继续执行到enq()方法,同时很明显compareAndSetTail是一个CAS操作,通常来说如果CAS操作失败会继续自旋(死循环)进行重试

综上所述,enq()方法可能承担两个任务:

  • 处理当前同步队列尾节点为null时进行入队操作
  • 如果CAS尾插入节点失败后负责自旋进行尝试

enq()源码如下:

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            // 头结点初始化
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            // CAS尾插,失败进行自旋重试直到成功为止。
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

首先会先创建头结点,说明同步队列是带头结点的链式存储结构,带头结点会在入队和出队的操作中获得更大的便捷性,带头节点的队列初始化时机是在tail为null时(当前线程是第一次插入同步队列),compareAndSetTail(t, node)方法会利用CAS操作设置尾节点,如果CAS操作失败会在死循环中不断尝
试,直至成功return返回为止。综上,对enq()方法可以做这样的总结:

  • 在当前线程是第一个加入同步队列时,调用compareAndSetHead(new Node())方法,完成链式队列的头结点的初始化
  • 自旋不断尝试CAS尾插入节点直至成功为止

在同步队列中的节点(线程)会做什么事情了来保证自己能够有机会获得独占式锁了?来看看acquireQueued()方法,这个方法的作用就是排队获取锁的过程:

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;
            }
            // 获取同步状态失败,线程进入等待状态等待获取独占锁
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

整体来看这是一个这又是一个自旋的过程,首先获取当前节点的先驱节点,如果先驱节点是头结点的并且成功获得同步状态的时候(if (p == head && tryAcquire(arg))),当前节点所指向的线程能够获取锁,反之,获取锁失败进入等待状态。获取锁的节点出队的逻辑是:

//队列头结点引用指向当前节点
setHead(node);
//释放前驱节点
p.next = null;
failed = false;
return interrupted;

setHead()方法为:

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

将当前节点通过setHead()方法设置为队列的头结点,然后将之前的头结点的next域设置为null并且pre域也为null,即与队列断开,无任何引用方便GC时能够将内存进行回收。那么当获取锁失败的时候会调用shouldParkAfterFailedAcquire()方法和parkAndCheckInterrupt()方法,shouldParkAfterFailedAcquire()方法源码为:

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()方法主要逻辑是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL) 使用CAS将节点状态由INITIAL设置成SIGNAL,表示当前线程阻塞,当compareAndSetWaitStatus设置失败则说明
shouldParkAfterFailedAcquire方法返回false,然后会在acquireQueued()方法死循环中会继续重试,直至compareAndSetWaitStatus设置节点状态位为SIGNAL时shouldParkAfterFailedAcquire返回true时才会执行方
法parkAndCheckInterrupt()方法,该方法的源码为:

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

该方法的关键是会调用LookSupport.park()方法),该方法是用来阻塞当前线程的。因此acquireQueued()在自旋过程中主要完成了两件事情:

  • 如果当前节点的前驱节点是头节点,并且能够获得同步状态的话,当前线程能够获得锁该方法执行结束退出
  • 获取锁失败的话,先将节点状态设置成SIGNAL,然后调用LookSupport.park方法使得当前线程阻塞

在这里插入图片描述

2.3.2.独占锁的释放(release()方法)

独占锁的释放调用unlock方法,而该方法实际调用了AQS的release方法,两个方法的源码如下:

public void unlock() {
	sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

如果同步状态释放成功(tryRelease返回true)则会执行if块中的代码,当head指向的头结点不为null,并且该节点的状态值不为0的话才会执行unparkSuccessor()方法。unparkSuccessor方法源码如下:

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling. It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node. But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    // 头结点的后继节点
    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)
    // 后继节点不为null时唤醒
        LockSupport.unpark(s.thread);
}

首先获取头节点的后继节点,当后继节点的时候会调用LookSupport.unpark()方法,该方法会唤醒该节点的后继节点所包装的线程,每一次锁释放后就会唤醒队列中该节点的后继节点所引用的线程,从而进一步可以佐证获得锁的过程是一个FIFO(先进先出)的过程


总结:

  • 线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试
  • 线程获取锁是一个自旋的过程,当且仅当 当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞
  • 释放锁的时候会唤醒后继节点

总体来说:在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态,在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点

2.3.3.可中断式获取锁

lock相较于synchronized有一些更方便的特性,比如能响应中断以及超时等待等特性。可响应中断式锁可调用方法lock.lockInterruptibly(),而该方法其底层会调用AQS的acquireInterruptibly方法,源码为:

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);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            // 获取锁出队
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                // 线程中断异常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

与acquire方法逻辑几乎一致,唯一的区别是当parkAndCheckInterrupt返回true时即线程阻塞时该线程被中断,代码抛出被中断异常

2.3.4.超时等待式获取锁(tryAcquireNanos()方法)

通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果,该方法会在三种情况下才会返回:

  • 在超时时间内,当前线程成功获取了锁
  • 当前线程在超时时间内被中断
  • 超时时间结束,仍未获得锁返回false

该方法会调用AQS的方法tryAcquireNanos(),源码为:

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
            // 实现超时等待的效果
            doAcquireNanos(arg, nanosTimeout);
}

最终是靠doAcquireNanos方法实现超时等待的效果,该方法源码如下:

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    // 1.根据超时时间和当前时间计算出截止时间
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            // 2.当前线程获得锁出队列
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            // 3.1 重新计算超时时间
            nanosTimeout = deadline - System.nanoTime();
            // 3.2 已经超时返回false
            if (nanosTimeout <= 0L)
                return false;
            // 3.3 线程阻塞等待
            if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            // 3.4 线程被中断抛出被中断异常
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

在第1步会先计算出按照现在时间和超时时间计算出理论上的截止时间,然后根据deadline - System.nanoTime() 就可以判断是否已经超时了,如果还没有超时(判断为true时)就会继续通过LockSupport.parkNanos使得当前线程阻塞,同时增加了对中断的检测,若检测出被中断直接抛出被中断异常

3.理解ReentrantLock(Lock中使用频率最高的类)——可重入锁

3.1.ReentrantLock介绍

ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁非公平锁两种方式

3.2.重入性的实现原理

要想支持重入性,就要解决两个问题:

  • 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功
  • 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功

针对第一个问题,我们来看看ReentrantLock是怎样实现的,以非公平锁为例,判断当前线程能否获得锁为例,核心方法为nonfairTryAcquire:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 1.如果该锁未被任何线程占有,该锁能被当前线程获取
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 2.若被占有,检查占有线程是否是当前线程
    else if (current == getExclusiveOwnerThread()) {
        // 3.再次获取,计数+1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

为了支持重入性,在第二步增加了处理逻辑,如果该锁已经被线程所占有了,会继续检查占有线程是否为当前线程,如果是的话,同步状态加1返回true,表示可以再次获取成功。每次重新获取都会对同步状态进行加一的操作

么释放的时候,依然还是以非公平锁为例,核心方法为tryRelease:

protected final boolean tryRelease(int releases) {
    // 1.同步状态-1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        // 2.只有当同步状态为0时,锁成功释放,返回false
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 3.锁未被完全释放,返回false
    setState(c);
    return free;
}

需要注意的是,重入锁的释放必须得等到同步状态为0时锁才算成功释放,否则锁仍未释放。如果锁被获取n次,释放了n-1次,该锁未完全释放返回false,只有被释放n次才算成功释放,返回true

3.3.公平锁与非公平锁

ReentrantLock支持两种锁:

  • 公平锁:,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO
  • 非公平锁

ReentrantLock的构造方法无参时是构造非公平锁,
源码为:

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

另外还提供了另外一种方式,可传入一个boolean值,true时为公平锁,false时为非公平锁,源码为:

public ReentrantLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync();
}

非公平锁获取时(nonfairTryAcquire方法)只是简单的获取了一下当前状态做了一些逻辑处理,并没有考虑到当前同步队列中线程等待的情况,我们来看看公平锁的处理逻辑:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 增加了判断是否有前驱节点
        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");
        setState(nextc);
        return true;
    }
    return false;
}

hasQueuedPredecessors,从方法名就可知道该方法用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败,如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁


公平锁和非公平锁对比:
公平锁保证每次取消锁均为同步队列的第一个节点,保证请求资源时间上的绝对顺序,但是效率较低,需要频繁的上下文交换,非公平锁会降低性能开销,降低一定的上下文切换,但是可能导致其他线程永远无法获取到锁造成线程的饥饿现象,通常来讲,没有特性的公平性要求尽量选择非公平锁,同时这个选择也是ReentrantLock的默认选择

4.ReentrantReadWriteLock(可重入读写锁)

读写者模型:读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程以及其他写线程均会被阻塞。但是要注意的是,读锁不等于无锁

  • 读锁限制写锁,无锁无法干扰写锁
  • 读锁可以控制锁数量

4.1.如何区分读写状态

在ReentrantReadWriteLock对象内部维护了一个读写状态:
在这里插入图片描述
读写锁依赖自定义同步器实现同步功能,读写状态也就是同步器的同步状态。读写锁将整形变量切分成两部分,高16位表示读,低16位表示写
在这里插入图片描述
读写锁通过位运算计算各自的同步状态。假设当前同步状态的值为c,写状态就为c&0x0000FFFF,读状态为c >>> 16(无符号位补0右移16位)。当写状态增加1状态变为c+1,当读状态增加1时,状态编码就是c+(1 <<< 16)

4.2.读锁——共享锁(一般与独占锁搭配使用,实现读写者模型)

首先读写锁中读状态为所有线程获取读锁的次数,在每次获取读锁之前都会进行一次判断是否存在独占式写锁,如果存在独占式写锁就直接返回获取失败,进入同步队列中。如果当前没有写锁被获取,则线程可以获取读锁,由于共享锁的存在,每次获取都会判断线程的类型,以便每个线程获取同步状态的时候都在其对应的本地变量上进行自增操作(只要当前没有写线程获取到写锁并且读锁的获取次数不超过最大值,读锁就能获取成功

4.3.写锁——独占锁

写锁的获取——tryAcquire(int acquires)

  • 获取读写锁状态
  • 获取独占式锁状态,即写锁状态
  • 表示当前有读线程拿到读锁,写线程无法获取同步状态
  • 写锁的可重入次数已达最大值
  • 写锁可重入
  • 此时读写状态为0,写锁可以正常获取到同步状态
  • 将当前线程置为只有写锁线程

写锁可以降级为读锁但是读锁不能升级为写锁

5.Condition

Object类是Java中所有类的父类, 在线程间实现通信的往往会应用到Object的几个方法: wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll() 实现等待/通知机制,同样的,在Java Lock体系下依然会有同样的方法实现等待/通知机制。从整体上来看Object的wait和notify是与对象监视器配合完成线程间的等待/通知机制,Condition的await、signal是与Lock配合完成等待/通知机制。 前者是Java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。 两者除了在使用方式上不同外,在功能特性上还是有很多的不同:

  • Condition支持不响应中断,而Object提供wait不支持
  • Condition支持多个等待队列,而Object的wait只有一个等待队列
  • Condition支持设置截止时间,而Object的wait只支持设置超时时间

5.1.类比Object的wait方法

  • void await() throws InterruptedException:当前线程进入等待状态,如果在等待状态中被中断会抛出被中断异常
  • long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时
  • boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位
  • boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间

5.2.类比Object的notify/notifyAll方法

  • void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回
  • void signalAll():与前者的区别在于能够唤醒所有等待在condition上的线程

5.3.等待队列

创建一个Condition对象是通过lock.newCondition(), 而这个方法实际上是会创建ConditionObject对象,该类是AQS的一个内部类。 Condition是要和Lock配合使用的也就是Condition和Lock是绑定在一起的,而lock的实现原理又依赖于AQS, 自然而然ConditionObject作为AQS的一个内部类无可厚非。 我们知道在锁机制的实现上,AQS内部维护了一个同步队列,如果是独占式锁的话, 所有获取锁失败的线程的尾插入到同步队列, 同样的,Condition内部也是使用同样的方式,内部维护了一个等待队列, 所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。 另外注意到ConditionObject中有两个成员变量:

//First node of condition queue
private transient Node firstWaiter;
//Last node of condition queue
private transient Node lastWaiter;

ConditionObject通过持有等待队列的头尾指针来管理等待队列。 注意Node类复用了在AQS中的Node类,Node类有这样一个属性:

//后继节点Node nextWaiter;

等待队列是一个单向队列,而在之前说AQS时知道同步队列是一个双向队列,我们可以多次调用lock.newCondition()方法创建多个Condition对象,也就是一个lLock可以持有多个等待队列。 利用Object的方式实际上是指在对象Object对象监视器上只能拥有一个同步队列和一个等待队列; 并发包中的Lock拥有一个同步队列和多个等待队列。另外ConditionObject是AQS的内部类, 因此每个ConditionObject能够访问到AQS提供的方法,相当于每个Condition都拥有所属同步器的引用

5.4.await实现原理

当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列, 如果该线程能够从await()方法返回的话一定是该线程获取了与condition相关联的lock,await()方法源码如下:

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 1. 将当前线程包装成Node,尾插法插入到等待队列中
    Node node = addConditionWaiter();
    // 2. 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点
    long savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        // 3. 当前线程进入到等待状态
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 4. 自旋等待获取到同步状态(即获取到lock)
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 5. 处理被中断的情况
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

当前线程调用condition.await()方法后,会使得当前线程释放lock然后加入到等待队列中, 直至被signal/signalAll后会使得当前线程从等待队列中移至到同步队列中去, 直到获得了lock后才会从await方法返回,或者在等待时被中断会做中断处理

addConditionWaiter(),将当前线程添加到等待队列中,其源码如下:

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //将当前线程包装成Node
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    //t==null,同步队列为空的情况
    if (t == null)
        firstWaiter = node;
    else
        //尾插法
        t.nextWaiter = node;
    //更新lastWaiter
    lastWaiter = node;
    return node;
}

这里通过尾插法将当前线程封装的Node插入到等待队列中, 同时可以看出等待队列是一个不带头结点的链式队列,之前我们学习AQS时知道同步队列是一个带头结点的链式队列

将当前节点插入到等待对列之后,使用fullyRelease(Node node)方法释放当前线程释放lock,源码如下:

final long fullyRelease(Node node) {
    boolean failed = true;
    try {
        long savedState = getState();
        if (release(savedState)) {
            //成功释放同步状态
            failed = false;
            return savedState;
        } else {
            //不成功释放同步状态抛出异常
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

调用AQS的模板方法release()方法释放AQS的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程, 如果释放成功则正常返回,若失败的话就抛出异常

线程如何能从await()方法中退出

  • 在等待时被中断,通过break退出循环
  • 被唤醒后置入同步队列,退出循环

5.5.signal和signalAll实现原理

5.5.1signal

调用Condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock, 按照等待队列是先进先出(FIFO)的, 所以等待队列的头节点必然会是等待时间最长的节点, 也就是每次调用condition的signal方法是将头节点移动到同步队列中。 signal()源码如下:

public final void signal() {  
    //1. 先检测当前线程是否已经获取lock
    if (!isHeldExclusively())       
        throw new IllegalMonitorStateException();   
    //2. 获取等待队列中第一个节点,之后的操作都是针对这个节点
    Node first = firstWaiter;  
    if (first != null)
        doSignal(first);
}

ignal方法首先会检测当前线程是否已经获取lock, 如果没有获取lock会直接抛出异常,如果获取的话再得到等待队列的头指针引用的节点,doSignal方法也是基于该节点, doSignal方法源码如下:

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        //1. 将头结点从等待队列中移除
        /first.nextWaiter = null;
        //2. while中transferForSignal方法对头结点做真正的处理
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
}

真正对头节点做处理的是transferForSignal(),该方法源码如下:

final boolean transferForSignal(Node node) {	
    //1. 更新状态为0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))    
        return false;	
    //2.将该节点移入到同步队列中去
    Node p = enq(node);   
    int ws = p.waitStatus;    
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))   
        LockSupport.unpark(node.thread);    
    return true;
}

这段代码主要做了两件事情:

  • 将头结点的状态更改为CONDITION
  • 调用enq方法,将该节点尾插入到同步队列中

调用condition的signal的前提条件是当前线程已经获取了lock,该方法会使得等待队列中的头节点(等待时间最长的那个节点)移入到同步队列, 而移入到同步队列后才有机会使得等待线程被唤醒, 即从await方法中的LockSupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出

5.5.2signalAll

sigllAll与sigal方法的区别体现在doSignalAll方法上。doSignalAll()的源码如下:

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;   
    do {        
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

doSignal方法只会对等待队列的头节点进行操作,而doSignalAll方法将等待队列中的每一个节点都移入到同步队列中, 即“通知”当前调用condition.await()方法的每一个线程

5.5.3.await与signal和signalAll的结合

await和signal和signalAll方法就像一个开关控制着线程A(等待方)和线程B(通知方),线程awaitThread先通过lock.lock()方法获取锁成功后调用了condition.await方法进入等待队列, 而另一个线程signalThread通过lock.lock()方法获取锁成功后调用了condition.signal或者signalAll方法, 使得线程awaitThread能够有机会移入到同步队列中, 当其他线程释放lock后使得线程awaitThread能够有机会获取lock, 从而使得线程awaitThread能够从await方法中退出,然后执行后续操作,如果awaitThread获取lock失败会直接进入到同步队列

猜你喜欢

转载自blog.csdn.net/LiLiLiLaLa/article/details/94554478