多线程学习-锁

前言

本篇文章将对基于AbstractQueuedSynchronizer实现的锁进行学习,同时对LockSupportCondition的使用进行整理和分析。内容参考了《Java并发编程的艺术》第5章。在之前的多线程学习-队列同步器中已经对AbstractQueuedSynchronizer的原理进行了详尽分析,如果不熟悉AbstractQueuedSynchronizer,可以先查阅该篇文章。

参考资料:《Java并发编程的艺术》

正文

一. 重入锁

重入锁,即ReentrantLock,继承于Lock接口,提供锁重入功能。重入锁与不可重入锁的区别在于,重入锁支持已经获取锁的线程重复对锁资源进行获取,Java中的synchronized关键字可以隐式的支持锁重入功能,考虑如下一个例子。

public class HelloUtil {

    public static synchronized void sayHello() {
        System.out.print("Hello ");
        sayWorld();
    }

    public static synchronized void sayWorld() {
        System.out.println("World");
    }

}

已知访问由synchronized关键字修饰的静态方法时需要先获取方法所在类的Class对象作为锁资源,所以当A线程调用HelloUtilsayHello()方法时,需要获取的锁资源为HelloUtil类的Class对象,此时B线程再调用HelloUtilsayHello()sayWorld()方法时会被阻塞,但是A线程却可以在sayHello()方法中再调用sayWorld()方法,即A线程在已经获取了锁资源的情况下又获取了一次锁资源,这就是synchronized关键字对锁重入的支持。

结合上面的例子,已经对重入锁有了直观的认识,下面将分析ReentrantLock是如何实现重入锁的。ReentrantLock的类图如下所示。

多线程学习-锁_第1张图片

ReentrantLock有三个静态内部类,其中Sync继承于AbstractQueuedSynchronizer,然后FairSyncNonfairSync继承于Sync,因此SyncFairSyncNonfairSync均是ReentrantLock组件中的自定义同步器,且FairSync提供公平获取锁机制,NonfairSync提供非公平获取锁机制。公平和非公平获取锁机制现在暂且不谈,下面先看一下SyncFairSyncNonfairSync实现了哪些方法,如下所示。

多线程学习-锁_第2张图片

NonfairSyncFairSync提供获取锁机制的不同就在于其实现的lock()tryAcquire()方法的不同,具体是使用哪一种获取锁机制,是在创建ReentrantLock时指定的,ReentrantLock的构造函数如下所示。

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

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

由上述可知,ReentrantLock默认使用非公平获取锁机制,然后可以在构造函数中根据传入的fair参数决定使用哪种机制。现在先对上面的讨论做一个小节:ReentrantLock是可重入锁,即已经获取锁资源的线程可以重复对锁资源进行获取,ReentrantLock内部有三个自定义同步器,分别为SyncNonfairSyncFairSync,其中NonfairSyncFairSync能分别提供非公平获取锁机制和公平获取锁机制,具体使用哪一种获取锁机制,需要在ReentrantLock的构造函数中指定。

接下来结合NonfairSyncFairSynclock()tryAcquire()方法的源码,对非公平获取锁机制和公平获取锁机制进行说明。

NonfairSynclock()方法如下所示。

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

非公平获取锁调用lock()方法时会先将stateCAS方式从0设置为1,设置成功表示竞争到了锁,因此非公平获取锁意味着同时获取锁资源时会存在竞争关系,不能满足先到先获取的原则。如果将stateCAS方式从0设置为1失败时,会调用模板方法acquire(),已知acquire()方法会调用tryAcquire()方法,而NonfairSynctryAcquire()方法会调用其父类SyncnonfairTryAcquire()方法,下面看一下其实现。

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //当前线程如果是获取到锁资源的线程,则将state字段加1
    //当前线程如果不是获取到锁资源的线程,则返回false然后加入同步队列
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

nonfairTryAcquire()方法中主要是对获取锁资源的线程进行判断,如果当前线程就是已经获取到锁资源的线程,那么就会将state加1,因为每次都是将state加1,所以可以重复获取锁资源。

接下来再看一下公平获取锁机制的FairSync的实现,首先FairSynclock()方法会直接调用模板方法acquire(),并已知在acquire()方法中会调用tryAcquire()方法,所以这里直接看FairSynctryAcquire()方法的实现。

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

FairSynctryAcquire()方法与NonfairSync的不同在于当state为0时多了一个hasQueuedPredecessors()方法的判断逻辑,即判断当前的同步队列中是否已经有正在等待获取锁资源的线程,如果有,则返回true因此公平获取锁意味着绝对时间上最先请求锁资源的线程会最先获取锁,以及等待获取锁资源时间最长的线程会最优先获取锁,这样的获取锁机制就是公平的。

现在最后分析一下ReentrantLock的解锁逻辑。无论是非公平获取锁机制还是公平获取锁机制,如果重复对锁资源进行了n次获取,那么成功解锁就需要对锁资源进行n次释放,前(n - 1)次释放锁资源都应该返回falseReentrantLockunlock()方法会直接调用AbstractQueuedSynchronizer的模板方法release(),并已知在release()方法中会调用tryRelease()方法,这里调用的是Sync实现的tryRelease()方法,如下所示。

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;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

tryRelease()方法中每成功释放一次锁资源,就会将state减1,所以当state为0时,就判断锁资源被全部释放,即释放锁资源成功。

二. 读写锁

读写锁,即ReentrantReadWriteLock,同一时刻可以允许多个读线程获取锁,但当写线程获取锁后,读线程和其它写线程应该被阻塞。下面以一个单例缓存的例子来说明ReentrantReadWriteLock的使用。

public class Cache {

    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock rLock = rwLock.readLock();
    private final Lock wLock = rwLock.writeLock();

    private final Map map = new HashMap<>();

    private static Cache instance = null;

    private Cache() {}

    public Cache getCacheInstance() {
        if (instance == null) {
            synchronized (Cache.class) {
                if (instance == null) {
                    instance = new Cache();
                }
            }
        }
        return instance;
    }

    public String getValueByKey(String key) {
        rLock.lock();
        try {
            return map.get(key);
        } finally {
            rLock.unlock();
        }
    }

    public void addValueByKey(String key, String value) {
        wLock.lock();
        try {
            map.put(key, value);
        } finally {
            wLock.unlock();
        }
    }

    public void clearCache() {
        wLock.lock();
        try {
            map.clear();
        } finally {
            wLock.unlock();
        }
    }

}

根据例子可知,ReentrantReadWriteLock提供了一对锁:写锁读锁,并且使用规则如下。

  • 当前线程获取读锁时,读锁是否被获取不会影响读锁的获取;
  • 当前线程获取读锁时,若写锁未被获取或者写锁被当前线程获取,则允许获取读锁,否则进入等待状态;
  • 当前线程获取写锁时,若读锁已经被获取,无论获取读锁的线程是否是当前线程,都进入等待状态;
  • 当前线程获取写锁时,若写锁已经被其它线程获取,则进入等待状态。

下面将结合源码对写锁和读锁的获取和释放进行分析。首先看一下ReentrantReadWriteLock的类图。

多线程学习-锁_第3张图片

ReentrantReadWriteLock一共有五个内部类,分别为SyncFairSyncNonfairSyncWriteLockReadLock,同时可以看到,只有WriteLockReadLock实现了Lock接口,因此ReentrantReadWriteLock的写锁和读锁的获取和释放实际上是由WriteLockReadLock来完成,所以这里对ReentrantReadWriteLock的工作原理进行一个简单概括:ReentrantReadWriteLock的写锁和读锁的获取和释放分别由其内部类WriteLockReadLock来完成,而WriteLockReadLock对同步状态的操作又是依赖于ReentrantReadWriteLock实现的三个自定义同步器SyncFairSyncNonfairSync

下面继续分析写锁和读锁的同步状态的设计。通过上面的分析可以知道WriteLockReadLock依赖同一个自定义同步组件Sync,因此WriteLockReadLock对同步状态进行操作时会修改同一个state变量,即需要在同一个整型变量state上维护写锁和读锁的同步状态,而Java中整型变量一共有32位,所以ReentrantReadWriteLockstate的高16位表示读锁的同步状态,低16位表示写锁的同步状态。鉴于读写锁同步状态的设计,对读写锁同步状态的运算操作归纳如下。

  • 获取写锁同步状态: state & 0x0000FFFF
  • 获取读锁同步状态: state >>> 16
  • 写锁同步状态加一: state + 1
  • 读锁同步状态加一: state + (1 << 16)

理清楚了ReentrantReadWriteLock的组件之间的关系和读写锁同步状态的设计之后,下面开始分析写锁和读锁的获取和释放。

1. 写锁的获取

WriteLocklock()方法直接调用了AbstractQueuedSynchronizer的模板方法acquire(),在acquire()方法中会调用自定义同步器Sync重写的tryAcquire()方法,下面看一下tryAcquire()方法的实现。

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    //c表示state
    int c = getState();
    //w表示写锁同步状态
    int w = exclusiveCount(c);
    if (c != 0) {
        //state不为0,但是写锁同步状态为0,表示读锁已经被获取
        //获取写锁时只要读锁被获取过,就不允许获取写锁
        //因为写锁是独占锁,所以持有写锁的线程不是当前线程也不允许获取写锁
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //执行到这里表示写锁重入
        setState(c + acquires);
        return true;
    }
    //非公平获取锁时writerShouldBlock()返回false
    //公平获取锁时writerShouldBlock()会调用hasQueuedPredecessors()方法
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

上述tryAcquire()方法中,在获取写锁之前会判断读锁是否被获取以及写锁是否被其它线程获取,任意一个条件满足都不允许当前线程获取写锁。同时如果写锁和读锁均没有被获取,即state为0时,还会调用writerShouldBlock()方法来实现非公平或公平锁的语义,如果是非公平锁,writerShouldBlock()方法会返回false,此时当前线程会以CAS方式修改state,修改成功则表示获取读锁成功,如果是公平锁,writerShouldBlock()方法会调用hasQueuedPredecessors()方法来判断同步队列中是否已经有正在等待获取锁资源的线程,如果有,则当前线程需要加入同步队列,后续按照等待时间越久越优先获取锁的机制来获取写锁。

2. 写锁的释放

WriteLockunlock()方法直接调用了AbstractQueuedSynchronizer的模板方法release(),在release()方法中会调用自定义同步器Sync重写的tryRelease()方法,下面看一下tryRelease()方法的实现。

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

因为写锁支持重入,所以在释放写锁时会对写锁状态进行判断,只有写锁状态为0时,才表示写锁被成功释放掉。

3. 读锁的获取

ReadLocklock()方法直接调用了AbstractQueuedSynchronizer的模板方法acquireShared(),在acquireShared()方法中会调用自定义同步器Sync重写的tryAcquireShared()方法,tryAcquireShared()方法并不完整,其最后会调用fullTryAcquireShared()方法,该方法的注释说明如下。

获取读锁同步状态的完整版本,能够实现在 tryAcquireShared()方法中未能实现的 CAS设置状态失败重试和读锁重入的功能。

JDK1.6ReentrantReadWriteLock提供了getReadHoldCount()方法,该方法用于获取当前线程获取读锁的次数,因为该方法的加入,导致了读锁的获取的逻辑变得更为复杂,下面将结合tryAcquireShared()fullTryAcquireShared()方法的实现,在抛开为实现getReadHoldCount()方法功能而新增的逻辑的情况下,给出读锁获取的简化实现代码。

final int fullTryAcquireShared(Thread current) {
    for (;;) {
        //c表示state
        int c = getState();
        //如果写锁被获取并且获取写锁的线程不是当前线程,则不允许获取读锁
        if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
            return -1;
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //安全的将读锁同步状态加1
        if (compareAndSetState(c, c + SHARED_UNIT))
            return 1;
    }
}

由上述可知,读锁在写锁被获取并且获取写锁的线程不是当前线程的情况下,不允许被获取,以及读锁的同步状态为所有线程获取读锁的次数之和。

4. 读锁的释放

ReadLockunlock()方法直接调用了AbstractQueuedSynchronizer的模板方法releaseShared(),在releaseShared()方法中会调用自定义同步器Sync重写的tryReleaseShared()方法,该方法同样在JDK1.6中加入了较为复杂的逻辑,下面给出其简化实现代码。

protected final boolean tryReleaseShared(int unused) {
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

由上述可知,只有在state为0时,即读锁和写锁均被释放的情况下tryReleaseShared()方法才会返回true,在官方的注释中给出了这样设计的原因,如下所示。

释放读锁对读线程没有影响,但是当读锁和写锁均被释放的情况下,在同步队列中等待的写线程就有可能去获取写锁。

三. Condition接口

Condition接口定义了一组方法用于配合Lock实现等待/通知模式,与之作为对比的是,用于配合synchronized关键字实现等待/通知模式的定义在java.lang.Object上的监视器方法wait()notify()等。《Java并发编程的艺术》5.6小节对两者的差异进行了对比和总结,这里直接贴过来作参考。

对比项 Object Monitor Methods Condition
当前线程释放锁并进入等待状态 支持 支持
当前线程释放锁并进入等待状态,等待过程中不响应中断 不支持 支持
当前线程释放锁并进入超时等待状态 支持 支持
当前线程释放锁并等待至将来某个时间点 不支持 支持
唤醒队列中的一个线程 支持 支持
唤醒队列中的多个线程 支持 支持

通常基于LocknewCondition()方法创建Condition对象并作为对象成员变量来使用,如下所示。

public class MyCondition {

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    
    ......
    
}

队列同步器AbstractQueuedSynchronizer的内部类ConditionObject实现了Condition接口,后续将基于ConditionObject的实现进行讨论。首先给出Condition接口定义的方法。

public interface Condition {

    void await() throws InterruptedException;

    void awaitUninterruptibly();

    long awaitNanos(long nanosTimeout) throws InterruptedException;

    boolean await(long time, TimeUnit unit) throws InterruptedException;

    boolean awaitUntil(Date deadline) throws InterruptedException;

    void signal();

    void signalAll();
    
}

上述方法的说明如下表所示。

方法 说明
await() 调用此方法的线程进入等待状态,响应中断,也可以被signal()signalAll()方法唤醒并返回,唤醒并返回前需要获取到锁资源。
awaitUninterruptibly() await(),但不响应中断。
awaitNanos() await(),并可指定等待时间,响应中断。该方法有返回值,表示剩余等待时间。
awaitUntil() await(),并可指定等待截止时间点,响应中断。该方法有返回值,true表示没有到截止时间点就被唤醒并返回。
signal() 唤醒等待队列中的第一个节点。
signalAll() 唤醒等待队列中的所有节点。

针对上面的方法再做两点补充说明:

  • 等待队列是Condition对象内部维护的一个FIFO队列,当有线程进入等待状态后会被封装成等待队列的一个节点并添加到队列尾;
  • 从等待队列唤醒并返回的线程一定已经获取到了与Condition对象关联的锁资源,Condition对象与创建Condition对象的锁关联。

下面将结合ConditionObject类的源码来对等待/通知模式的实现进行说明。await()方法的实现如下所示。

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //基于当前线程创建Node并添加到等待队列尾
    //这里创建的Node的等待状态为CONDITION,表示等待在等待队列中
    Node node = addConditionWaiter();
    //释放锁资源
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //Node从等待返回后会被添加到同步队列中
    //Node成功被添加到同步队列中则退出while循环
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //让Node进入自旋状态,竞争锁资源
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    //遍历等待队列,将已经取消等待的节点从等待队列中去除链接
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    //Node如果是被中断而从等待返回,则抛出中断异常
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

理解await()方法的整个执行流程前,先看一下等待队列的一个示意图,如下所示。

多线程学习-锁_第4张图片

Condition对象分别持有等待队列头节点和尾节点的引用,新添加的节点会添加到等待队列尾,同时lastWaiter会指向新的尾节点。

现在回到await()方法,在await()方法中,会做如下事情。

  • 首先,会基于当前线程创建Node并添加到等待队列尾,创建Node有两个注意点:1. 这里创建的Node复用了同步队列中的Node定义;2. 在创建Node前会判断等待队列的尾节点是否已经结束等待(即等待状态不为Condition),如果是则会遍历等待队列并将所有已经取消等待的节点从等待队列中去除链接;
  • 然后,当前线程会释放锁资源,并基于LockSupport.park()进入等待状态;
  • 再然后,当前线程被其它线程唤醒,或者当前线程被中断,无论哪种方式,当前线程对应的Node都会被添加到同步队列尾并进入自旋状态竞争锁资源,注意,此时当前线程对应的Node还存在于等待队列中;
  • 再然后,判断当前线程对应的Node是否是等待队列尾节点,如果不是则触发一次清除逻辑,即遍历等待队列,将已经取消等待的节点从等待队列中去除链接,如果是等待队列尾节点,那么当前线程对应的Node会在下一次创建Node时从等待队列中被清除链接;
  • 最后,判断当前线程从等待返回的原因是否是因为被中断,如果是,则抛出中断异常。

上面讨论了等待的实现,下面再结合源码看一下通知的实现。首先是signal()方法,如下所示。

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

signal()方法可知,调用signal()方法的线程需要持有锁,其次signal()方法会唤醒等待队列的头节点,即可以理解为唤醒等待时间最久的节点。下面再看一下signalAll()方法,如下所示。

public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}

可以发现,signalAll()signal()方法大体相同,只不过前者最后会调用doSignalAll()方法来唤醒所有等待节点,后者会调用doSignal()方法来唤醒头节点,下面以doSignal()方法进行说明。

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

实际就是在transferForSignal()方法中将头节点添加到同步队列尾,然后再调用LockSupport.unpark()进行唤醒。

四. LockSupport

在本篇文章的最后,对java.util.concurrent.locks包中的一个重要工具类LockSupport进行说明。LockSupport提供了一组静态方法用于阻塞/唤醒线程,方法签名如下所示。

public static void park()

public static void park(Object blocker)

public static void parkNanos(long nanos)

public static void parkNanos(Object blocker, long nanos)

public static void parkUntil(long deadline)

public static void parkUntil(Object blocker, long deadline)

public static void unpark(Thread thread)

LockSupport的方法进行整理如下。

方法 说明
park() 将调用park()方法的线程阻塞,响应中断。
parkNanos() 将调用parkNanos()方法的线程阻塞,并指定阻塞时间,响应中断。
parkUntil() 将调用parkUntil()方法的线程阻塞,并指定截止时间点,响应中断。
unpark(Thread thread) 唤醒传入的线程。

现在已知让线程睡眠(阻塞或等待)的方式有四种,分别是Thread.sleep(time)LockSupport.park()Object.wait()Condition.await(),在本篇文章的最后,对上述四种方式进行一个简单对比,如下表所示。

方式 说明
Thread.sleep(time) 调用该方法必须指定线程睡眠的时间,睡眠中的线程可以响应中断并抛出中断异常,调用该方法时不需要线程持有锁资源,但是持有锁资源的线程调用该方法睡眠后不会释放锁资源
LockSupport.park() 调用该方法的线程会被阻塞,被阻塞中的线程可以响应中断但不会抛出中断异常,调用该方法时不需要线程持有锁资源,但是持有锁资源的线程调用该方法睡眠后不会释放锁资源
Object.wait() 调用该方法的线程会进入等待状态,等待状态中的线程可以响应中断并抛出中断异常,调用该方法时需要线程已经持有锁资源,调用该方法后会释放锁资源
Condition.await() 调用该方法的线程会进入等待状态,等待状态中的线程可以响应中断并抛出中断异常,调用该方法时需要线程已经持有锁资源,调用该方法后会释放锁资源

总结

本篇文章主要对重入锁读写锁的使用和原理进行了学习,重入锁读写锁的实现均是基于队列同步器AbstractQueuedSynchronizer。然后又对Condition的使用和原理进行了学习,通过Condition进入等待状态的线程会在等待队列上进行等待,被唤醒或中断时又会进入同步队列加入到对锁资源的竞争中,只有获取到了锁资源才能从等待状态返回。最后对LockSupport工具类进行了一个简单说明,并针对线程进入睡眠的四种方式做了一个简单对比。

你可能感兴趣的:(多线程学习-锁)