并发编程-Lock

并发编程-Lock

从本篇开始就正式进入到 J.U.C 并发包里面主要工具的解析,终于可以暂时不用关注像 synchronized 和 volatile关键字所涉及到的硬件层面相关的知识了。J.U.C 一个简称,它的核心是在 rt.jar这个包中 路径是 java.util.concurrent
在这里插入图片描述
从上图中我们可以看到。在J.U.C中有个Lock接口,顾名思义它是一个锁和synchronized 的作用类似,都是保证线程安全性的选择,那么我们就以Lock作为J.U.C的开端。

Lock

要深入了解Lock,首先我们先来看它定义了哪些方法
在这里插入图片描述
主要的方法有

  1. lock() 抢占锁的方法。
  2. tryLock() 尝试获得锁
  3. unlock() 释放锁

知道了它的主要方法后,我们再来看它的类关系图以及lock()方法有哪些实现类,先有个大体的印象
在这里插入图片描述
lock() 方法的实现类
在这里插入图片描述
具体方法具体分析,我们就先从ReentranLock(重入锁)开始

RenntrantLock

重入锁(ReentrantLock)是一种可重入的独占式锁或者可以称为可以重入的排他锁,它允许一个线程对资源的重复加锁。这意味着如果一个线程已经获取了某个资源的锁,那么它可以再次调用lock()方法获取该锁时,不会被阻塞。只记录重入次数。

使用

ReentrantLock 的使用非常简单,话不多说直接上代码

public class ReentrantLockDemo {
    private int i = 0;
    static Lock lock = new ReentrantLock();
    private void incr(){
        lock.lock();
        try {
            i++;
        }finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
        Thread[] threads=new Thread[2];
        for (int j = 0;j<2;j++) {
            // 创建两个线程
            threads[j]=new Thread(() -> {
                // 每个线程跑1000次
                for (int k=0;k<1000;k++) {
                    reentrantLockDemo.incr();
                }
            });
            threads[j].start();
        }
        try {
            threads[0].join();
            threads[1].join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(reentrantLockDemo.i);
    }
}

过去我们知道了在遇到原子性问题时可以用 synchronized 关键字解决原子性问题。现在有了一个新的解决办法,在上述代码中 由于i++是个非原子操作,我们引入了ReentrantLock来解决原子性问题 lock.lock()方法获得锁,lock.unlock()释放锁(需要注意的一点是ReentrantLock 必须手动释放锁)。使用这里就不用太多篇幅来解释了,我们将重点放在ReentrantLock实现原理上来

实现原理

在探究ReentrantLock实现原理之前,不妨先来思考一下或者猜一猜。它是如何实现的,怎么加锁,怎么释放锁,没有获得锁的线程怎么办等等一系列问题。猜想的这个过程就等同于将锁的需求理清,对于下一步阅读源码会有很大的帮助

需求分析
  1. 锁是怎么抢占的,会不会有个标记状态表示已经抢占到锁。像synchronized 会在对象头中标记位中表示有没有锁

  2. 抢占到锁的线程执行代码逻辑,那没有抢占到锁的线程怎么办。

    (1)没有抢占到锁的线程需要等待,等待的话也就是让其阻塞。这里我们通常会想到 wait/notify(线程通信机制,notify可以唤醒处于阻塞状态下的某个线程)或者是 LockSupport.park()/LockSupport.unpark()(阻塞或唤醒指定线程)。

    (2)多线程的情况下只有一个线程获得锁那会有多个线程等待,凡事总得有个先来后到,没有获得锁的线程是不是也得按顺序等待。就像我们平常去餐厅吃饭,没有空桌的时候我们都在餐厅门口取号等待。同理,没有获得锁的线程大概也会是这样。怎么给他们安排顺序呢?我们常规做法是不是可以放到数组、集合、链表等数据结构中。

  3. 锁抢占的公平性,等待的线程是否允许插队

  4. 锁的释放有什么操作,既然是重入,且有重入次数,那锁的释放重入次数会不会递减。处于等待的线程会不会按顺序一个一个的唤醒。

带着这些疑问我们去源码中一探究竟,在探究源码之前我们还是需要先看一下ReentrantLock的类关系图
在这里插入图片描述
从图中我们可以看到一个很重要的类 AbstractQueuedSynchronizer 简称AQS,后续J.U.C源码中很多地方都会看到它的身影。那我们就简单剧透一下,看一下它里面有什么

static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    static final Node SHARED = new Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;

    /** waitStatus value to indicate thread has cancelled */
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    static final int CONDITION = -2;
    /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
    static final int PROPAGATE = -3;

    volatile int waitStatus;

    volatile Node prev;

    
    volatile Node next;

    /**
         * The thread that enqueued this node.  Initialized on
         * construction and nulled out after use.
         */
    volatile Thread thread;

    
    Node nextWaiter;

    /**
         * Returns true if node is waiting in shared mode.
         */
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    /**
         * Returns previous node, or throws NullPointerException if null.
         * Use when predecessor cannot be null.  The null check could
         * be elided, but is present to help the VM.
         *
         * @return the predecessor of this node
         */
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

我们大致浏览一下AbstractQueuedSynchronizer 这个类,可以发现一个很重要的点就是它有一个叫Node的内部类,通过Node我们很容易就联想到一种数据结构——链表,其中Node(Thread thread, Node mode)等构造方法也需要传入thread ,所以这更加接近了我们之前对于锁的猜想中没有抢占到锁的线程需要放入一个容器中,而这个容器很有可能就是个链表。到底是不是这样?接下来我们就揣着一个半信半疑的态度去源码中寻找答案。

源码分析

ReentrantLock 源码分析首先我们得找到一个入口,我们就从它的构造方法开始

  /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

构造构造方法中我们看到两个很关键的类,在我们之前的类关系图中也有体现。NonfairSync和FairSync看名字有的小伙伴就能猜出大致意思来,这俩就是公平锁和非公平锁(源码有个好处就是名字用词准确,通过粗略的翻译就能知晓大致含义)它们都继承Sync 这个抽象类,而Sync 这个类又继承于 AbstractQueuedSynchronizer

static final class NonfairSync extends Sync {
        //省略代码 ........
}
static final class FairSync extends Sync {
        //省略代码 ........
}
private final Sync sync;
    //这里代码篇幅过长,先截取重点
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;
        abstract void lock();
        //省略代码 ........
    }

这几个类梳理完之后,我们再来看细节,在ReentrantLock() 构造方法中我们看到它默认的是非公平锁,那我们就着顺着非公平锁往下走

非公平锁
static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * 这里的lock() 方法重写了Sync里的lock()方法 因此我们在demo中执行的lock.lock()方法加锁的核心逻辑就在这里
         */
        final void lock() {
            /**
             *首先是一个cas操作,这个我们在研究synchronized 时提到过,比较并替换。
              这里比较的是 AbstractQueuedSynchronizer 中 state 的偏移量 也就是偏移量与预期值0相等,则state=1 
              这就是抢占到锁的标记
             */
            if (compareAndSetState(0, 1))
                /**
                *这是一个赋值操作,也就是把当前线程赋值给 exclusiveOwnerThread 看名字翻译过来叫排他性所有者,这不就是获得                 *锁的线程
                */
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
}

我们接着再看else也就是没抢占到锁怎么办 acquire(1)

public final void acquire(int arg) {
    //我们先看整体,这里是个判断条件如果要条件成立则需要 tryAcquire(arg) == false(注:这里直接进入这个方法我们是进入了         // AbstractQueuedSynchronizer里面的tryAcquire() 方法,默认是抛出异常。这里很明显是不对的。所以它肯定是在               // ReentrantLock中重写了。tryAcquire翻译过来是尝试获取
    if (!tryAcquire(arg) &&
    //这里先粗略扫一眼acquireQueued参数中的这个addWaiter(Node.EXCLUSIVE) 构建一个Node节点,具体操作我们稍后回来再看
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    //调用interrupt() 修改当前线程中断状态    
    selfInterrupt();
}

我们看 tryAcquire里面做了什么

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
    //拿到当前线程
    final Thread current = Thread.currentThread();
    //获取锁的状态
    int c = getState();
    //判断锁的状态,等于0 通过cas操作来获取锁
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果是已经获得所得线程则state+1
    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;
}

nonfairTryAcquire() 这个方法中的逻辑整体比较简单,就是判断state的值,为0就再去抢占锁,不等于0的话再判断当前线程是不是已经获得了锁的线程并将state加1。其实这个地方就是我们所说的重入,当前获得锁的线程并不需要再去尝试获得锁,而是直接记录一个次数这样的好处是能避免死锁。回过头来 我们来看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 这个方法

//首先看addWaiter(Node.EXCLUSIVE)
private Node addWaiter(Node mode) {
        //将当前线程封装成Node
        Node node = new Node(Thread.currentThread(), mode);
        //tail顾名思义 尾,也就是说tail是个尾节点,默认是null
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
}
//当pred节点为空时enq() 方法做了什么
private Node enq(final Node node) {
        //这里是个自旋操作
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                //当t等于null,也就是说链表还没有创建执行一个cas操作,
                //创建一个node节点,这个节点既是头节点也是尾节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //把prev指向t
                node.prev = t;
                //比较并替换判断t是不是最后一个节点,是的话替换成node
                if (compareAndSetTail(t, node)) {
                    //把next指向node
                    t.next = node;
                    return t;
                }
            }
        }
}

这里我们可以通过图来理解代码的含义
在这里插入图片描述
理解了这个双向列表如何时产生的之后,我们看acquireQueued() 方法

//看这个方法我们首先要注意一下参数,它的参数中有Node节点,而这个node节点是通过addWatier()也就是加入队列中的线程
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //这里是一个自旋操作,也就是说已经加入队列的线程会先去不断的尝试获得锁,和synchronized 轻量级锁一样
            //它也不是一直自旋
            for (;;) {
                //node.predecessor()表示获取上一个节点
                final Node p = node.predecessor();
                //非公平锁的一个特点就是抓住任何机会都会去尝试获得锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //如果已经抢占到锁的线程还没释放锁的情况下,此时其他线程进入acquireQueued()方法肯定抢占锁失败,那失败之后得                 //需要修改状态 我们来看shouldParkAfterFailedAcquire()
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}
/**
我们粗略了看一眼这个方法会发现这个方法整体并不是多么的复杂,它比较的是Node节点的状态。那这个地方我们得先补充一下Node节点状态的含义
Node 有5种状态,分别是CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、默认状态(0)
CANCELLED: 如果在同步队列中等待的线程等待超时或者被中断,那么需要从同步队列中取消该Node节点,其节点的watiStatis为  CANCELLED,即结束状态,进入该状态后的节点不会发生变化。简单来说就是把无用的节点从队列中清除掉
SIGNAL: 顾名思义翻译过来是信号,只要前面的那个节点释放锁,就会通知标识为SIGNAL状态的后续节点的线程
CONDITION:表示节点在等待条件队列中。当一个线程在等待条件变量时,会被放入条件队列,节点的状态被设置为 CONDITION。
PROPAGATE:在共享模式下,PROPAGATE状态的线程处于可运行状态。
默认状态是 0
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //在了解了Node节点的几个状态之后,关于重入锁这块 我们只需要重点关注CANCELLED、SIGNAL、默认状态
        //pred.waitStatus 获取上一个节点的状态
        int ws = pred.waitStatus;
        //判断上一个节点的状态是否为-1,如果为-1。那么当前这个节点的线程可以安全的挂起
        if (ws == Node.SIGNAL)
            return true;
        //ws大于0,只有等于CANCELLED状态成立,也就是剔除前一个无效的节点
        if (ws > 0) {
            do {
                //这个地方是通过修改双向链表的指向来剔除掉无效的节点,有的小伙伴会有疑问,为啥是先通过修改prev指向而不是next                   指向,这里是为了保证线程安全性,因为之前Node节点都是先prev指向完之后通过CAS操作来指向next的操作,
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //通过CAS操作把前面那一个系欸但状态改为-1,这么做的目的是确保在同步队列中每个等待的线程的状态是正常的,否则就需要               把不正常的节点移除掉
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
}
//当shouldParkAfterFailedAcquire()返回 true时 parkAndCheckInterrupt()才能执行,挂起当前线程
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
}

截止到这个地方,非公平锁的lock()加锁的源码分析完成,那我们继续来看公平锁

公平锁

公平锁相对于非公平锁代码上整体相差不大,也就是说锁的抢占是一样的,那我们的侧重点就可以放到看它怎么保证公平性上来。与非公平锁的开头一样,我们仍然是把构造方法作为入口

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}
//进入到FairSync()中来,我们可以看到在lock()方法中没有像非公平锁那样上来就通过CAS操作去抢占锁,而是调用的acquire(1)方法
//而且公平锁又重写了tryAcquire()方法,所以侧重点就在tryAcquire()方法
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }
    //tryAcquire()与非公平锁的tryAcquire方法大致相同 区别就在于hasQueuedPredecessors()这个方法
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            //非公平锁和公平所得区别在于hasQueuedPredecessors()方法
            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 final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        //这里的判断也非常的明确,当头节点等于尾节点则队列为空,当头节点等于下一个节点为空则队列为空
        //总的来说结果返回false,队列为空才会去抢占锁
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
}

剩余代码跟非公平锁就一样的了,这里不做重复描述了。加锁我们分析完了,我们继续看释放锁的逻辑

锁的释放

锁的释放是lock.unlock()方法,我们进入源码

public void unlock() {
   sync.release(1);
}
public final boolean release(int arg) {
    //我们先假设tryRelease(arg)结果为true
    if (tryRelease(arg)) {
        //获取到队列的头节点
        Node h = head;
        //如果头节点不等于空并且状态不等于0,唤醒头节点的下一个节点
        if (h != null && h.waitStatus != 0)
            //唤醒头节点的下一个节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}
//从上述代码中我们缕了一下逻辑发现两个重要的方法,我们逐个进行分析
//先来看tryRelease()方法
protected final boolean tryRelease(int releases) {
    //这里是获得state的值,然后做减-1的操作。因为是重入锁,所以c的值可能大于1,只有调用lock()和unlock()的次数相等才能减到0
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //当c等于0,把独占线程设置为null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    //设置state的值表示无锁
    setState(c);
    return free;
}
//再看unparkSuccessor()
//锁释放的情况下,就要去唤醒下一个节点,就像一桌客人走后开始叫下一桌客人用餐
private void unparkSuccessor(Node node) {
        //参数Node是头节点,获取头节点的waitStatus
        int ws = node.waitStatus;
        if (ws < 0)
            //ws小于零的情况有 SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3),后面俩先不用管,那就暂认定当ws==-1时
            //把waitStatus修改为0
            compareAndSetWaitStatus(node, ws, 0);
        //获取下一个节点
        Node s = node.next;
        //这里的判断条件s.waitStatus > 0 大于0的时候只有CANCELLED的状态,所以这里是判断下一个是不是无效的节点,是的话要清除
        if (s == null || s.waitStatus > 0) {
            s = null;
            //从尾部开始扫描,找到距离head头节点最近的一个waitStatus <= 0的节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //下一个节点不为空直接唤醒线程,唤醒之后最开始的那个头节点Node会被回收掉,而被唤醒线程的那个节点会作为新的头节点
        if (s != null)
            LockSupport.unpark(s.thread);
}

我们来思考一下,唤醒之后的线程该干啥。首先我们已经知道它是在哪被LockSupport.park() 阻塞挂起的。为什么被挂起,是因为抢占锁失败后加入到队列中并挂起,所以它唤醒之后会从阻塞的位置开始执行也就是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;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}
小结

在这一小章节中我们主要分析了ReentrantLock (重入锁)的源码。从加锁开始到锁的释放其中涉及到公平锁和非公平锁,以及抢占锁失败后怎么处理,是怎么把抢占失败的线程加入到队列中的。通过分析我们知道了它的核心是围绕AQS构建的队列。最后我们用一张图再来回顾一下这个过程
在这里插入图片描述

ReentrantReadWriteLock

ReentrantReadWriteLock表示可重入读写锁,具体是什么意思呢?为什么需要这么一个锁?我们来假设一个场景

public class ReentrantReadWriteLockDemo {
    private final Lock lock = new ReentrantLock();
    private List<String > list = new ArrayList<>();
    private void add(String str){
        lock.lock();
        try {
            list.add(str);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    private String get(int index){
        lock.lock();
        try {
            return  list.get(index);
        }finally {
            lock.unlock();
        }
    }
}

在上述代码中ArrayList是线程不安全的集合,所以为了保证线程安全性我们分别对add()方法和get()方法加了锁。安全性问题得到了解决,但这里又延申出另外一个问题。就是当线程A去调用get方法时,如果有其他线程已经抢占到了锁,那么这个线程A就得阻塞。但get()方法本身对数据不会产生影响,还得再加上一把锁。这明显的造成了资源的浪费,所以我们需要一个同时有多个线程访问get方法时都可以获得锁并且不阻塞。读锁和写锁有一定规则限制的这么一种锁,也就是接下来要说的ReentrantReadWriteLock。在这之前我们还是缕一缕读写锁的规则

  1. 读和读不互斥,也就是说多个线程同时执行读操作,线程不会阻塞
  2. 读和写互斥,假设有A线程在执行读操作,B线程在执行写操作,为了保证数据一致性读和写都需要阻塞
  3. 写和写互斥,这个就不用多说了,多个线程同时进行写操作,加锁互斥
使用

ReentrantReadWriteLock 的使用也很简单,我们就基于上面的示例进行一下改造

public class ReentrantReadWriteLockDemo {
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final Lock readLock = readWriteLock.readLock();
    private final Lock writeLock = readWriteLock.writeLock();
    private List<String > list = new ArrayList<>();
    private void add(String str){
        writeLock.lock();
        try {
            list.add(str);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            writeLock.unlock();
        }
    }
    private String get(int index){
        readLock.lock();
        try {
            return  list.get(index);
        }finally {
            readLock.unlock();
        }
    }
}

在上述代码中我们稍稍做了改动,将get()方法加上了读锁,add()方法加上了写锁。整体来说可重入读写锁的使用也很简单,我们还是将重心放在原理分析上来,使用部分就不用过多篇幅描述了。

实现原理

这里我们要打乱一下顺序,我们从写锁开始分析。这样做的目的是因为我们知道只要涉及到写的操作基本都是互斥,所以都会有锁的抢占和没抢占到锁的处理。这跟我们上一小节的重入锁类似,从写锁开始会更好理解。话不多说我们进入正题

WriteLock(写锁)

还是以构造方法作为入口

//ReentrantReadWriteLock默认是非公平锁,在这个构造方法中我们可以看到读锁和写锁分别对应的是 ReadLock 和 WriteLock
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; }

先来看 WriteLock是怎么加锁的

//WriteLock 重写了Lock接口的lock()方法
public static class WriteLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -4992448646407690164L;
    private final Sync sync;
    protected WriteLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
    //写锁加锁的核心在这里
    public void lock() {
        sync.acquire(1);
    }
    //...... 代码省略
}
//看到这个acquire方法是不是非常的眼熟,在重入锁中也是这么个逻辑。先尝试获得锁,如果获取锁失败则加入等待队列
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
//与重入锁逻辑大致相同的情况下,它的获取锁的核心就在tryAcquire(arg)这个方法中。那么它依然是重写了tryAcquire()方法
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    //获取state的值
    int c = getState();
    //获取写线程的数量 这里详细内容先不做展开
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        //当c!=0,w==0则说明有其他线程获得了读锁,因为读和写互斥,所以也会抢占锁失败返回false
        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;
    }
    //判断写锁是否应该阻塞 或者直接通过cas来抢占锁
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    //设置独占线程也就是抢占到锁了
    setExclusiveOwnerThread(current);
    return true;
}

很多小伙伴可能对exclusiveCount()这个方法不是很理解,我们详细解读一下

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 6317671515068378041L;
        /*
         * Read vs write count extraction constants and functions.
         * Lock state is logically divided into two unsigned shorts:
         * The lower one representing the exclusive (writer) lock hold count,
         * and the upper the shared (reader) hold count.
         * 这段英文注释很重要 简单翻译过来就是
         *   读取与写入计数的提取常量和函数。
         *   锁状态在逻辑上被划分为两个无符号短整数:
         *   较低的一个表示独占(写)锁的持有计数,
         *   较高的一个表示共享(读)锁的持有计数。
         * 简单来说就是state即包含读锁的数,也包含写锁的数,那怎么通过一个int类型的state变量来表示的呢?
         * 众所周知 在大多数计算机系统中,int 类型通常占用 4 个字节(32 位) 而且根据下面的代码涉及到位运算,以及上面的注释描          * 述我们大致可以得出 state是用高低位来存储读锁和写锁的,高16位存读锁,低16位存写锁
         */
        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        //exclusiveCount()方法是个 &(与)运算 得到写锁的重入次数
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
        //。。。。。。。代码省略
}

这里分享一个小规律,有些小伙伴可能看到<< 或者>>符号运算比较懵。再转换二级制进行计算又比较麻烦,其实也是有规律可循。假设有 int m;int n;(以下规律仅试用于正整数)

  1. m<<n 可以等同于 m乘以2的n次方
  2. m>>n 可以等同于 m除以2的n次方

我们可以看到整体上写锁的抢占跟重入锁很接近,但也有些逻辑上的不同。抢占锁失败的处理逻辑是一样的,这里就不花篇幅再描述一遍了。

我们再看一下写锁的释放

//第一眼看到这个方法又是似曾相识的感觉,跟重入锁的释放主体是一样的。很明显主要区别就在tryRelease()方法
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        //这里的逻辑跟重入锁释放是一样的,不做重复说明了
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
//着重来看 tryRelease() 方法
protected final boolean tryRelease(int releases) {
    //isHeldExclusively()判断当前线程是不是获得锁的线程
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //是的获得锁的线程直接进行递减,这里小伙伴会有疑问,为什么不用&(与)运算了,是因为写锁存储在低位,在内存地址上它是空间连续的     //所以可以直接进行递减
    int nextc = getState() - releases;
    //判断重入次数是否为0
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        //独占线程设置为null
        setExclusiveOwnerThread(null);
    //将State赋值为0 写锁释放完成
    setState(nextc);
    return free;
}

写锁的抢占和释放自此就告一段落了,整体上比较简单。代码也不复杂。接下来的重心要转移到读锁上来

ReadLock(读锁)

先来看读锁是如何加锁的

//实现了Lock接口,重写lock()方法
public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        private final Sync sync;
        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
        public void lock() {
            sync.acquireShared(1);
        }
    //。。。。。。代码省略
}
//我们来看acquireShared() 方法
public final void acquireShared(int arg) {
    //这里代码逻辑很简单当tryAcquireShared(arg)< 0时执行 doAcquireShared(arg)
    //doAcquireShared(arg)进入这个方法会看到它是个抢占锁失败的处理逻辑,反向推导tryAcquireShared()>0才是抢占锁成功
    //所以我们重点来看tryAcquireShared() 方法
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
    //获取当前线程
    Thread current = Thread.currentThread();
    //获取state的值
    int c = getState();
    //如果写锁的数量不为0 并且独占线程不是当前线程则直接返回-1,代表当前有写锁,因为读写互斥所以抢占锁失败
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    //获取读锁的数量
    int r = sharedCount(c);
    //下面这段代码我们先从大方面来看 当readerShouldBlock()返回false 并且读锁数量小于65335 才通过cas去抢占锁 
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        //当r==0代表第一次获取读锁
        if (r == 0) {
            //当前线程赋值给firstReader
            firstReader = current;
            //记录重入次数
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            //当r!=0 判断firstReader是不是当前线程,如果是重入次数+1    
            firstReaderHoldCount++;
        } else {
            //当r!=0 ,firstReader也不是当前线程,则证明这是其他线程获得了读锁,因为state状态不能满足记录多个线程获得读锁的             //次数,所以这个地方用ThreadLocal来保存每个线程获取读锁的次数
            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;
    }
    //抢占锁失败调用fullTryAcquireShared()
    return fullTryAcquireShared(current);
}

在上述代码中我们从大方面初步了解了读锁是如何加锁,其中有些细节我们来做一个补充

  1. readerShouldBlock()

    readerShouldBlock()方法我们在阅读源码是会发现它是个策略模式,有公平锁和非公平锁的对应方法
    在这里插入图片描述

    //公平锁
    static final class FairSync extends Sync {
            private static final long serialVersionUID = -2274990926593161451L;
            final boolean writerShouldBlock() {
                return hasQueuedPredecessors();
            }
            final boolean readerShouldBlock() {
                //公平锁的情况下逻辑就很简短,就是判断有没有等待队列,没有才能去抢占锁
                return hasQueuedPredecessors();
            }
    }
    //非公平锁
    static final class NonfairSync extends Sync {
            private static final long serialVersionUID = -8159625535654395037L;
            //这里代表默认情况下都去抢占写锁
            final boolean writerShouldBlock() {
                return false; // writers can always barge
            }
            final boolean readerShouldBlock() {
                return apparentlyFirstQueuedIsExclusive();
            }
    }
    //主要看apparentlyFirstQueuedIsExclusive()方法
    //在看这个方法之前我们来回忆一下Node的两个属性,独占和共享,独占我们之前了解过在重入锁addWaiter(Node.EXCLUSIVE)中没有获得锁的线程加入等待队列这时候该节点是EXCLUSIVE
    //在代码中是这样体现的 
    //static final Node SHARED = new Node();
    //static final Node EXCLUSIVE = null;
    final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        //判断头节点和头节点下一个节点的情况
        return (h = head) != null &&
            (s = h.next)  != null &&
            //isShared()这个方法很重要判断头节点的下一个节点是不是共享节点
            !s.isShared()         &&
            s.thread != null;
    }
    //如果队列头节点的下一个节点是独占节点,也就是说isShared()返回false,readerShouldBlock()== true此时当前线程抢占锁失败需要加入队列中
    //如果队列头节点的下一个节点是共享节点,isShared()返回true 也说明readerShouldBlock()== false
    /**
       if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT))
       这个抢占锁的条件成立,当前要获得读锁的线程可以通过CAS去抢占锁
     */
    //这样做的好处是避免了写锁一直等待的情况
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    
  2. HoldCounter rh = cachedHoldCounter

    HoldCounter 属性有count和tid,count是记录的重入次数,tid表示线程的id

    static final class HoldCounter {
        int count = 0;
        final long tid = getThreadId(Thread.currentThread());
    }
    

    为了能使每个线程都有对应的重入次数,又用了ThreadLocal做了隔离

    private transient ThreadLocalHoldCounter readHolds;
    static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
    return new HoldCounter();
    }
    }
    
  3. fullTryAcquireShared(current)

    抢占锁失败的时候还调用了fullTryAcquireShared() 方法,我们来看一下这个方法做了什么

    final int fullTryAcquireShared(Thread current) {
        HoldCounter rh = null;
        for (;;) {//自旋操作
            //获取state的值
            int c = getState();
            //判断有没有写锁,如果有写锁且不是当前线程基于读和写互斥直接返回-1
            if (exclusiveCount(c) != 0) {
                if (getExclusiveOwnerThread() != current)
                    return -1;
            //如果readerShouldBlock()==true 则表示当前抢占锁的线程需要排队    
            } else if (readerShouldBlock()) {
                //这是个断言
                if (firstReader == current) {
                } else {
                    if (rh == null) {
                        rh = cachedHoldCounter;
                        //如果HoldCounter 是null 或者HoldCounter中的tid不等于当前线程的tid
                        if (rh == null || rh.tid != getThreadId(current)) {
                            //从readHolds找到当前当前线程tid的HoldCounter
                            rh = readHolds.get();
                            //如果count==0,则从readHolds剔除掉
                            if (rh.count == 0)
                                readHolds.remove();
                        }
                    }
                    //此处证明当前抢占锁的线程不是重入
                    if (rh.count == 0)
                        return -1;
                }
            }
            //判断读锁的总数是否大于最大值
            if (sharedCount(c) == MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            //这个地方又是去抢占锁,这里的临界条件是刚好有写锁释放,逻辑跟tryAcquireShared()类似就不再重复了
            if (compareAndSetState(c, c + SHARED_UNIT)) {
                if (sharedCount(c) == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    if (rh == null)
                        rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                    cachedHoldCounter = rh; // cache for release
                }
                return 1;
            }
        }
    }
    

以上是读锁的加锁过程,我们接着来看读锁的释放

public void unlock() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    //很明显主要逻辑在tryReleaseShared(arg)中
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    //判断firstReader是不是当前线程
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        //如果是当前线程再获取firstReaderHoldCount是否等于1,等于1的话证明没有其他线程获得读锁直接赋值null并递减数量
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        //下面的代码很简单就是判断cachedHoldCounter中的tid是不是当前线程,如果不是就从readHolds获取
        //释放锁得需要清理ThreadLocal中的数据
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    //自旋操作不断递减state的值直到state为0
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}
小结

纯看代码一点点抠细节很多小伙伴看起来会非常的乱,也容易忘记前后关联。所以在阅读过源码之后我们再来理一遍思路,这样能有一个更深刻的认知

  1. 假设有两个线程ThreadA ,ThreadB去获取读锁。此时没有其他线程获取写锁所以ThreadA和ThreadB获取读锁成功。

    firstReader = ThreadA ,firstReaderHoldCount = 1 ThreadB则是用HoldCounter 来记录tid和count。用图来表示此时的情况是这样的
    在这里插入图片描述

  2. 当ThreadC来获取写锁,基于读和写互斥的原则,ThreadC是抢占锁失败的,所以需要把它加入等待队列中,队列中的ThreadC是 Exclusive节点,反之要是读锁加入队列就是Shared节点
    在这里插入图片描述

  3. 如果此时又有一个线程ThreadD来抢占读锁,它不满足直接抢占读锁的条件,需要加入等待队列,原因就是我们之前源码分析过程中的readerShouldBlock() 方法,我们知道是公平锁的时候判断有没有等待队列,显然有等待队列并且ThreadC在队列中。若是非公平锁则要判断队列的头结点的下一个节点s.isShared() 是不是共享节点,很明显ThreadC是Exclusive节点。所以ThreadD抢占读锁失败,也需要加入等待队列中,此时的队列是这样的
    在这里插入图片描述

  4. 当ThreadA和ThreadB这时候释放了读锁,那就会从等待队列中唤醒头节点的下一个节点。也就是ThreadC,ThreadC是获取写锁在获得锁之后把原来的头节点移除掉,包装ThreadC的这个节点作为头节点。此时state的低位为1,ExclusiveOwnerThread== ThreadC,此时的队列是这样的
    在这里插入图片描述

  5. 如果ThreadC写锁也释放了之后,则继续唤醒队列中头结点的下一个节点ThreadD,ThreadD 是Shared节点,这种节点有一个特点,如果队列中有一个Shared节点被唤醒,那么会把后续所有的Shared节点都唤醒,允许多个线程抢占读锁。直到下一个节点是Exclusive节点时停止唤醒。

  • 17
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值