《Java并发编程的艺术》第五章 Java中的锁


第五章 Java中的锁

框架图

高清图片地址

高清图片地址


Lock接口

与Sync相比:提供给了与sync关键字相似的同步功能,只是使用时需要显示地获取和释放锁,虽然少了点便捷性,但是却增加了锁的操作性,如可中断、超时等。

Lock的使用方式
注意,不要把锁的获取放在try里,不然如果try发生了异常,会导致锁的无故释放(本来就没拿到,还给人释放了)。

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

Lock有的而Sync没有的特性
(Sync是阻塞地? )

特性描述
尝试非阻塞地获取锁当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取,则成功获取并持有锁
能被中断地获取锁与sync不同,能响应中断,线程中断时中断异常会被抛出,同时锁会被释放
超时获取锁在指定时间截止之前获取锁,如果截止时间到了仍旧无法获取锁,则返回

Lock的API

性质:Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的。


队列同步器

int是state获取那个? dui

有个同步器(AbstractQueuedSynchronizer同步器),子类通过继承并实现抽象方法来管理同步状态(子类是不是就是XXXXxxxLock?),至于具体的同步实现功能,同步器没有定义任何同步接口,仅仅定义了与同步状态获取的释放的几个方法可以给子类使用,而通过这些方法,子类们就可以共享式地获取同步状态。
(但是看代码,好像不是抽象方法,是protected?)
同步器有三个方法操作状态:getState()setState(int newState)compareAndSetState(int expect, int update),这三个方法可以保证状态的改变是安全的。

同步器是实现锁的关键,在锁的实现中要聚合同步器,利用同步器实现锁的语义(聚合是什么意思?)

锁面向使用者,同步器面向锁的实现者。


队列同步器的接口与示例

同步器实现方法:同步器是基于模板方法实现的,首先同步器自己算是个模板吧,继承就相当于在这个模板上修改、填充,完事后要把这些模板组合在一起,再调用一个同步器提供的模板方法,这个模板方法相当于所有的模板总管理,会调用使用者重写的方法,即把所有模板集合到了一起。

模板方法是不是,通用按钮?把重写的方法放在里面,也不管你里面怎么搞的,重写了啥,对于外面的使用者,我就调用模板方法就行了?
(这个模板方法和SpringBoot有点像啊)

同步器可重写的方法
这块是自己根据首先需要选择是否重写的。

同步器提供的模板方法
这块可以直接用。

源代码例子:

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

同步器提供的模板方法基本上分为3类

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

独占锁示例
同一时刻只有一个线程能获取到锁,其他的线程只能在同步队列中等待。
那么,各个锁的状态值代表什么呢?答:每次锁被获取的时候,state就+1,所以只要stat不为0,就表示有线程正在用(所以还涉及到可重入锁?)。
代码中写了同步器的重写方法,但是并没有用到同步器模板方法?倒是实现了不少Lock接口的方法。

public class Mutex implements Lock {
    // 静态内部类,自定义同步器的关键
    // 重写的方法很少啊
    private static class Sync extends AbstractQueuedSynchronizer{
        // 检测是否处于占用状态
        // 现在是根据同步状态来处理锁, 这里设定state=1就是被占用,因为也没别的线程
        protected boolean isHeldExclusively(){
            return getState() == 1;
        }

        // 当state=0时,就可以获取锁
        // 这个是AQS中的同名方法,好像不是抽象,是protected方法?
        // 因为是模板方法,所以重写相关的函数,模板就能自己知道该干嘛?
        public boolean tryAcquire(int acquires){
            if (compareAndSetState(0,1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 释放锁
        // 写对了方法会亮
        protected boolean tryRelease(int releases){
            if (getState() == 0){
                throw new IllegalMonitorStateException();
            }
            // 设置当前占有锁的线程为空
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        // 返回一个Condition
        // 这是个啥?跟监视方法有关
        Condition newCondition(){
            return new ConditionObject();
        }
    }

    // 将操作代理到Sync上
    // 实例化刚定义的类,用这个类的方法来做各种重写
    private final Sync sync = new Sync();

    @Override
    public void lock() {
        // 参数是如果没获取到锁,传递给tryAcquire用的
        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 sync.tryAcquireNanos(1, unit.toNanos(time));
    }

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

    @Override
    public Condition newCondition() {
        // 不知道有啥用
        return sync.newCondition();
    }
}

队列同步器的实现分析

同步队列

本质:同步器依赖内部的同步队列管理,这个同步队列是FIFO双向的,线程获取的同步状态失败后,会带着部分信息一起变成一个节点,加入到同步队列中。

同步队列节点保存内容
获取同步状态失败的线程引用、等待状态,以及前驱和后继节点。

同步队列的基本结构

同步队列中线程特点:同步队列里存放的是一些跟同步状态相关的线程,如已经获取同步状态的线程、尚未获取同步状态在阻塞状态的线程;那些不想获取、或是已经用过并释放了同步状态的线程,不在这里。

入队列:新加入的节点(获取同步状态失败的)要跟在尾节点的后面变成新的尾节点,这个过程要保证线程安全(可能同时很多个竞争,都进入了阻塞,这时就有可能冲突),所以同步器提供了一个基于CAS的设置尾节点方法:compareAndSetTail(Node expect, Node update)

出队列:首节点是获取同步状态成功的节点,首节点释放同步状态后,会唤醒后后继节点,之后后继节点会获取同步状态,变成新的首节点。因为同步状态就一个,所以能释放的节点也就一个,不需要CAS操作。


独占式同步状态获取与释放

独占式同步状态获取流程
下面的方法介绍基本按着这个流程来的。

acquire

  • 功能:请求同步状态,无视中断,失败了就进入队列进行反复阻塞、解开阻塞的过程,直到成功。
  • 源码:
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
  • 说明:先尝试用tryAcquire(arg)来保证线程安全的获取同步状态,如果失败,就构造同步节点,此处为独占式Node.EXCLUSIVE,然后通过addWaiter()添加到同步队列的尾部,最后调用acquireQueued()方法通过死循环的方式获取同步状态,如果线程在等待的时候被中断的话,就会返回true,此时就会执行下面的selfInterrupt()中断当前的线程。

从阻塞唤醒:前驱节点出队(不是只有首节点能出队),或者阻塞线程被中断(中断会从等待状态返回,这里是不出队列)(所以这里的中断是一种唤醒手段?)。

addWaiter和enq

  • 功能:将节点加到同步队列中变成尾节点。
  • 源码:
        private Node addWaiter(Node mode) {
            Node node = new Node(Thread.currentThread(), mode);
            // 先用方法简单试一下,如果直接成了就能省一些操作,一次不成就进入死循环添加模式
            Node pred = tail;
            if (pred != null) {
                node.prev = pred;
                // CAS设置尾节点,有可能失败
                // 尾节点设置完了不代表队列就可以了,这里尾节点和首节点应该单独的成员变量,用起来很快,而不是在队列里的位置
                if (compareAndSetTail(pred, node)) {
                    // 可以的话就继续完成节点设置
                    pred.next = node;
                    return node;
                }
            }
            enq(node);
            return node;
        }
    
    // 死循环的方法,只有通过CAS设置了尾节点,才会退出
    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;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    // 唯一退出位置
                    return t;
                }
            }
        }
    }
    

节点进入同步队列后,就会进入自旋的过程,反复确认自身,当满足条件获得了同步状态,就可以从自旋中退出,否则就留在自旋中,并会阻塞节点的线程

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;
                }
                // 第一个函数检测并更新请求失败的节点状态,如果线程应该被阻塞就返回true
                // 判断标准就是前驱是不是首节点,不是肯定竞争不到
                // 第二个函数方便线程进入park,然后检查是否被中断
                // park就是让线程进入等待状态,不需要锁就能进行
                // 后面会通过unparkSuccessor再唤醒
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  • 说明:后面有个方法会让线程进入阻塞,而阻塞的解决方法之一就是前驱节点离队,这就是这里对自旋的处理吧,在恰当的时机不断尝试,在不恰当的实际进入阻塞。也可以看到,节点的循环检查是不需要相互通信的,只要简单判断自己的前驱节点是不是首节点就行。
  • 自旋获取同步状态:

release

  • 功能:释放同步状态,退位让给下一位。
  • 源码:
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                // 唤醒节点的后继者
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    

共享式同步状态获取与释放

共享式和独占式的区别:同一时刻是否有多个线程同时获取到同步状态。例如读写操作,读的时候写操作都会被阻塞,而读可以同时进行。读是共享式访问,写是独占式访问,对比可看下图:

acquireShared和doAcquiredShared:

  • 功能:获取共享式同步状态。
  • 源码:
        public final void acquireShared(int arg) {
            // 如果tryAcquireShared的返回值大于等于0,则说明能够获取到同步状态
            if (tryAcquireShared(arg) < 0)
                doAcquireShared(arg);
        }
        private void doAcquireShared(int arg) {
            final Node node = addWaiter(Node.SHARED);
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    final Node p = node.predecessor();
                    if (p == head) {
                        int r = tryAcquireShared(arg);
                        // 大于0,自旋结束
                        if (r >= 0) {
                            // 设置首节点并根据参数广播
                            setHeadAndPropagate(node, r);
                            p.next = null; // help GC
                            if (interrupted)
                                selfInterrupt();
                            failed = false;
                            return;
                        }
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    

releaseShared

  • 功能:释放同步状态。
  • 源码:
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            // 死循环做CAS
            doReleaseShared();
            return true;
        }
        return false;
    }
    
  • 说明:这里和独占式有所不同,独占式只有一个能获取同步状态,所以不需要CAS,而共享式不止一个,所以必须确保同步状态线程安全释放,所以一般是通过循环和CAS来保证的。

独占式超时获取同步状态

介绍:在指定时间段内获取同步状态,如果获取到同步状态则返回true,否则返回false。

等待机制:因为有指定等待时间,所以在剩余时间还长的时候(超过自己设定的阈值),可以让线程先等待一会(pack),这样能节省一部分资源消耗;当剩下的时间不多的时候,就开始无条件自旋。

doAcquireNanos:

  • 源码:
    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        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 true;
                }
                // 剩余时间
                nanosTimeout = deadline - System.nanoTime();
                // 超时
                if (nanosTimeout <= 0L)
                    return false;
                // 剩余时间还很多就等待一会
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

独占式超时获取同步状态的流程


自定义同步组件——TwinsLock

自己写的
好吧,实在是太拉跨了

public class TwinsLock implements Lock {

    static class Sync extends AbstractQueuedSynchronizer{
        @Override
        protected boolean isHeldExclusively() {
            // 在这里可得state上限制
            return getState() < 2;
        }

        @Override
        protected int tryAcquireShared(int arg) {
            if (compareAndSetState(0,1)){
                return 1;
            }
            if (compareAndSetState(1,2)){
                return 1;
            }
            return -1;
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            return releaseShared(1);
        }

        Condition newCondition(){
            return new ConditionObject();
        }

    }

    private final Sync sync = new Sync();

    @Override
    public void lock() {
        sync.acquireShared(1);
    }

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

    @Override
    public boolean tryLock() {
        return sync.tryAcquireShared(1) > 0;
    }

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

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

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

Lock代码

public class TwinsLock implements Lock {

    private static final class Sync extends AbstractQueuedSynchronizer{
        Sync(int count){
            if (count <= 0){
                // 如果输入的线程数少于1,则抛出异常
                throw new IllegalArgumentException(
                        "count must large than zero."
                );
            }
            // 直接设置State就行,之前那种应该算是原理介绍吧
            setState(count);
        }

        // 因为同一时刻可以多个线程访问,所以是共享的
        @Override
        protected int tryAcquireShared(int arg) {
            // 死循环的方式做CAS操作
            for (;;){
                int current = getState();
                // 参数为1
                int newState = current - arg;
                if (newState<0 || compareAndSetState(current, newState)){
                    // 如果State小于0,或者更新成功就返回
                    // 可通过正负来判断有没有获得同步状态
                    return newState;
                }
            }
        }

        @Override
        protected boolean tryReleaseShared(int arg) {

            for (;;){
                int current = getState();
                int newState = current + arg;
                // 不需要控制范围吗?
                if (compareAndSetState(current, newState)){
                    return true;
                }
            }
        }

        @Override
        protected boolean isHeldExclusively() {
            return super.isHeldExclusively();
        }
    }

    private final Sync sync = new Sync(2);
    @Override
    public void lock() {
        sync.acquireShared(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

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

    @Override
    public Condition newCondition() {
        return null;
    }
}

测试代码

public class TwinsLockTest {
    @Test
    public void test(){
        final Lock lock = new TwinsLock();
        class Worker extends Thread{
            @Override
            public void run() {
                // 前后各睡一秒再释放
                while (true){
                    lock.lock();
                    try {
                        SleepUtils.second(1);
                        System.out.println(Thread.currentThread().getName());
                        SleepUtils.second(1);
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }

        for (int i = 0; i < 10; i++) {
            Worker w = new Worker();
            w.setDaemon(true);
            w.start();
        }

        // 换行
        for (int i = 0; i < 10; i++) {
            SleepUtils.second(1);
            System.out.println();

        }
    }
}

输出结果:

Thread-1
Thread-2


Thread-1
Thread-2


Thread-2
Thread-1


Thread-2
Thread-1


Thread-1
Thread-2

感觉是tryAcquireShared把所有的State用完了之后,才做doAcquireShared,共享状态用完了再争取首节点?但doAcquireShared里面要try返回值大于等于0才行,而进入doAcquireShared的条件是小于0,而且设置首节点的时候会根据剩余State的值,指向后继(setHeadAndPropagate)。
那么既然可以有多个同步状态,首节点和其他几个拥有同步状态的节点是什么关系?一个节点里面有多个线程?
realeaseShared一释放就全部都释放了。


重入锁

功能:能够支持一个线程对资源的重复加锁。没有可重入功能的,在自身获得锁后,如果再次调用lock()方法,该线程就会被自己阻塞。

sync关键字:这个关键字隐式支持重进入,执行线程获得了锁之后仍然能多次获得锁。

ReentrantLock:不是隐式重进入,但是再次调用lock()方法的时候能不被阻塞。

公平性问题:在绝对时间上,先对锁提出获取请求的线程先获得锁,这就是公平的。所以等待时间越长的线程获取锁的概率越高,但公平的一般不如非公平效率高。


实现重进入

重进入特性:得满足下面两个要求。

  • 线程再次获取锁:锁要识别当前获得的线程是不是之前已经占据锁的线程。
  • 锁的最终释放:一个线程重复获取了 n n n次,那么也要重复释放 n n n次才行。

ReentrantLock获取锁源码
默认非公平性,看这个样子只能是独占式的。

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        // 判断是否一致,一致就计数器累加
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

ReentrantLock释放锁源码

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

公平与非公平获取锁的区别

TwinsLockTest

ReentrantLock的公平锁获取锁源码
多了一个hasQueuedPredecessors,判断是否有前驱节点,没有的话说明是队列第一个,可以获取锁。

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

公平与非公平对比代码
会输出逆序列表。

代码
没写好,没给的地方没写出来,干!同步队列里没线程。

public class FairAndUnfairTest {

    private static Lock fairLock = new ReentrantLock2(true);
    private static Lock unfairLock = new ReentrantLock2(false);

    @Test
    public void fair(){
        testLock(fairLock);
    }

    @Test
    public void unfair(){
        testLock(unfairLock);
    }

    private void testLock(Lock lock){
        for (int i = 0; i < 5; i++) {
            Job job = new Job(lock);
            job.setDaemon(true);
            job.start();
        }
    }

    private static class Job extends Thread{
        private Lock lock;
        public Job(Lock lock){
            this.lock = lock;
        }

        @Override
        public void run() {
            System.out.println(((ReentrantLock2)lock).getQueuedThreads().size());
            System.out.println(Thread.currentThread().getName() + " , waiting by " + ((ReentrantLock2)lock).getQueuedThreads().toString());
            System.out.println(Thread.currentThread().getName() + " , waiting by " + ((ReentrantLock2)lock).getQueuedThreads().toString());
            System.out.println("=========================");
        }
    }


    private static class ReentrantLock2 extends ReentrantLock{
        public ReentrantLock2(boolean fair){
            // 父类构造函数
            super(fair);
        }

        public Collection<Thread> getQueuedThreads(){
            // 获取队列中的线程
            List<Thread> arrayList = new ArrayList<Thread>(super.getQueuedThreads());
            Collections.reverse(arrayList);
            return arrayList;
        }
    }
}

输出结果
可见,公平性锁每次都是从同步队列的第一个节点取得锁,而非公平性则是随机选,还经常出现一个线程连续获得锁的情况。

连续获取原因分析:在nonfairTryAcquire中可看到,只要获取了同步状态即成功获取锁,在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。

性能比较:公平锁每次都会切换线程,而非公平锁不会,而切换线程是需要消耗性能的,所以非公平锁开销更小,下面是对比图:

使用场景

  • 公平性锁:保证了FIFO原则,代价是进行大量的线程切换。
  • 非公平性锁:极少的线程切换,保证了更大的吞吐量。

读写锁

要求:读的时候可以同一时刻多个线程访问;写的时候所有读和其他的写线程都要阻塞。

老版本实现:Java5之前,用的等待通知机制,开始写的时候,后面的都进入等待,写完了再通知继续执行。

优点:读写锁比排他锁提供更好的并发性和吞吐量。

ReentrantReadWriteLock的特性


读写锁的接口与示例

关系ReentrantReadWriteLockReadWriteLock的实现,ReadWriteLock只定义了两个方法:readLock()writeLock()

ReentrantReadWriteLock展示工作状态的方法

缓存示例

public class Cache {
    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();
    // 获取一个key对应的value
    public static final Object get(String key){
        r.lock();
        try {
            SleepUtils.second(2);
            return map.get(key);
        } finally {
            r.unlock();
        }
    }

    // 设置key对应的value,返回旧value
    public static final Object put(String key, Object value){
        w.lock();
        try {
            SleepUtils.second(3);
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }

    // 清空所有内容
    public static final void clear(){
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }

    public static void main(String[] args) {
        // 不行,一个线程的
        map.put("1","1");
        map.put("2","11");
        map.put("3","111");
        map.put("4","1111");
        map.put("5","11111");
        // System.out.println("Get:");
        // System.out.println(get("1"));
        // System.out.println(get("2"));
        // System.out.println(get("3"));
        // System.out.println(get("4"));
        // System.out.println(get("5"));
        System.out.println("Put:");
        System.out.println(put("1","2"));
        System.out.println("开始清理");
        clear();
        // System.out.println(put("2","22"));
        // System.out.println(put("3","222"));
        // System.out.println(put("4","2222"));
    }
}


读写锁的实现分析

读写状态的设计

  • 区别:跟之前自定义的同步器的区别在于,之前的同步状态表示被一个线程重复获取的次数(共享式不也是多个线程吗?),而这里要同时维护一个写线程和多个读线程的状态。
  • 方法:如果要一个整型变量维护多种状态,那就一定需要”按位切割使用“这个变量,读写锁将变量切成了两个部分,高16位表示读,低16位表示写,下面这个例子是写被重进入了2次(加上第一次就是3);读锁也获得了两个线程(不只是重进入还是就两个线程?)。

写锁的获取与释放

  • 属性:是一个支持重进入的排他锁。如果线程获取写锁的时候,发现写锁已经被获取了,如果上次获取锁的线程是自己,那么就可以重进入;如果不是,就进入等待。
  • 源码
protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    // 返回独占持有的数量,为啥参数是C?
    // static final int SHARED_SHIFT   = 16;
    // static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; 这个是0x0000FFFF,
    // static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } 得到写锁的数量
    // 我明白了,排他就是写锁的意思;共享就是读锁的意思:sharedCount
    // w得到的是写状态
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        // c不等于0说明有线程得到锁,w=0说明没写锁,就说明有读锁,这样是不行的
        // 当前有写锁不是该线程也不行
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 超出写锁重入上限?
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}
  • 说明:先判断写锁是否存在,存在的话,两种情况该线程不能获取这个锁:1、存在读锁。2、存在写锁并不是当前线程。

读锁的获取和释放

  • 属性:支持重进入的共享锁。大部分情况都不会等待,除非有写锁在被线程使用。读锁因为是共享的,所以同步状态记录的是所有获取读锁的线程的总数,为了计算各自线程获取读锁的次数,将其保存在了ThreadLocal中,可以通过getReadHoldCount()方法获取。
  • 源码简化版
  • 源码
    protected final int tryAcquireShared(int unused) {
        /*
         * Walkthrough:
         * 1. If write lock held by another thread, fail.
         * 2. Otherwise, this thread is eligible for
         *    lock wrt state, so ask if it should block
         *    because of queue policy. If not, try
         *    to grant by CASing state and updating count.
         *    Note that step does not check for reentrant
         *    acquires, which is postponed to full version
         *    to avoid having to check hold count in
         *    the more typical non-reentrant case.
         * 3. If step 2 fails either because thread
         *    apparently not eligible or CAS fails or count
         *    saturated, chain to version with full retry loop.
         */
        Thread current = Thread.currentThread();
        int c = getState();
        if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
            return -1;
        int r = sharedCount(c);
        if (!readerShouldBlock() &&
            r < MAX_COUNT &&
            compareAndSetState(c, c + SHARED_UNIT)) {
            if (r == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    cachedHoldCounter = rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
            }
            return 1;
        }
        return fullTryAcquireShared(current);
    }
    
  • 说明:读锁每次释放均减少读状态,减少的值是(1<<16)

锁降级

降级定义:在把持住写锁的情况下,再获取到读锁(此时同时有两个锁),随后释放先前拥有的写锁的过程。分段完成不叫降级(先有写锁,再释放这个写锁,再获取读锁,没有同时拥有两个的情况)。

因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作。
(写锁不是就一个吗?重入不也是线程自己吗,一个线程也能搞并发?)

流程:首先,有很多访问processData()方法的线程,当processData方法里面发生了数据变化,update变成了false,所有的有processData方法的线程就感知到了,此时就会共同竞争一个写锁,但是只有一个线程能得到,这时其他的线程就进入等待状态(大哥要开始干活了,小的别碍事,写完了再说)。当前线程完成了写的操作之后,再获取读锁,然后释放锁,完成锁降级。(活干完了,数据也更新了,大家可以瞧瞧了)。

问题:锁降级中是否有必要获取读锁?有必要,当线程持有写锁的时候,其他的线程都是等待的,因为先获取读锁再释放写锁,此时这个线程已经准备好了,等写锁一释放就跟其他线程一样了;如果先释放写锁,再获取读锁,那么这两个步骤之间会有时间差,如果在这段时间内,线程B获取了写锁并修改了数据,由于A(刚释放读锁的线程)还没有读锁,所以看不见刚发生的改动,就出错了。
所以,这个步骤的目的就是为了保证数据的可见性。

ReentrantReadWriteLock不支持锁升级(把持读锁、获取写锁、最后释放读锁):目的也是为了保持数据的可见性,你拿走了写锁,然后改了数据,同时你自己有读锁,你是看到了数据的变化,但那些持有读锁的等待状态的线程还是两眼一抹黑,你最后释放读锁与否都没用了,这期间的改动还是见不到。


LockSupport工具

使用场景与作用:当需要阻塞或唤醒一个线程的时候,可使用LockSupport工具类,其定义了一组公共的静态方法来满足最基本的需求。

方法

  • park开头:停车, 即阻塞的意思
  • unpark:唤醒。
  • 具体方法:

额外方法
Java6开始才有的,因为Java5中实现Lock等工具时忘记了用dump提供阻塞对象,所以这三个方法是弥补这一问题的:park(Object blocker)parkNanos(Object blocker, long nanos)parkUntil(Object blocker, long deadline),其中blocker用来标记当前线程在等待的对象。

dump示例


Condition接口

功能:提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。

与Object监视器对比


Condition接口与示例

示例代码

public class ConditionUseCase {
    Lock lock = new ReentrantLock();
    // conditon的创建依赖lock对象
    Condition condition = lock.newCondition();

    public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            // 释放锁并进入等待
            condition.await();
        } finally {
            lock.unlock();
        }
    }

    public void conditionSignal(){
        lock.lock();
        try {
            // 通知等待线程,获取锁并从await()方法返回
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}

说明

  • Condition对象是由Lock对象创建的。
  • 线程调用Condition的方法的前提是,该线程已经获取到了跟Condition相关的锁。
  • 一般会将Condition对象作为成员变量。
  • 使用Condition.await()后,当前线程会释放锁并在此等待。
  • 其他线程调用Condition.signal()方法后,会通知当前线程,当前线程会获取锁并从await()方法返回。

Condition部分方法

有界队列:当队列为空时,队列的获取操作会阻塞线程,直到有新的加入;当队列为满时,插入操作也会阻塞,直到有空位。

有界队列代码

public class BoundedQueue<T> {
    private Object[] items;
    // 添加的下标,删除的下标,数组当前数量
    private int addIndex, removeIndex, count;
    private Lock lock = new ReentrantLock();
    // 两个,管两种?
    // 答:对,分别唤醒对方
    // 管数组为空的情况
    private Condition notEmpty = lock.newCondition();
    // 管数组满了的情况
    private Condition notFull = lock.newCondition();
    public BoundedQueue(int size){
        items = new Object[size];
    }
    // 添加一个元素,如果数组满,则添加线程进入等待状态,直到有空位
    public void add(T t) throws InterruptedException {
        // 先获取锁,确保数组修改的可见性和排他性
        lock.lock();
        try {
            // 使用while防止过早或意外通知,只有条件符合了才能退出循环
            while (count == items.length){
                // 如果满了,就等待
                notFull.await();
            }
            // 添加
            items[addIndex] = t;
            // addindex已经添加到了最后一位,又满了?
            if (++addIndex == items.length){
                // 为何要设为0?
                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];
            // 移除的是最后一个,那又怎么样?为何设为0?
            if (++removeIndex == items.length){
                removeIndex = 0;
            }
            --count;
            // 只要删除了,就不会满,可唤醒
            notFull.signal();
            return (T) x;
        } finally {
            lock.unlock();
        }
    }
}

Condition的实现分析

每个Condition对象都包含着一个等待队列,该队列是Condition对象实现等待/通知功能的关键。

等待队列

属性:FIFO队列,队列中每个节点都包含一个线程引用,就是在Condition对象上等待的线程。

进入队列:线程调用了Condition.await()方法后,线程就会释放锁、构造成节点加入等待队列变成等待状态。另外,这个节点和同步队列节点是一样的,都是同步器AQS的内部类。

基本结构
注意,节点更新的时候并没有使用CAS,因为调用await()方法的时候已经获取锁了,锁已经保证线程安全了。

同步队列与等待队列

  • Object的监视器模型只有一个同步队列和一个等待队列;Lock拥有一个同步队列和多个等待队列。(只要用Lock生成多个Condition就有多个等待队列)。
  • 同步队列和等待队列一样,都是同步器的内部类,所以每个Condition实例都能访问同步器提供的方法。
  • 结构图:

等待

行为:当调用了await()的时候,当前线程会从同步队列的首节点移动到等待队列的最后一个节点,不过是新的节点。

源码

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 将当前线程添加到等待队列的末尾,返回的是当前线程
    // 这里是new了一个新的节点,而不是移动同步队列的节点
    Node node = addConditionWaiter();
    // 释放同步状态,里面还包含了唤醒后继者的操作
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 是否在同步队列
    while (!isOnSyncQueue(node)) {
        // 进入等待状态
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 请求恢复,设置为原来的同步状态
    // 只有signal相关方法唤醒才行,如果是中断唤醒,会抛出异常
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        // 移除已取消连接的节点
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

流程图


通知

功能:调用Condition的signal()方法,会唤醒等待队列中等待时间最长的节点(首节点),唤醒之前,先将节点移动到同步队列。

源码

public final void signal() {
    // 判断有没有获得锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 移出第一个节点
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

说明

  • 调用方法的前置是获得了锁,唤醒该线程。
  • 调用后会把等待队列的第一个节点移动到同步队列的尾节点(意义上的移动,不是实际上的移动),然后该结点的线程就进入竞争状态,等到获取了同步状态之后,被唤醒的线程会从await()方法中返回。
  • signalAll()方法相当于对等待队列的每个节点都执行一次signal()方法,效果是将等待队列的节点全部移动到同步队列,并唤醒每个节点的线程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值