【JUC】Lock全解读

0、什么是Lock锁

java.util.concurrent.locks.Lock 是一个类似于synchronized 块的线程同步机制。但是 Lock比 synchronized 块更加灵活。Lock是个接口,有个实现类是ReentrantLock。

1、自定义Lock锁

当我们在学习Lock源码时,我们重点关注什么?

两个示例代码告诉我们答案:

1.1 SelfLock.java

package com.zyhstu;

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

/**
 * @author DarkSky
 * @version 1.0
 */
public class SelfLock implements Lock {

    // AQS呢?如何使用?
    private static class Sync extends AbstractQueuedSynchronizer {
        // 加锁的时候用
        // AQS使用一个 int 成员变量 `state` 来表示同步状态:0 - 未加锁;1 - 已加锁
        public boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, acquires)) {//预期值 更新值
                setExclusiveOwnerThread(Thread.currentThread());// 设置当前拥有锁的线程
                return true;
            }

            return false;
        }

        // 解锁的时候用
        public boolean tryRelease(int releases) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            // 为什么不用CAS(compareAndSetState)?不用,当前线程释放锁,说明当前线程持有锁
            setState(0);// 状态改变为解锁

            return true;
        }

        // 创建condition   .wait()  .notified()
        Condition newCondition() {
            return new ConditionObject();// new了一个condition的子类
        }

        // 添加方法:是否锁定
        public boolean isLocked() {
            return getState() == 1;
        }
    }

    // 初始化Sync
    private final Sync sync = new Sync();

    @Override
    public void lock() {// 加锁
        //sync.tryAcquire();
        // 如果lock失败,应该CAS一直循环监听,调用错误
        sync.acquire(1);// 模板方法(父类的方法)
    }

    @Override
    public boolean tryLock() {// 尝试加锁
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isLocked();
    }

    // 是否有等待线程
    public boolean hasQueuedThread() {
        return sync.hasQueuedThreads();
    }
}

当我们自定义一个Lock锁时

(1)首先要继承 Lock 接口,实现它的 6 个抽象类

image-20221013212102125

(2)创建一个继承AQS(AbstractQueuedSynchronizer)的类,在实现上述6个抽象类的过程中,发现需要重写tryAcquire(int acquires),tryRelease(int releases),通过返回一个Condition的子类Conditionobject来实现上述方法

由此,我们需要在接下来源码的学习过程中,重点关注tryAcquire()tryAcquireNanos()acquireInterruptibly()release()ConditionObject.class等相关内容

1.2 测试代码:SelfLockTest.java

package com.zyhstu;

import java.util.concurrent.locks.Lock;

/**
 * @author DarkSky
 * @version 1.0
 */
public class SelfLockTest {

    static Lock lock = new SelfLock();

    public static void main(String[] args) throws InterruptedException {
        Thread A = new Thread(() -> {
           testLock();
        });

        Thread B = new Thread(() -> {
            testLock();
        });
        A.setName("I am A");
        A.start();
        Thread.sleep(100);
        B.setName("I am B");
        B.start();
    }


    public static void testLock() {
        lock.lock();
        try {
            System.out.println("获取锁了 : " + Thread.currentThread().getName());

            while (true) {} // 一直持有锁,不释放
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}

输出:A获得线程后便会阻塞

2、Lock锁的由来及特性及API

2.1 Lock 锁概述

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问(读)共享资源,比如读写锁)。

在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。

在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。

不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。

2.2 Lock 特性

image-20221013214413028

2.3 Lock API

image-20221013214417230

3、AQS底层原理

3.1 什么是AQS

AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁的分配机制,这个机制AQS是通过CLH队列实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig, Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个节点(Node)来实现锁分配。

AQS的原理图:

image-20221013161035883

3.2 队列同步器的设计

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量satte表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,AQS使用CAS对该同步状态进行原子操作,实现对其值的改变,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。

子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。

3.3 同步器可重写的方法

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。

  • getState():获取当前同步状态。
  • setState(int newState):设置当前同步状态。
  • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

同步器可重写的方法:

image-20221015150054552

3.4 FIFO同步队列

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

节点是构成同步队列(等待队列,在Condition中将会介绍)的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部,同步队列的基本结构如图所示。

image-20221015150731495

同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。试想一下,当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。

3.4.1 设置尾结点

当线程获取锁失败时,CAS设置为尾结点

image-20221015152814178

为什么同步队列AQS在设置尾结点时需要使用CAS?

三个线程abc,a持有锁,bc竞争失败,需要添加到 AQS 的同步队列尾端。此时bc同时竞争 tail 节点,这个时候就是要保证线程安全性,正确添加尾结点,需要使用 CAS操作。

3.4.2 设置头结点

设置首节点是通过获取同步状态成功的线程来完成的(**谁抢到锁,谁设置头结点 **),由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。

image-20221015153409455

4、ReentrantLock(可重入锁)源码解读

4.1 读源码

读 ReentrantLock,就是在读 AQS;

两者相辅相成,缺一不可;

只有结合读,才能达到最好效果。

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {
    static ReentrantLock lock = new ReentrantLock();
    static ReentrantLock lock1 = new ReentrantLock(true);

    public static void main(String[] args) throws InterruptedException {
        /**
        * AQS-compareAndSetState
        * acquire
        * addWaiter
        * enq
        * acquireQueued
        * shouldParkAfterFailedAcquire
        * parkAndCheckInterrupt
        * cancelAcquire
        */
        // acquire 方法是 aqs提供的模板方法,是为了进行锁的获取;tryAcquire 方法是aqs提供的可以复写的方法,主要是完成了加锁
        //状态变化的逻辑(state);addWaiter 将我们的获取失败的线程放到我们的同步队列里;enq 如果addwaiter第一次没有成功,就
        //进行死循环添加;acquireQueued:这部分其实是通过循环的自我检查,如果当前节点的pred节点是头节点,那么就尝试获取锁;
        //如果不是头节点,就调用 shouldParkAfterFailedAcquire 方法,判断pred节点是否为 SIGNAL 状态,如果是signal状态,自己就好好
        //的等着;如果是 cancell状态,就移除cancell的节点。其他状态的节点,会通过cas操作替换为 SIGNAL状态。
        //SIGNAL:等待被通知状态,如果pre节点是这个状态,那么当前节点就会进行park操作
        //Cancelled : 一个取消的线程状态。这个状态的线程会被移除
        lock.lock();

        /**
         *AQS - tryAcquireNanos
         * doAcquireNanos
         * shouldParkAfterFailedAcquire
         *
         */
        //tryLock() 为了进行一次性的获取锁,如果获取成功则成功,如果失败则失败
        //tryLock(1,null) 在超时时间以内,循环获取锁、
        lock.tryLock();
        lock.tryLock(1,null); //InterruptedException

        /**
         * AQS -acquireInterruptibly
         */
        // lockInterruptibly方法,是一个支持中断的加锁方式。他与 lock.tryLock(1,null) 这个有什么区别?
        // 相同点:都支持中断
        //不同点: lockInterruptibly方法仅仅支持中断;不支持超时。lock.tryLock(1,null)即支持超时,也支持超时内的时间中断
        lock.lockInterruptibly();
        lock.isHeldByCurrentThread();

        /**
         * AQS - release
         * unparkSuccessor
         *
         */
        // 调用tryRelease,直到释放掉所有的锁(state =0),因为考虑有重入的情况。然后唤醒后继(unparkSuccessor)线程让他进行锁竞争。
        lock.unlock();

        /**
         * AQS - hasQueuedPredecessors // 如果新线程来了,它在queue里吗?没在q里就没有pre节点。没办法,直接加入tail
         * 他是公平锁的关键方法
         */
        lock1.lock();

    }
}

4.2 Node状态及获取锁的流程图

image-20221015182216106

image-20221015182223627

【所谓获取同步状态就是获取锁】

4.3 公平锁与非公平锁

(1)synchronized 是一个非公平锁

(2)Lock 分为非公平锁(默认)和公平锁。

  • 非公平锁:当我们的线程在同步队列里排队完成之后,获取锁的时候,如果有其他过来的新的线程来竞争锁,那么当前的排队的线程可能会被插队(当前线程可能竞争不过新来的线程,导致自己竞争失败)。这是不公平的,当前线程已经在同步队列中排了好长时间队,反而被新来的线程抢走了
  • 公平锁:获取锁的时候,在这个时间点上如果有其他新的线程来竞争锁,那么新的线程会直接加入到同步队列之中(cas setTail)

(3)性能比较(公平和非公平):肯定是非公平锁性能更高。(公平锁,每个线程进来都要加到同步队列里,都要进行我们源码中的一系列的操作;公平锁会有更多的上下文切换, 挂起,park())

(4)非公平锁容易造成线程饥饿。(会被插队,极限情况考虑,如果一直被插队,同步队列里的其他线程就等着呗,饥饿。)

(5)很多情况我们在进行实战开发的时候,如果要限定我们的线程的访问先后顺序,就要使用公平锁了。

5、ReentrantReadWriteLock实现原理

5.1 概述

我们知道,对于一个数据,不管是几个线程同时读都不会出现任何问题,但是写就不一样了,几个线程对同一个数据进行更改就可能会出现数据不一致的问题,因此想出了一个方法就是对数据加锁,这时候出现了一个问题:
线程写数据的时候加锁是为了确保数据的准确性,但是线程读数据的时候再加锁就会大大降低效率,这时候怎么办呢?那就对写数据和读数据分开,加上两把不同的锁,不仅保证了正确性,还能提高效率。

在没有读写锁支持的(Java 5之前)时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。

读读并发,读写,写写互斥

一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的实现是ReentrantReadWriteLock,它提供的特性如表:

image-20221015192309307

锁降级:

20201015074118850

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指持住(当前拥有的)写锁再获取到读锁随后释放(先前拥有的)写锁的过程

锁降级中读锁的获取是否必要呢?

答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

5.2 读写状态设计

读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。回想ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。

如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写,划分方式如图:

image-20221015192523031

当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。读写锁是如何迅速确定读和写各自的状态呢?

假设当前同步状态值为S,get和set的操作如下:

(1)获取写状态:

S&0x0000FFFF:将高16位全部抹去

(2)获取读状态:

S>>>16:无符号补0,右移16位

(3)写状态加1:

S+1

(4)读状态加1:

​ S+(1<<16)即S + 0x00010000

在代码层的判断中,如果S不等于0,当写状态(S&0x0000FFFF),而读状态(S>>>16)大于0,则表示该读写锁的读锁已被获取。

6、ReentrantReadWriteLock源码解读

ReentrantReadWriteLock整体结构

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {

    /** 读锁 */
    private final ReentrantReadWriteLock.ReadLock readerLock;

    /** 写锁 */
    private final ReentrantReadWriteLock.WriteLock writerLock;

    final Sync sync;
    
    /** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
    public ReentrantReadWriteLock() {
        this(false);
    }

    /** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    /** 返回用于写入操作的锁 */
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    
    /** 返回用于读取操作的锁 */
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }


    abstract static class Sync extends AbstractQueuedSynchronizer {}

    static final class NonfairSync extends Sync {}

    static final class FairSync extends Sync {}

    public static class ReadLock implements Lock, java.io.Serializable {}

    public static class WriteLock implements Lock, java.io.Serializable {}
}

6.1 类的继承关系

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {}

ReentrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock接口定义了获取读锁和写锁的规范,具体需要实现类去实现;同时其还实现了Serializable接口,表示可以进行序列化,在源代码中可以看到ReentrantReadWriteLock实现了自己的序列化逻辑。

6.2 类的内部类

ReentrantReadWriteLock有五个内部类,五个内部类之间也是相互关联的。内部类的关系如下图所示。

img

如上图所示,Sync继承自AQS、NonfairSync继承自Sync类、FairSync继承自Sync类(通过构造函数传入的布尔值决定要构造哪一种Sync实例);ReadLock实现了Lock接口、WriteLock也实现了Lock接口。

6.3 写锁的获取与释放

WriteLock类中的 lock 和 unlock 方法:

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

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

可以看到就是调用的独占式同步状态的获取与释放,因此真实的实现就是Sync的 tryAcquire和 tryRelease。

6.3.1 写锁的获取

写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(高16位读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。

【源码:tryAcquire方法】

protected final boolean tryAcquire(int acquires) {
    //当前线程
    Thread current = Thread.currentThread();
    //获取同步状态(包含读线程与写线程)
    int c = getState();
    //写线程数量(即获取独占锁的重入数)
    int w = exclusiveCount(c);

    //当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁
    if (c != 0) {
        // 当前state不为0,此时:如果写锁状态为0说明读锁此时被占用返回false;
        // 如果写线程数量为0或写锁没有被当前线程持有返回false
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;

        //判断同一线程获取写锁是否超过最大次数(65535),支持可重入
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //更新状态
        //此时当前线程已持有写锁,现在是重入,所以只需要修改锁的数量即可。
        setState(c + acquires);
        return true;
    }

    //到这里说明此时c=0,读锁和写锁都没有被获取
    //writerShouldBlock表示是否阻塞
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;

    //设置锁为当前线程所有
    setExclusiveOwnerThread(current);
    return true;
}

流程图如下:

img

(1)首先获取 当前锁状态,写线程数量。判断同步状态 state 是否为0,如果同步状态不为0,说明已经有其他线程获取了读锁或写锁,则执行(2),为0则执行(4)

(2)如果当前锁状态,即同步状态(state)不为0,同时写状态为0,即写线程的数量为0,说明读锁此时被其他线程占用,所以当前线程不能获取写锁,返回false。或者同步状态不为0(即锁未被其他线程持有)的同时,写状态也不为0,但是获取写锁的线程不是当前线程,则当前线程也不能获取写锁。

(3)判断当前线程是否写状态超过了最大次数,若超过则抛出异常,反之则更新同步状态,返回true

(4)如果同步状态为0,此时读锁和写锁都没有被持有,判断写线程是否需要阻塞(公平锁和非公平锁实现不同),在非公平策略下总是不会被阻塞,在公平策略下回进行判断(判断同步队列中是否有等待时间更长的线程,即新节点的前面是否存在节点。若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回 true ,失败则说明锁被别的线程抢去了,返回 false 。如果需要阻塞也返回 false。

(5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回 true

注意:tryAcquire方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断(如果我想写,必须确保没有读线程,也没有写线程)。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见(要可见,即排他),因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。

在线程持有读锁的情况下,该线程不能获取写锁(即读锁不能升级为写锁),在线程持有写锁的情况下,该线程可以继续获取读锁(即写锁可以降级为读锁)。因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁(独占);而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。

6.3.2 写锁的释放

【源码:tryRelease方法】

protected final boolean tryRelease(int releases) {
    //若锁的持有者不是当前线程,抛出异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //更新写锁状态
    int nextc = getState() - releases;
    //如果独占模式重入数为0了,说明独占模式被释放
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        //若写锁的新线程数为0,则将锁的持有者设置为null
        setExclusiveOwnerThread(null);
    //设置写锁的新线程数
    //不管独占模式是否被释放,更新独占重入数
    setState(nextc);
    return free;
}

此方法用于释放写锁资源,首先会判断该线程是否为独占线程,若不为独占线程,则抛出异常,否则,计算释放资源后的写锁的数量,若为0,表示成功释放,资源不将被占用,否则,表示资源还被占用。其方法流程图如下

img

6.4 读锁的获取与释放

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。

获取读锁的实现从Java 5到Java 6变得复杂许多,主要原因是新增了一些功能,例如getReadHoldCount()方法,作用是返回当前线程获取读锁的次数。

读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中(A 和 B 线程获取读锁,A线程重入了3次, B线程只有1次,那么对于我们读锁的读状态 = 3+1 = 4;A线程的 ThreadLocal保存了 3,B线程的ThreadLocal保存了1),由线程自身维护,这使获取读锁的实现变得复杂。

类似于写锁,读锁的lock和unlock的实际实现对应Sync的 tryAcquireShared 和 tryReleaseShared方法。

6.4.1 读锁的获取

protected final int tryAcquireShared(int unused) {//返回1获取锁成功,-1获取失败
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 获取状态
    int c = getState();

    //如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败,因为存在锁降级
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 读锁数量(可分享的数量)
    int r = sharedCount(c);
    /*
     * readerShouldBlock():读锁是否需要等待(公平锁原则)
     * r < MAX_COUNT:持有线程小于最大数(65535)
     * compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
     */
     // 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        //r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
        if (r == 0) { // 读锁数量为0
            // 设置第一个读线程
            firstReader = current;
            // 读线程占用的资源数为1
            firstReaderHoldCount = 1;
        } else if (firstReader == current) { // 当前线程为第一个读线程,表示第一个读锁线程重入
            // 占用资源数加1
            firstReaderHoldCount++;
        } else { // 读锁数量不为0并且不为当前线程,多个线程有读锁
            // 获取计数器
            HoldCounter rh = cachedHoldCounter;
            // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
            if (rh == null || rh.tid != getThreadId(current))
                // 获取当前线程对应的计数器
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0) // 计数为0
                //加入到readHolds中
                readHolds.set(rh);
            //计数+1
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);// 死循环获取锁
}

在使用tryAcquireShared(int unused)方法时,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁(锁降级)。

读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)

7、LockSupport工具

当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作。LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。

LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。Park有停车的意思,假设线程为车辆,那么park方法代表着停车,而unpark方法则是指车辆启动离开,这些方法以及描述如表:

image-20221016163040791

在Java 6中,LockSupport增加了park(Object blocker)、parkNanos(Object blocker,long nanos)和parkUntil(Object blocker,long deadline)3个方法,用于实现阻塞当前线程的功能,其中参数blocker是用来标识当前线程在等待的对象(以下称为阻塞对象),该对象主要用于问题排查和系统监控。

下面的示例中,将对比parkNanos(long nanos)方法和parkNanos(Object blocker,long nanos)方法来展示阻塞对象blocker的用处,代码片段和线程dump(部分)如下图所示。

从下图线程dump结果可以看出,代码片段的内容都是阻塞当前线程10秒,但从线程dump结果可以看出,有阻塞对象的parkNanos方法能够传递给开发人员更多的现场信息。

image-20221016163146844

8、Condition(等待队列) 原理+使用+源码

8.1 Condition 概述

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

通过对比Object的监视器方法和Condition接口,可以更详细地了解Condition的特性,对比项与结果下所示。

Object 的监视器方法与 Condition 接口的对比

image-20221016163350990

Condition常用API

void await(): 当前线程从运行状态进入等待状态或者中断,直到被通知唤醒。

boolean await(long time, TimeUnit unit);当前线程进入等待状态,直到被通知、中断或者超时

boolean awaitUntil(Date deadline)当前线程进入等待状态,直到被通知、中断或者到达指定的时间。到达指定的时间返回 false,否则返回true(还没有导致指定时间就被唤醒

void signal():唤醒一个等待在Condition上的线程,但是必须获得与该Condition相关的锁

void signalAll():唤醒所有等待在Condition上的线程,但是必须获得与该Condition相关的锁

8.2 Condition 的使用

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

public class LockCondition<T> {
    private Object[] items;
    // 添加的下标,删除的下标和数组当前数量
    private int addIndex, removeIndex, count;
    private Lock lock = new ReentrantLock(); //创建锁
    private Condition notEmpty = lock.newCondition();//创建condition。 1个等待队列
    private Condition notFull = lock.newCondition();//创建condition   2个等待队列
    // 是不是我们的阻塞队列都是如此实现的呢? 后续我们会对队列进行一个分析,也会看源码,到时候你就能看到
    // 队列的头和队列的尾都是分别创建了一个condition,就是为了将我们的队列的双端的等待队列进行区分,互不影响。
    public LockCondition(int size) {
        items = new Object[size];
    }
    // 添加一个元素,如果数组满,则添加线程进入等待状态,直到有"空位"
    public void add(T t) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) // 如果生产已满
                notFull.await(); //生产者暂定生产,去等待队列吧
            items[addIndex] = t;
            if (++addIndex == items.length)
                addIndex = 0;
            ++count;
            notEmpty.signal(); //告诉消费者线程开始消费吧
        } finally {
            lock.unlock();
        }
    }
    // 由头部删除一个元素,如果数组空,则删除线程进入等待状态,直到有新添加元素
    @SuppressWarnings("unchecked")
    public T remove() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await(); //没东西了,消费者的线程无法进行消费,进入等待队列
            Object x = items[removeIndex];
            if (++removeIndex == items.length)
                removeIndex = 0;
            --count;
            notFull.signal();// 通知生产者线程赶紧工作。
            return (T) x;
        } finally {
            lock.unlock();
        }
    }
}

8.3 Condition 原理

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。

等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node。(NodeStatus CONDITION = -2; SIGNAL=-1;CANCELLED=1)

一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如图所示。

image-20221016164803993

如图所示,Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证(因为我们调用await方法的线程当前是持有锁的),原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,其对应关系如图所示。

image-20221016164931193

一个持有锁的线程,调用了await方法之后加入了等待队列进行排队,当这个线程被唤醒(需要执行await后边的代码),需要从新竞争我们的锁(因为await方法将锁释放掉了)。如果竞争成功还行,如果竞争失败,就会加入到同步队列里进行排队。如果排到了同步队列的头部且争抢锁成功,就继续执行await方法后边的代码。如果在执行过程中又调用了await方法,就再次回到等待队列。依次循环下去。

8.4 Condition 源码

【await() + single()方法源码】

public final void await() throws InterruptedException {
    // 判断当前线程的中断标记为是否被中断
    if (Thread.interrupted())
        throw new InterruptedException();// 中断抛出异常,对中断敏感
    // 将 node 添加到等待队列中
    Node node = addConditionWaiter();
    // 释放当前线程
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 如果当前节点不在同步队列中
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);// 阻塞当前线程
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
public final void signal() {
    // 当前线程是否是持有锁正在执行的线程
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 找到第一个节点
    Node first = firstWaiter;
    if (first != null) // 不为null
        doSignal(first);// 唤醒等待队列的第一个节点
}


private void doSignal(Node first) {
    do {
        // firstWaiter指针移动至下一个
        if ( (firstWaiter = first.nextWaiter) == null)// 如果为null
            lastWaiter = null;
        first.nextWaiter = null;// 不为 null,将其更新为 null
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}
posted @ 2022-12-15 16:21  DarkSki  阅读(90)  评论(0编辑  收藏  举报