Java 编程之美:并发编程高级篇之三

版权声明:本文为GitChat作者的原创文章,未经 GitChat 允许不得转载。 https://blog.csdn.net/GitChat/article/details/80851346

640?wx_fmt=gif

本文来自作者 追梦  GitChat 上的分享

编辑 | 灰原

一、前言

借用 Java 并发编程实践中的话:编写正确的程序并不容易,而编写正常的并发程序就更难了。

相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的。

并发编程相比 Java 中其他知识点学习起来门槛相对较高,学习起来比较费劲,从而导致很多人望而却步;

而无论是职场面试和高并发高流量的系统的实现却都还离不开并发编程,从而导致能够真正掌握并发编程的人才成为市场比较迫切需求的。

本 Chat 作为 Java 并发编程之美系列的高级篇之三,主要讲解锁,内容如下:(建议先阅读 Java 并发编程之美:并发编程高级篇之二

  • 抽象同步队列 AQS (AbstractQueuedSynchronizer) 概述,AQS 是实现同步的基础组件,并发包中锁的实现底层就是使用 AQS 实现,虽然大多数开发者可能从来不会直接用到 AQS,但是知道其原理对于架构设计还是很有帮助的。

  • 独占锁 ReentrantLock 原理探究,ReentrantLock 是可重入的独占锁或者叫做排它锁,同时只能有一个线程可以获取该锁,其实现分为公平与非公平的独占锁。

  • 读写锁 ReentrantReadWriteLock 原理,ReentrantLock 是独占锁,同时只有一个线程可以获取该锁,而实际情况下会有写少读多的场景,显然 ReentrantLock 满足不了需求,所以 ReentrantReadWriteLock 应运而生,本文来介绍读写分离锁的实现。

  • StampedLock 锁原理探究,StampedLock 是并发包里面 jdk8 版本新增的一个锁,该锁提供了三种模式的读写控制。

二、抽象同步队列 AQS 概述

2.1 AQS - 锁的底层支持

AbstractQueuedSynchronizer 抽象同步队列, 简称 AQS,是实现同步器的基础组件,并发包中锁的实现底层就是使用 AQS 实现,另外大多数开发者可能从来不会直接用到 AQS,但是知道其原理对于架构设计还是很有帮助的,下面看下 AQS 的类图结构:

640?wx_fmt=png

AQS 是一个 FIFO 的双向队列,内部通过节点 head 和 tail 记录队首和队尾元素,队列元素类型为 Node。

其中 Node 中 thread 变量用来存放进入 AQS 队列里面的线程;Node 节点内部 SHARED 用来标记该线程是获取共享资源时候被阻塞挂起后放入 AQS 队列,EXCLUSIVE 标示线程是获取独占资源时候被挂起后放入 AQS 队列;

waitStatus 记录当前线程等待状态,分别为 CANCELLED(线程被取消了),SIGNAL(线程需要被唤醒),CONDITION(线程在条件队列里面等待),PROPAGATE(释放共享资源时候需要通知其它节点);prev 记录当前节点的前驱节点,next 记录当前节点后继节点。

AQS 中维持了一个单一的状态信息 state, 可以通过 getState,setState,compareAndSetState 函数修改其值;对于 ReentrantLock 的实现来说,state 可以用来表示当前线程获取锁的可重入次数;

对应读写锁 ReentrantReadWriteLock 来说 state 的高 16 位表示读状态也就是获取该读锁的次数,低 16 位表示获取到写锁的线程的可重入次数;对于 semaphore 来说 state 用来表示当前可用信号的个数;

对于 FutuerTask 来说,state 用来表示任务状态(例如还没开始,运行,完成,取消);对应 CountDownlatch 和 CyclicBarrie 来说 state 用来表示计数器当前的值。

AQS 有个内部类 ConditionObject 是用来结合锁实现线程同步,ConditionObject 可以直接访问 AQS 对象内部的变量,比如 state 状态值和 AQS 队列;

ConditionObject 是条件变量,每个条件变量对应着一个条件队列 (单向链表队列),用来存放调用条件变量的 await() 方法后被阻塞的线程,如类图,这个条件队列的头尾元素分别为 firstWaiter 和 lastWaiter。

对于 AQS 来说线程同步的关键是对状态值 state 进行操作,根据 state 是否属于一个线程,操作 state 的方式分为独占模式和共享模式。
独占方式下获取和释放资源使用方法为:

void acquire(int arg)
void acquireInterruptibly(int arg)
boolean release(int arg)

共享模式下获取和释放资源方法为:

void acquireShared(int arg)
void acquireSharedInterruptibly(int arg)
boolean releaseShared(int arg)

对于独占方式获取的资源是与具体线程绑定的,就是说如果一个线程获取到了资源,就会标记是那个线程获取到了,其它线程尝试操作 state 获取资源时候发现当前该资源不是自己持有的,就会获取失败后被阻塞;

比如独占锁 ReentrantLock 的实现,当一个线程获取了 ReentrantLock 的锁后,AQS 内部会首先使用 CAS 操作把 state 状态值从 0 变为 1,然后设置当前锁的持有者为当前线程。

当该线程再次获取锁时候发现当前线程就是锁的持有者则会把状态值从 1 变为 2 也就是设置可重入次数,当另外一个线程获取锁的时候发现自己并不是该锁的持有者就会被放入 AQS 阻塞队列后挂起。

对应共享操作方式资源是与具体线程不相关的,多个线程去请求资源时候是通过 CAS 方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次获取时候如果当前资源还能满足它的需要,则当前线程只需要使用 CAS 方式进行获取即可,共享模式下并不需要记录那个线程获取了资源;

比如 Semaphore 信号量,当一个线程通过 acquire() 方法获取一个信号量时候,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋 CAS 获取信号量。

对应独占模式的获取与释放资源流程:

1)当一个线程调用 acquire(int arg) 方法获取独占资源时候,会首先使用 tryAcquire 尝试获取资源,具体是设置状态变量 state 的值,成功则直接返回。失败则将当前线程封装为类型为 Node.EXCLUSIVE 的 Node 节点后插入到 AQS 阻塞队列尾部,并调用 LockSupport.park(this) 挂起当前线程。

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

2)当一个线程调用 release(int arg) 时候会尝试使用, tryRelease 操作释放资源,这里是设置状态变量 state 的值,然后调用 LockSupport.unpark(thread) 激活 AQS 队列里面最早被阻塞的线程 (thread)。被激活的线程则使用 tryAcquire 尝试看当前状态变量 state 的值是否能满足自己的需要,满足则该线程被激活然后继续向下运行,否者还是会被放入 AQS 队列并被挂起。

    public final boolean release(int arg) {      
       if (tryRelease(arg)) {            Node h = head;            
           if (h != null && h.waitStatus != 0)                unparkSuccessor(h);            
           return true;        }        
       return false;    }

需要注意的 AQS 类并没有提供可用的 tryAcquire 和 tryRelease,正如 AQS 是锁阻塞和同步器的基础框架,tryAcquire 和 tryRelease 需要有具体的子类来实现。

子类在实现 tryAcquire 和 tryRelease 时候要根据具体场景使用 CAS 算法尝试修改状态值 state, 成功则返回 true, 否者返回 false。

子类还需要定义在调用 acquire 和 release 方法时候 state 状态值的增减代表什么含义。

比如继承自 AQS 实现的独占锁 ReentrantLock,定义当 status 为 0 的时候标示锁空闲,为 1 的时候标示锁已经被占用,在重写 tryAcquire 时候,内部需要使用 CAS 算法看当前 status 是否为 0,如果为 0 则使用 CAS 设置为 1,并设置当前线程的持有者为当前线程,并返回 true, 如果 CAS 失败则 返回 false。

比如继承自 AQS 实现的独占锁实现 tryRelease 时候,内部需要使用 CAS 算法把当前 status 值从 1 修改为 0,并设置当前锁的持有者为 null,然后返回 true, 如果 cas 失败则返回 false。

对应共享资模式的获取与释放流程:

1)当线程调用 acquireShared(int arg) 获取共享资源时候,会首先使用 tryAcquireShared 尝试获取资源,具体是设置状态变量 state 的值,成功则直接返回。失败则将当前线程封装为类型为 Node.SHARED 的 Node 节点后插入到 AQS 阻塞队列尾部,并使用 LockSupport.park(this) 挂起当前线程。

    public final void acquireShared(int arg) {        
       if (tryAcquireShared(arg) < 0)            doAcquireShared(arg);    }

2)当一个线程调用 releaseShared(int arg) 时候会尝试使用, tryReleaseShared 操作释放资源,这里是设置状态变量 state 的值,然后使用 LockSupport.unpark(thread)激活 AQS 队列里面最早被阻塞的线程 (thread)。

被激活的线程则使用 tryReleaseShared 尝试看当前状态变量 state 的值是否能满足自己的需要,满足则该线程被激活然后继续向下运行,否者还是会被放入 AQS 队列并被挂起。

 public final boolean releaseShared(int arg) {        
        if (tryReleaseShared(arg)) {            doReleaseShared();            
        return true;        }        
      return false;    }

同理需要注意的 AQS 类并没有提供可用的 tryAcquireShared 和 tryReleaseShared,正如 AQS 是锁阻塞和同步器的基础框架,tryAcquireShared 和 tryReleaseShared 需要有具体的子类来实现。

子类在实现 tryAcquireShared 和 tryReleaseShared 时候要根据具体场景使用 CAS 算法尝试修改状态值 state, 成功则返回 true, 否者返回 false。

比如继承自 AQS 实现的读写锁 ReentrantReadWriteLock 里面的读锁在重写 tryAcquireShared 时候,首先看写锁是否被其它线程持有,如果是则直接返回 false,否者使用 cas 递增 status 的高 16 位,在 ReentrantReadWriteLock 中 status 的高 16 为获取读锁的次数。

比如继承自 AQS 实现的读写锁 ReentrantReadWriteLock 里面的读锁在重写 tryReleaseShared 时候,内部需要使用 CAS 算法把当前 status 值的高 16 位减一,然后返回 true, 如果 cas 失败则返回 false。

基于 AQS 实现的锁除了需要重写上面介绍的方法,还需要重写 isHeldExclusively 方法用来判断锁是被当前线程独占还是被共享。

另外也许你会好奇独占模式下的

void acquire(int arg)
void acquireInterruptibly(int arg)

和共享模式下获取资源的:

void acquireShared(int arg) 
void acquireSharedInterruptibly(int arg)

这两套函数都有一个带有 Interruptibly 关键字的函数,那么带有这个关键字的和不带的有什么区别那?

其实不带 Interruptibly 关键字的方法是说不对中断进行响应,也就是线程在调用不带 Interruptibly 关键字的方法在获取资源的时候或者获取资源失败被挂起时候,其他线程中断了该线程,那么该线程不会因为被中断而抛出异常,还是继续获取资源或者被挂起,也就是不对中断进行响应,忽略中断。

带 Interruptibly 关键字的方法是说对中断进行响应,也就是也就是线程在调用带 Interruptibly 关键字的方法在获取资源的时候或者获取资源失败被挂起时候,其他线程中断了该线程,那么该线程会抛出 InterruptedException 异常而返回。

本节最后我们来看看 AQS 提供的队列是如何维护的,主要看入队操作

  • 入队操作: 当一个线程获取锁失败后该线程会被转换为 Node 节点,然后就会使用 enq(final Node node) 方法插入该节点到 AQS 的阻塞队列,

    private Node enq(final Node node) {        
       for (;;) {            Node t = tail;//(1)            if (t == null) { // Must initialize                if (compareAndSetHead(new Node()))//(2)                    tail = head;            } else {                node.prev = t;//(3)                if (compareAndSetTail(t, node)) {//(4)                    t.next = node;                    
                   return t;                }            }        }    }

下面结合代码和下面的节点图来讲解下,如上代码第一次循环当要在 AQS 队列尾部插入元素时候,AQS 队列状态为图(default), 也就是队列头尾节点都指向 null;当执行代码(1)后节点 t 指向了尾部节点,这时候队列状态如图(I)。

可知这时候 t 为 null,则执行代码(2)使用 CAS 算法设置一个哨兵节点为头结点,如果 CAS 设置成功,然后让尾部节点也指向哨兵节点,这时候队列状态如图(II)。

到现在只是插入了一个哨兵节点,还需要插入的 node 节点,所以第二次循环后执行到步骤(1),这时候队列状态如图(III);然后执行代码(3)设置 node 的前驱节点为尾部节点,这时候队列状态图如图(IV);然后通过 CAS 算法设置 node 节点为尾部节点,CAS 成功后队列状态图为(V);CAS 成功后在设置原来的尾部节点的后驱节点为 node, 这时候就完成了双向链表的插入了,这时候队列状态为图(VI)。

640?wx_fmt=png

2.2 AQS - 条件变量的支持

正如基础篇讲解的 notify 和 wait 是配合 synchronized 内置锁实现线程间同步基础设施,条件变量的 signal 和 await 方法是用来配合锁(使用 AQS 实现的锁)实现线程间同步的基础设施。

在基础篇讲解了在调用共享变量的 notify 和 wait 方法前必须先获取该共享变量的内置锁,同理在调用条件变量的 signal 和 await 方法前必须先获取条件变量对应的锁。

说了那么多,到底什么是条件变量那?如何使用那?不急,下面看一个例子:

ReentrantLock lock = new ReentrantLock();//(1)
Condition condition = lock.newCondition();//(2)

lock.lock();//(3)
try {    System.out.println("begin wait");    condition.await();//(4)    System.out.println("end wait"); } catch (Exception e) {    e.printStackTrace(); } finally {    lock.unlock();//(5)
}
lock.lock();//(6)
try {    System.out.println("begin signal");    condition.signal();//(7)    System.out.println("end signal"); } catch (Exception e) {    e.printStackTrace(); } finally {    lock.unlock();//(8)
}

如上代码(1)创建了一个独占锁 ReentrantLock 的对象,ReentrantLock 是基于 AQS 实现的锁。

代码(2)使用创建的 lock 对象的 newCondition()方法创建了一个 ConditionObject 变量,这个变量就是 lock 锁对应的一个条件变量。需要注意的是一个 Lock 对象可以创建多个条件变量。

代码(3)首先获取了独占锁,代码(4) 则调用了条件变量的 await()方法阻塞挂起了当前线程,当其它线程调用了条件变量的 signal 方法时候,被阻塞的线程才会从 await 处返回,需要注意的是和调用 Object 的 wait 方法一样,如果在没有获取到锁前调用了条件变量的 await 方法会抛出 java.lang.IllegalMonitorStateException 异常。

代码(5) 则释放了获取的锁。

其实这里的 lock 对象等价于 synchronized 加上共享变量,当调用 lock.lock()方法就相当于进入了 synchronized 块(获取了共享变量的内置锁),当调用 lock.unLock() 方法时候就相当于退出了 synchronized 块。 当调用条件变量的 await() 方法时候就相当于调用了共享变量的 wait() 方法,当调用了条件变量的 signal 方法时候就相当于调用了共享变量的 notify() 方法。当调用了条件变量的 signalAll()方法时候就相当于调用了共享变量的 notifyAll() 方法。

有了上面的解释相信大家对条件变量是什么,用来做什么用的有了一定的认识了。

上面通过 lock.newCondition() 作用其实是 new 了一个 AQS 内部声明的 ConditionObject 对象,ConditionObject 是 AQS 的内部类,可以访问到 AQS 内部的变量(例如状态变量 status 变量)和方法。

对应每个条件变量内部维护了一个条件队列,用来存放当调用条件变量的 await() 方法被阻塞的线程。注意这个条件队列和 AQS 队列不是一回事情。

如下代码,当线程调用了条件变量的 await() 方法时候(事先必须先调用了锁的 lock() 方法获取锁),内部会构造一个类型为 Node.CONDITION 的 node 节点,然后插入该节点到条件队列末尾,然后当前线程会释放获取的锁(也就是会操作锁对应的 status 变量的值),并被阻塞挂起。

这时候如果有其它线程调用了 lock.lock() 尝试获取锁时候,就会有一个线程获取到锁,如果获取到锁的线程有调用了条件变量的 await()方法,则该线程也会被放入条件变量的阻塞队列,然后释放获取到的锁,阻塞到 await() 方法处。

 public final void await() throws InterruptedException {            
        if (Thread.interrupted())                
            throw new InterruptedException();            
            //创建新的node,并插入到条件队列末尾(9)            Node node = addConditionWaiter();            
            //释放当前线程获取的锁(10)            int savedState = fullyRelease(node);            
           int interruptMode = 0;            
           //调用park方法阻塞挂起当前线程(11)            while (!isOnSyncQueue(node)) {                LockSupport.park(this);                
               if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)                    
               break;            }            ...      }

如下代码,当另外一个线程调用了条件变量的 signal 方法时候(事先必须先调用了锁的 lock() 方法获取锁),内部会把条件队列里面队头的一个线程节点从条件队列里面移除后放入到 AQS 的阻塞队列里面,然后激活这个线程。

 public final void signal() {       
    if (!isHeldExclusively())          
        throw new IllegalMonitorStateException();       Node first = firstWaiter;      
      if (first != null)          
          //移动条件队列队头元素到AQS队列           doSignal(first);   }

需要注意的是 AQS 只提供了 ConditionObject 的实现,并没有提供 newCondition 函数来 new 一个 ConditionObject 对象,需要由 AQS 的子类来提供 newCondition 函数。

下面来看下当一个线程调用条件变量的 await() 方法被阻塞后,如何放入的条件队列。

        private Node addConditionWaiter() {
            Node t = lastWaiter;
            ...            
           //(1)            Node node = new Node(Thread.currentThread(), Node.CONDITION);            
           //(2)            if (t == null)                firstWaiter = node;            
           else                t.nextWaiter = node;//(3)            lastWaiter = node;//(4)            return node;        }

如上代码(1)首先根据当前线程创建一个类型为 Node.CONDITION 的节点,然后通过步骤(2)(3)(4)在单向条件队列尾部插入一个元素。

注:当多个线程同时调用 lock.lock() 获取锁的时候,同时只有一个线程获取到了该锁,其他线程会被转换为 Node 节点插入到 lock 锁对应的 AQS 阻塞队列里面,并做自旋 CAS 尝试获取锁;

如果获取到锁的线程又调用了对应的条件变量的 await() 方法,则该线程会释放获取到的锁,并被转换为 Node 节点插入到条件变量对应的条件队列里面;

这时候因为调用 lock.lock() 方法被阻塞到 AQS 队列里面的一个线程会获取到被释放的锁,如果该线程也调用了条件变量的 await()方法则该线程也会被放入条件变量的条件队列。

当另外一个线程调用了条件变量的 signal() 或者 signalAll() 方法时候,会把条件队列里面的一个或者全部 Node 节点移动到 AQS 的阻塞队列里面,等待时机获取锁。

640?wx_fmt=png

最后一个图总结下,一个锁对应有一个 AQS 阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。

三、独占锁 ReentrantLock 原理

3.1 类图结构简介

ReentrantLock 是可重入的独占锁,同时只能有一个线程可以获取该锁,其它获取该锁的线程会被阻塞后放入该锁的 AQS 阻塞队列里面。首先一览 ReentrantLock 的类图以便对它的实现有个大致了解

640?wx_fmt=png

从类图可知 ReentrantLock 最终还是使用 AQS 来实现,并且根据参数决定内部是公平还是非公平锁,默认是非公平锁:

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

其中类 Sync 直接继承自 AQS,它的子类 NonfairSync 和 FairSync 分别实现了获取锁的公平和非公平策略。

在这里 AQS 的状态值 state 代表线程获取该锁的可重入次数,默认情况下 state 的值为 0 标示当前锁没有被任何线程持有,当一个线程第一次获取该锁时候会使用尝试使用 CAS 设置 state 的值为 1。

如果 CAS 成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程,在该线程没有释放锁第二次获取改锁后状态值被为 2,这就是可重入次数,在该线程释放该锁的时候,会尝试使用 CAS 让状态值减一,如果减一后状态值为 0 则当前线程释放该锁。

3.2 获取锁

  • void lock()
    当一个线程调用该方法,说明该线程希望获取该锁,如果锁当前没有被其它线程占用并且当前线程之前没有获取该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为 1 后直接返回。

    如果当前线程之前已经获取过该锁,则这次只是简单的把 AQS 的状态值 status 加 1 后返回。

    如果该锁已经被其它线程持有,则调用该方法的线程会被放入 AQS 队列后阻塞挂起。

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

如上代码 ReentrantLock 的 lock() 是委托给了 sync 类,根据创建 ReentrantLock 时候构造函数选择 sync 的实现是 NonfairSync 或者 FairSync,这里先看 sync 的子类 NonfairSync 的情况,也就是非公平锁的时候:

final void lock() {  
 //(1)CAS设置状态值  if (compareAndSetState(0, 1))      setExclusiveOwnerThread(Thread.currentThread());  
 else  //(2)调用AQS的acquire方法      acquire(1); }

如上代码(1)因为默认 AQS 的状态值为 0,所以第一个调用 Lock 的线程会通过 CAS 设置状态值为 1,CAS 成功则表示当前线程获取到了锁,然后 setExclusiveOwnerThread 设置了该锁持有者是当前线程。

如果这时候有其它线程调用 lock 方法企图获取该锁执行代码(1)CAS 会失败,然后会调用 AQS 的 acquire 方法,这里注意传递参数为 1,这里在贴下 AQS 的 acquire 骨干代码:

    public final void acquire(int arg) {        
       //(3)调用ReentrantLock重写的tryAcquire方法        if (!tryAcquire(arg) &&            
           // tryAcquiref返回false会把当前线程放入AQS阻塞队列            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))            selfInterrupt();    }

之前说过 AQS 并没有提供可用的 tryAcquire 方法,tryAcquire 方法需要子类自己定制化,所以这里代码(3)会调用 ReentrantLock 重写的 tryAcquire 方法代码,这里先看下非公平锁的代码如下:

 protected final boolean tryAcquire(int acquires) {            
            return nonfairTryAcquire(acquires); }
final boolean nonfairTryAcquire(int acquires) {  
    final Thread current = Thread.currentThread();  
    int c = getState();  
    //(4)当前AQS状态值为0     if (c == 0) {      
        if (compareAndSetState(0, acquires)) {             setExclusiveOwnerThread(current);          
            return true;      }  }//(5)当前线程是该锁持有者  else if (current == getExclusiveOwnerThread()) {      
      int nextc = c + acquires;      
      if (nextc < 0) // overflow          throw new Error("Maximum lock count exceeded");      setState(nextc);      
      return true;  }//(6)  return false; }

如上代码(4)会看当前锁的状态值是否为 0,为 0 则说明当前该锁空闲,那么就尝试 CAS 获取该锁(尝试将 AQS 的状态值从 0 设置为 1),并设置当前锁的持有者为当前线程返回返回 true。

如果当前状态值不为 0 则说明该锁已经被某个线程持有,所以代码(5)看当前线程是否是该锁的持有者,如果当前线程是该锁持有者,状态值增加 1 然后返回 true。

如果当前线程不是锁的持有者则返回 false, 然后会被放入 AQS 阻塞队列。

这里介绍完了非公平锁的实现代码,回过头来看看非公平在这里是怎么体现的,首先非公平是说先尝试获取锁的线程并不一定比后尝试获取锁的线程优先获取锁。

这里假设线程 A 调用 lock()方法时候执行到了 nonfairTryAcquire 的代码(4)发现当前状态值不为 0,所以执行代码(5)发现当前线程不是线程持有者,则执行代码(6)返回 false,然后当前线程会被放入了 AQS 阻塞队列。

这时候线程 B 也调用了 lock() 方法执行到 nonfairTryAcquire 的代码(4)时候发现当前状态值为 0 了(假设占有该锁的其它线程释放了该锁)所以通过 CAS 设置获取到了该锁。

而明明是线程 A 先请求获取的该锁那,这就是非公平锁的实现,这里线程 B 在获取锁前并没有看当前 AQS 队列里面是否有比自己请求该锁更早的线程,而是使用了抢夺策略。

那么下面看看公平锁是怎么实现公平的,公平锁的话只需要看 FairSync 重写的 tryAcquire 方法

protected final boolean tryAcquire(int acquires) {            
   final Thread current = Thread.currentThread();            
   int c = getState();            
   //(7)当前AQS状态值为0    if (c == 0) {            
   //(8)公平性策略        if (!hasQueuedPredecessors() &&            compareAndSetState(0, acquires)) {            setExclusiveOwnerThread(current);                    
           return true;        }    }            
   //(9)当前线程是该锁持有者    else if (current == getExclusiveOwnerThread()) {                
        int nextc = c + acquires;                
        if (nextc < 0)                    
            throw new Error("Maximum lock count exceeded");        setState(nextc);                
       return true;    }//(10)    return false;  } }

如上代码公平性的 tryAcquire 策略与非公平的类似,不同在于代码(8)处在设置 CAS 前添加了 hasQueuedPredecessors 方法,该方法是实现公平性的核心代码,代码如下:

public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;        
   return h != t &&        ((s = h.next) == null || s.thread != Thread.currentThread()); }

如上代码如果当前线程节点有前驱节点则返回 true,否者如果当前 AQS 队列为空或者当前线程节点是 AQS 的第一个节点则返回 false.

其中如果 h==t 则说明当前队列为空则直接返回 false,如果 h!=t 并且 s==null 说明有一个元素将要作为 AQS 的第一个节点入队列,那么返回 true, 如果 h!=t 并且 s!=null 并且 s.thread != Thread.currentThread() 则说明队列里面的第一个元素不是当前线程则返回 true.

  • void lockInterruptibly()
    与 lock() 方法类似,不同在于该方法对中断响应,就是当前线程在调用该方式时候,如果其它线程调用了当前线程线程的 interrupt()方法,当前线程会抛出 InterruptedException 异常然后返回

public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)       throws InterruptedException {  
       //当前线程被中断则直接抛出异常   if (Thread.interrupted())      
       throw new InterruptedException();  
  //尝试获取资源   if (!tryAcquire(arg))      
      //调用AQS可被状态的方法       doAcquireInterruptibly(arg); }
  • boolean tryLock()
    尝试获取锁,如果当前该锁没有被其它线程持有则当前线程获取该锁并返回 true, 否者返回 false,注意该方法不会引起当前线程阻塞。

public boolean tryLock() {   
   return sync.nonfairTryAcquire(1); }

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

如上代码与非公平锁的 tryAcquire() 方法类似,所以 tryLock() 使用的是非公平策略。

  • boolean tryLock(long timeout, TimeUnit unit)
    尝试获取锁与 tryLock()不同在于设置了超时时间,如果超时没有获取该锁则返回 false。

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {            
           //调用AQS的tryAcquireNanos方法。        return sync.tryAcquireNanos(1, unit.toNanos(timeout));    }

3.3 释放锁

  • void unlock()
    尝试释放锁,如果当前线程持有该锁,调用该方法会让该线程对该线程持有的 AQS 状态值减一,如果减去 1 后当前状态值为 0 则当前线程会释放对该锁的持有,否者仅仅减一而已。如果当前线程没有持有该锁调用了该方法则会抛出 IllegalMonitorStateException 异常 ,代码如下:

public void unlock() {
    sync.release(1);
}   
protected final boolean tryRelease(int releases) {      
//(11)如果不是锁持有者调用UNlock则抛出异常。   int c = getState() - releases;      
   if (Thread.currentThread() != getExclusiveOwnerThread())          
       throw new IllegalMonitorStateException();      
       boolean free = false;      
       //(12)如果当前可重入次数为0,则清空锁持有线程   if (c == 0) {       free = true;       setExclusiveOwnerThread(null);   }      
  //(13)设置可重入次数为原始值-1   setState(c);      
  return free; }

如上代码(11)如果当前线程不是该锁持有者则直接抛出异常,否者看状态值剩余值是否为 0,为 0 则说明当前线程要释放对该锁的持有权,则执行(12)把当前锁持有者设置为 null。如果剩余值不为 0,则仅仅让当前线程对该锁的可重入次数减一。

3.4 案例介绍

下面使用 ReentrantLock 来实现一个简单的线程安全的 list:

public static class ReentrantLockList {        
   //线程不安全的list    private ArrayList<String> array = new ArrayList<String>();        
   //独占锁    private volatile ReentrantLock lock = new ReentrantLock();        
   //添加元素    public void add(String e) {        lock.lock();            
       try {              array.add(e);        } finally {            lock.unlock();        }    }        
   //删元素    public void remove(String e) {        lock.lock();            
       try {            array.remove(e);        } finally {            lock.unlock();        }    }        
   //获取数据    public String get(int index) {        lock.lock();            
       try {        
            return array.get(index);        } finally {            lock.unlock();        }    }

如上代码通过在操作 array 元素前进行加锁保证同时只有一个线程可以对 array 数组进行修改,但是同时也只能有一个线程对 array 元素进行访问。

同理最后使用几个图来加深理解:

640?wx_fmt=png

如上图,假如线程 Thread1,Thread2,Thread3 同时尝试获取独占锁 ReentrantLock,假设 Thread1 获取到了,则 Thread2 和 Thread3 就会被转换为 Node 节点后放入 ReentrantLock 对应的 AQS 阻塞队列后阻塞挂起。

640?wx_fmt=png

如上图,假设 Thread1 获取锁后调用了对应的锁创建的条件变量 1,那么 Thread1 就会释放获取到的锁,然后当前线程就会被转换为 Node 节点后插入到条件变量 1 的条件队列,由于 Thread1 释放了锁。

所以阻塞到 AQS 队列里面 Thread2 和 Thread3 就有机会获取到该锁,假如使用的公平策略,那么这时候 Thread2 会获取到该锁,会从 AQS 队列里面移除 Thread2 对应的 Node 节点。

四、读写锁 ReentrantReadWriteLock 原理

在解决线程安全问题上使用 ReentrantLock 就可以,但是 ReentrantLock 是独占锁,同时只有一个线程可以获取该锁,而实际情况下会有写少读多的场景,显然 ReentrantLock 满足不了需求。

所以 ReentrantReadWriteLock 应运而生,ReentrantReadWriteLock 采用读写分离,多个线程可以同时获取读锁。

<扫码阅读全文>

并在读者圈向作者提问

640?wx_fmt=png

推荐阅读

Java 编程之美:并发编程基础晋级篇

Java 编程之美:线程相关的基础知识

Java 编程之美:并发编程高级篇之一

Java 编程之美:并发编程高级篇之二

640?wx_fmt=png

展开阅读全文

没有更多推荐了,返回首页