• 【并发编程-2】JUC-1


    Lock

    互斥锁:

    1、锁的可重入性:

    当一个线程调用object.lock()获取到锁,进入临界区后,还可以再次调用object.lock()。

    通常锁都应该设计为可重入,否则就会发生死锁(比如lock方法中调用另一个lock方法)。  比如synchronized就是可重入,在一个synchronized方法中可以继续调用另一个synchronized方法。

    2、Lock:

    基本认识:

    1. public interface Lock {
    2. //一直等待直到获取锁
    3. void lock();
    4. //可以被中断,不再等待
    5. void lockInterruptibly() throws InterruptedException;
    6. //获取不到马上放弃
    7. boolean tryLock();
    8. //在指定时间内不断尝试获取
    9. boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    10. void unlock();
    11. Condition newCondition();
    12. }

    ReentrantLock实现Lock接口,它的实现都在Sync类中:

    1. public class ReentrantLock implements Lock, java.io.Serializable {
    2. private final Sync sync;
    3. public ReentrantLock() {
    4. sync = new NonfairSync();
    5. }
    6. public ReentrantLock(boolean fair) {
    7. sync = fair ? new FairSync() : new NonfairSync();
    8. }
    9. public void lock() {
    10. sync.acquire(1);
    11. }
    12. public void unlock() {
    13. sync.release(1);
    14. }// ...
    15. }

    我们说了锁需要作用在一个共同的对象,ReentrantLock中便是Sync对象。Sync是一个抽象类,它有两个子类FairSyncNonfairSync,分别对应公平锁和非公平锁。

    如果一个线程来了不排队,直接去抢锁,就是非公平。 这也是默认的构造方法,目的是为了提高效率。

    锁的基本原理:

    Sync 的父类 AbstractQueuedSynchronizer,被称作队列同步器(AQS),它的父类是AbstractOwnableSynchronizer(AOS)。  看命名,都是Synchronizer结尾,因此,此锁具有备synchronized 功能,可以阻塞一个线程。    为了实现一把具有阻塞或唤醒功能的锁,需要几个要素:
    1. 需要一个state变量,标记该锁的状态。state变量至少有两个值:01。对state变量的操作, 使用CAS(Compare and Swap)保证线程安全。
    2. 需要记录当前是哪个线程持有锁。
    3. 需要底层支持对一个线程进行阻塞唤醒操作。
    4. 需要有一个队列维护所有阻塞的线程。这个队列也必须是线程安全的无锁队列,也需要使用 CAS

    针对1和2,Sync的两个父类AOS、AQS已有对应的实现:

    1. public abstract class AbstractOwnableSynchronizer implements
    2. java.io.Serializable {
    3. private transient Thread exclusiveOwnerThread; // 记录持有锁的线程
    4. }
    5. public abstract class AbstractQueuedSynchronizer extends
    6. AbstractOwnableSynchronizer implements java.io.Serializable {
    7. private volatile int state; // 记录锁的状态,通过CAS修改state的值。
    8. }

    state可以大于1,例如,同样一个线程,调用5lockstate会变成5;然后调用5unlockstate减为0。

    • state=0时,没有线程持有锁,exclusiveOwnerThread=null
    • state=1时,有一个线程持有锁,exclusiveOwnerThread=该线程;
    • state > 1时,说明该线程重入了该锁;

    针对第3点:Unsafe类提供了阻塞或唤醒线程的一对操作,park/unpark。 LockSupport工具类进行了进一步封装:

    1. public class LockSupport {
    2. // ...
    3. private static final Unsafe U = Unsafe.getUnsafe();
    4. public static void park() {
    5. U.park(false, 0L);
    6. }
    7. public static void unpark(Thread thread) {
    8. if (thread != null)
    9. U.unpark(thread);
    10. }
    11. }

    所以可以使用该工具来实现:当一个线程中调用park(),该线程就会被阻塞; 然后另一个线程中调用

    unpark(Thread thread),传入一个被阻塞的线程,就可以将其唤醒(notify只能唤醒一个不确定的线程)。

    针对第4点:AOS这个父类中,还实现了一个双向链表的阻塞队列,存放阻塞的线程:

    1. public abstract class AbstractQueuedSynchronizer {
    2. // ...
    3. static final class Node {
    4. volatile Thread thread; // 每个Node对应一个被阻塞的线程
    5. volatile Node prev;
    6. volatile Node next;
    7. // ...
    8. }
    9. private transient volatile Node head;
    10. private transient volatile Node tail;
    11. // ...
    12. }

    head指向第一个Node的位置,tail指向下一个要添加的位置。 初始为空,head和tail都指向null,入队时往tail处添加,tail往后移指向下一个null;出队时,将head指向的Node移除,head往后移。   所以,当head=tail=null时,代表队列为空。

    ReentrantLock在公平性和非公平性上的实现差异:

    非公平锁:如果state为0,直接将当前线程设置为锁持有者,并设置state的值;  如果state不是0,但锁的持有者是当前线程,直接更新state。

    公平锁:如果state为0,要看看队列中有没有其他等待线程,如果没有才将当前线程设置为持有者;   如果state不为0,和上面一样。

    问:基于非公平锁,先后有多个线程请求,是依次获取锁吗?

    不一定。   非公平锁的作用在于,线程来获取锁的时候,恰好没有其他线程持有锁,那么可以优先获取锁。。 否则,也需要加入队列中,依次等待获取。

    阻塞队列与唤醒机制:

    lock.lock()

    调用lock.lock(),最终会到AQS中的核心方法,acquire:

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

    解析:

    1. 如果tryAcquire没有获取到锁,就调用acquireQueued(加入队列并阻塞)。
    2. 在acquireQueued中先调用addWaiter: 为当前线程生成一个Node,然后把Node放入双向链表的尾部。 此时还未阻塞,需要调用acquireQueued。  线程一旦进入acquireQueued方法,就会无限期阻塞,即使其他线程调用interrupt也无法唤醒,直到方法结束,也就是它获取到锁那一刻才会被唤醒。此时,会删除队列的第一个Node。

            阻塞方法parkAndCheckInterrupt,其实就是调用了LockSupport.park方法。另外,在队列中会一直循环尝试获取锁,当前一个节点是头节点时,才有可能获取成功。 

           3.此外,acquireQueued有个返回值,代表当前线程有没有中断标志(在阻塞期间,可能有其他线程给他发送过中断信号,但此时无法响应),如果有会调用selfInterrupt(),自己给自己发送一下中断信号,重新响应一下中断。

    lock.unlock()

    unlock 不区分公不公平,直接释放锁后,唤醒head节点,让其获取锁。
    代码逻辑在AQS中:
    1. public final boolean release(int arg) {
    2. if (tryRelease(arg)) {
    3. Node h = head;
    4. if (h != null && h.waitStatus != 0)
    5. unparkSuccessor(h);
    6. return true;
    7. }
    8. return false;
    9. }

    如果尝试释放锁成功,就调用unparkSuccessor唤醒头节点,让其获取锁。

    tryRelease中,就是判断当前线程是否持有锁,并state的值减到0为止。 参数中的releases,在上层调用unlock时默认传的1,因此,lock了几次,就要调用unlock几次,才能真正的释放锁。

    1. protected final boolean tryRelease(int releases) {
    2. int c = getState() - releases;
    3. if (Thread.currentThread() != getExclusiveOwnerThread())
    4. throw new IllegalMonitorStateException();
    5. boolean free = false;
    6. if (c == 0) {
    7. free = true;
    8. setExclusiveOwnerThread(null);
    9. }
    10. setState(c);
    11. return free;
    12. }

    lockInterruptibly ():

    ReentrantLock除了lock()方法,还可以调用lockInterruptibly (),此方法可以响应中断。 底层调用了AQS中的acquireInterruptibly:

    1. public final void acquireInterruptibly(int arg)
    2. throws InterruptedException {
    3. if (Thread.interrupted())
    4. throw new InterruptedException();
    5. if (!tryAcquire(arg))
    6. doAcquireInterruptibly(arg);
    7. }

    里面的tryAcquire只是个模版方法,分别被FairSync和 NonfairSync实现。  当tryAcquire中没有获取到锁时,会执行doAcquireInterruptibly,判断如果有其他线程发了中断信号,则抛出异常,不会一直阻塞。

    tryLock():

    ReentrantLock中,其实用的比较多的,还有tryLock。 它是基于非公平锁的tryAcquire实现逻辑,如果拿到锁就返回true,否则返回false,不会一直阻塞等待。

    读写锁:

    与上面的互斥锁ReentrantLock相比,读写锁(ReadWriteLock)也是实现了Lock接口。 但是,它可以满足:读读不互斥(一个线程获取了读锁,其他线程还能获取读锁),读写互斥(一个线程获取了读锁,其他线程就不能获取写锁。 反之亦然。),写写互斥(一个线程获取了写锁,其他线程不能再获取写锁)。 ReadWriteLock也是个接口,具体逻辑由ReentrantReadWriteLock实现(RRWL)。  而在RRWL中,有两个内部类,读锁与写锁,也是实现了Lock。 因此,在使用读写锁时,要分别获取读锁与写锁:

    ReadWriteLock readWriteLock = new ReentrantReadWriteLock ();
    Lock readLock = readWriteLock . readLock ();
    readLock . lock ();
    // 进行读取操作
    readLock . unlock ();
    Lock writeLock = readWriteLock . writeLock ();
    writeLock . lock ();
    // 进行写操作
    writeLock . unlock ();

    实际上,两把锁都只是同一把锁的两个视图而已,他们只有一个sync对象, 所以,在同一个对象中,也才能实现读写互斥的逻辑:当对象中state=0时,说明没有线程持有锁;当state != 0时,要么有线程持有读锁,要么有线程持有写锁。再通过 sharedCount(state)和exclusiveCount(state)判断到底是读线程还是写线程持有了该锁。

    从构造方法可以看出,共用了一个sync,sync也同样实现了公平,非公平的逻辑,并继承AQS。
    (注意,这里的sync,是ReentrantReadWriteLock中的内部类; 上面说的互斥锁中的sync,是ReentrantLock的内部类。 包括公平与非公平实现类,也都各是各的, 只不过sync都是继承了同一个AQS)
    1. public ReentrantReadWriteLock(boolean fair) {
    2. sync = fair ? new FairSync() : new NonfairSync();
    3. readerLock = new ReadLock(this);
    4. writerLock = new WriteLock(this);
    5. }

    因此,两把锁的逻辑实现,其实就是调用了sync的方法(AQS的方法,由多个sync继承实现):acquire/release(互斥锁和读写锁的写锁)、acquireShared/releaseShared(读写锁的读锁),公平和非公平(是否需要阻塞)在sync不同的子类中实现:

    1. static final class NonfairSync extends Sync {
    2. private static final long serialVersionUID = -8159625535654395037L;
    3. // 写线程抢锁的时候是否应该阻塞
    4. final boolean writerShouldBlock() {
    5. // 写线程在抢锁之前永远不被阻塞,非公平锁
    6. return false;
    7. }
    8. // 读线程抢锁的时候是否应该阻塞
    9. final boolean readerShouldBlock() {
    10. // 读线程抢锁的时候,当队列中第一个元素是写线程的时候要阻塞(即便是非公平,也要排在写线程之后)
    11. return apparentlyFirstQueuedIsExclusive();
    12. }
    13. }
    1. static final class FairSync extends Sync {
    2. private static final long serialVersionUID = -2274990926593161451L;
    3. // 写线程抢锁的时候是否应该阻塞
    4. final boolean writerShouldBlock() {
    5. // 写线程在抢锁之前,如果队列中有其他线程在排队,则阻塞。公平锁
    6. return hasQueuedPredecessors();
    7. }
    8. // 读线程抢锁的时候是否应该阻塞
    9. final boolean readerShouldBlock() {
    10. // 读线程在抢锁之前,如果队列中有其他线程在排队,阻塞。公平锁
    11. return hasQueuedPredecessors();
    12. }
    13. }

    对于公平,都需要排队获取锁;   对于非公平,就要分情况了:

    1. 写锁:当state=0没有其他线程持有锁(或者state!=0,但是持锁的是自己),直接获取锁,不用排队。
    2. 读锁:如果队列的第一个是写线程,先让写线程获取锁,否则可能导致写线程一直获取不到。

    Condition:

    Condition本身也是一个接口,其功能和wait/notify类似,必须同Lock一起使用。 所以,Lock接口中,有一个和创建Conditon的方法。

    1. public interface Lock {
    2. void lock();
    3. void lockInterruptibly() throws InterruptedException;
    4. // 所有的Condition都是从Lock中构造出来的
    5. Condition newCondition();
    6. boolean tryLock();
    7. boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    8. void unlock();
    9. }
    1. public interface Condition {
    2. void await() throws InterruptedException;
    3. boolean await(long time, TimeUnit unit) throws InterruptedException;
    4. long awaitNanos(long nanosTimeout) throws InterruptedException;
    5. void awaitUninterruptibly();
    6. boolean awaitUntil(Date deadline) throws InterruptedException;
    7. void signal();
    8. void signalAll();
    9. }

    我们知道,如果使用wait notify,是无差别唤醒。 假如只有一个生产者和一个消费者还好,如果有多个,可能出现生产者通知生产者、消费者通知消费者的问题(如果没有使用notifyAll,还可能出现死锁)。 而使用Condition,就可以精确唤醒,具体用法就是在Lock中new 两个Condition,分别给生产者和消费者使用(生产者使用condition1来等待,也可以唤醒condition2;  消费者则反过来)。

    StampedLock:

    StampedLock是在JDK8中新增的,可以支持读写不互斥。 

    ReentrantReadWriteLock 采用的是 悲观读 的策略,当第一个读线程拿到锁之后,
    第二个、第三个读线程还可以拿到锁,使得写线程一直拿不到锁,可能导致写线程 饿死 。虽然在其公平或非公平的实现中,都尽量避免这种情形,但还有可能发生。
    StampedLock 引入了 乐观读 策略,读的时候不加读锁,读出来发现数据被修改了,再升级为 悲观读” ,相当于降低了 的地位,把抢锁的天平往 的一方倾斜了一下,避免写线程被饿死。
  • 相关阅读:
    【分布式技术专题】「分布式技术架构」MySQL数据同步到Elasticsearch之N种方案解析,实现高效数据同步
    基于Python计算PLS中的VIP值(变量投影重要性分析法)
    最新总结MySQL核心知识点
    数智人才培养丨学生学徒阶段成果分享
    计算机毕业设计 基于HTML+CSS+JavaScript响应式网站健身7页,适配手机端,响应式页面,页面精美,使用bootstrap 框架
    Redux系列实现异步请求
    OpenHarmony 唤醒花屏问题
    使用 Apache ECharts 实现圣都装饰的延期日历图
    Latex IEEE模板导入中文问题
    ZCU102启动镜像(详细版)
  • 原文地址:https://blog.csdn.net/growing_duck/article/details/133994282