乐趣区

关于多线程:多线程学习锁

前言

本篇文章将对基于 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的类图如下所示。

ReentrantLock有三个动态外部类,其中 Sync 继承于 AbstractQueuedSynchronizer,而后FairSyncNonfairSync继承于 Sync,因而SyncFairSyncNonfairSync均是 ReentrantLock 组件中的自定义同步器,且 FairSync 提供偏心获取锁机制,NonfairSync提供非偏心获取锁机制。偏心和非偏心获取锁机制当初暂且不谈,上面先看一下 SyncFairSyncNonfairSync实现了哪些办法,如下所示。

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<String, String> 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 的类图。

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() 办法的整个执行流程前,先看一下期待队列的一个示意图,如下所示。

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 工具类进行了一个简略阐明,并针对线程进入睡眠的四种形式做了一个简略比照。

退出移动版