第三章 显式锁和AQS笔记

谈到并发不得不谈ReentrantLock,而谈到ReentrantLock不得不谈AbstractQueuedSynchronizer(AQS)。类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。
state的访问方式有三种:getState(),setState(),compareAndSetState()。

一、AQS定义两种资源共享方式:

Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。不同的自定义同步器争用共享资源的方式也不同,自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式,尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式,尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
  • 再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
在这里插入图片描述
实现自定义同步组件时,将会调用同步器提供的模板方法:
在这里插入图片描述
这些模板方法同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放、同步状态和查询同步队列中的等待线程情况。

函数流程如下:

tryAcquire()尝试直接去获取资源,如果成功则直接返回(该方法可重写)
addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

可重写的方法:

在这里插入图片描述

访问或修改同步状态的方法:

重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。
• getState():获取当前同步状态。
• setState(int newState):设置当前同步状态。
• compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
实现一个自己的独占锁参见代码thread-platform/src/com/chj/thread/cpat04/aqs/SelfLock.java

二、深入源码

AQS中的数据结构—节点和同步队列—节点Node
在这里插入图片描述
既然说Java中的AQS是CLH队列锁的一种变体实现,毫无疑问作为队列来说,必然要有一个节点的数据结构来保存我们前面所说的各种域,比如前驱节点,节点的状态等,这个数据结构就是AQS中的内部类Node。作为这个数据结构应该关心些什么信息?

  • 1)、线程信息,肯定要知道我是哪个线程;
  • 2)、队列中线程状态,既然知道是哪一个线程,肯定还要知道线程当前处在什么状态,是已经取消了“获锁”请求,还是在“等待中”,或者说“即将得到锁”。
  • 3)、前驱和后继线程,因为是一个等待队列,那么也就需要知道当前线程前面的是哪个线程,当前线程后面的是哪个线程(因为当前线程释放锁以后,理当立马通知后继线程去获取锁)。

所以这个Node类是这么设计的:
在这里插入图片描述
其中包括了线程的2种等待模式:

  • SHARED:表示线程以共享的模式等待锁(如ReadLock)
  • EXCLUSIVE:表示线程以互斥的模式等待锁(如ReetrantLock),互斥就是一把锁只能由一个线程持有,不能同时存在多个线程使用同一个锁。

线程在队列中的状态枚举

  • CANCELLED:值为1,表示线程的获锁请求已经“取消”
  • SIGNAL:值为-1,表示该线程一切都准备好了,就等待锁空闲出来给我
  • CONDITION:值为-2,表示线程等待某一个条件(Condition)被满足
  • PROPAGATE:值为-3,当线程处在SHARED模式时,该字段才会被使用上初始化Node对象时,默认为0

1、成员变量:

  • waitStatus:该int变量表示线程在队列中的状态,其值就是上述提到的CANCELLED、SIGNAL、CONDITION、PROPAGATE
  • prev:该变量类型为Node对象,表示该节点的前一个Node节点(前驱)
  • next:该变量类型为Node对象,表示该节点的后一个Node节点(后继)
  • thread:该变量类型为Thread对象,表示该节点的代表的线程
  • nextWaiter:该变量类型为Node对象,表示等待condition条件的Node节点

当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点。

  • head和tail:AQS还拥有首节点(head)和尾节点(tail)两个引用,一个指向队列头节点,而另一个指向队列尾节点。
    注意:因为首节点head是不保存线程信息的节点,仅仅是因为数据结构设计上的需要,在数据结构上,这种做法往往叫做“空头节点链表”,对应的就有“非空头结点链表”。

2、节点在同步队列中的增加和移出

2.1 节点加入到同步队列

当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,也就是获取同步状态失败,AQS会将这个线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列的尾部。而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Nodeupdate),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。

2.2 首节点的变化:

首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。

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

获取—通过调用同步器的acquire(int arg)方法可以获取同步状态,主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:

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

AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS),至于能不能重入,能不能加塞,那就看具体的自定义同步器怎么去设计了!当然,自定义同步器在进行资源访问时要考虑线程安全的影响。首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法需要保证线程安全的获取同步状态。
如果同步状态获取失败(tryAcquire返回false),则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
addWaiter(Node node)方法中:

 private Node addWaiter(Node mode) {
  		// 以给定的模式构造节点,mode有两种:独占,共享
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure 尝试快速方式直接放到队尾
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node); // 上一步失败则通过enq入队
        return node;
    }

将当前线程包装成Node后,队列不为空的情况下,先尝试把当前节点加入队列并成为尾节点,如果不成功或者队列为空进入enq(final Node node)方法。

 private Node enq(final Node node) {
        for (;;) { // CAS自旋,直到成功加入队尾
            Node t = tail;
            // 如果队里为空,创建一个空的标致节点作为head节点,并将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;
                }
            }
        }
    }

在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,这个死循环中,做了两件事,第一件,如果队列为空,初始化队列,new出一个空节点,并让首节点(head)和尾节点(tail)两个引用都指向这个空节点;第二件事,把当前节点加入队列,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。
节点进入同步队列之后,观察acquireQueued(Node node,int arg)方法
在这里插入图片描述
到这里了,我们先不急着总结acquireQueued()的函数流程,先看看shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()具体干些什么。shouldParkAfterFailedAcquire(Node, Node) 此方法主要用于检查状态,看看自己是否真的可以去休息,万一队列前边的线程都放弃了只是瞎站着,那也说不定!

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; // 拿到前驱的状态
        if (ws == Node.SIGNAL)
            // This node has already set status asking a release to signal it, so it can safely park.
            return true;
        if (ws > 0) {
            // Predecessor was cancelled. Skip over predecessors and indicate retry.
            // 如果前驱放弃了,那就一直往前找,直到找到最近几个正常等待的状态,并排在他的后面。
            // 注意:那些放弃的节点,有雨被自己‘加塞’到他们的前边,他们相当于形成一个无引用链,后面会被GC
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // waitStatus must be 0 or PROPAGATE.  Indicate that we  need a signal, but don't park yet.  Caller will need to
             // retry to make sure it cannot acquire before parking.
             // 如果前驱正常,那就把前驱的状态设置为signal,告诉它获取以后通知自己一下,也有可能失败,刚好释放完
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。

private final boolean parkAndCheckInterrupt() { 	
	LockSupport.park(this);//调用park()使线程进入waiting状态     
	return 	Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。 
}

其实就是一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)。
在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,这是为什么?原因有两个。

  • 第一,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
  • 第二,维护同步队列的FIFO原则。

当前线程获取到同步状态后,让首节点(head)这个引用指向自己所在节点。当同步状态获取成功后,当前线程就从acquire方法返回了。如果同步器实现的是锁,那就代表当前线程获得了锁。

3、释放

当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。

public final boolean release(long arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h); // 唤醒等待队列里的下一个线程
            return true;
        }
        return false;
    }

该方法执行时,会唤醒首节点(head)所指向节点的后继节点线程,unparkSuccessor(Node node)方法使用LockSupport来唤醒处于等待状态的线程,而unparkSuccessor方法用于唤醒等待队列中下一个线程

private void unparkSuccessor(Node node) {
        // If status is negative (i.e., possibly needing signal) try  to clear in anticipation of signalling.  
        // It is OK if thisfails or if status is changed by waiting thread.
        int ws = node.waitStatus; // node一般为当前节点
        if (ws < 0) // 当前线程状态置0,允许失败
            compareAndSetWaitStatus(node, ws, 0);
        /*
         * Thread to unpark is held in successor, which is normally just the next node. But if cancelled or apparently null,
         * traverse backwards from tail to find the actual non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) { // 如果为空或者已取消
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0) // 从这里可以看出,《=0节点,都是还有效的节点
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

这段代码的意思,一般情况下,被唤醒的是head指向节点的后继节点线程,如果这个后继节点处于被cancel状态,(我推测开发者的思路这样的:后继节点处于被cancel状态,意味着当锁竞争激烈时,队列的第一个节点等了很久(一直被还未加入队列的节点抢走锁),包括后续的节点cancel的几率都比较大,所以)先从尾开始遍历,找到最前面且没有被cancel的节点。

总结

在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒head指向节点的后继节点。

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

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以读写为例,如果一个程序在进行读操作,那么这一时刻写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问。
在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0。可以看到,在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。
该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。
共享式的同步工具类:设计一个同步工具:该工具在同一时刻,只允许至多3个线程同时访问,超过3个线程的访问将被阻塞。
首先,确定访问模式。TrinityLock能够在同一时刻支持多个线程的访问,这显然是共享式访问,因此,需要使用同步器提供的acquireShared(int args)方法等和Shared相关的方法,这就要求TwinsLock必须重写tryAcquireShared(int args)方法和tryReleaseShared(int args)方法,这样才能保证同步器的共享式同步状态的获取与释放方法得以执行。
其次,定义资源数。TrinityLock在同一时刻允许至多三个线程的同时访问,表明同步资源数为3,这样可以设置初始状态status为3,当一个线程进行获取,status减1,该线程释放,则status加1,状态的合法范围为0、1和2,3,其中0表示当前已经有3个线程获取了同步资源,此时再有其他线程对同步状态进行获取,该线程只能被阻塞。在同步状态变更时,需要使用compareAndSet(int expect,int update)方法做原子性保障。
最后,组合自定义同步器。前面的章节提到,自定义同步组件通过组合自定义同步器来完成同步功能,一般情况下自定义同步器会被定义为自定义同步组件的内部类。

三、了解Condition的实现

1、Condition的数据结构

等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类。
在这里插入图片描述
一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列
在这里插入图片描述
调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。
如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。
在这里插入图片描述
如图所示,同步队列的首节点并不会直接加入等待队列,而是通过addConditionWaiter()方法把当前线程构造成一个新的节点并将其加入等待队列中。
在这里插入图片描述
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。
调用该方法的前置条件是当前线程必须获取了锁,可以看到signal()方法进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。
通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。
被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法返回true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中。
成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁。
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。

2、锁的可重入

2.1、Lock的实现、ReentrantLock的实现

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。

  • 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是则再次成功获取。
  • 锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

nonfairTryAcquire方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。同步状态表示锁被一个线程重复获取的次数。
如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

2.2 公平和非公平锁

ReentrantLock的构造函数中,默认的无参构造函数将会把Sync对象创建为NonfairSync对象,这是一个“非公平锁”;而另一个构造函数ReentrantLock(boolean fair)传入参数为true时将会把Sync对象创建为“公平锁”FairSync。
nonfairTryAcquire(int acquires)方法,对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同。tryAcquire方法,该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
改造我们的独占锁为可重入:参见代码cn.enjoyedu.ch4.aqs.ReenterSelfLock

2.3、ReentrantReadWriteLock的实现

1)读写状态的设计**

读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。
回想ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。
如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写,读写锁是如何迅速确定读和写各自的状态呢?
答案是通过位运算。假设当前同步状态值为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000。根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
写锁的获取与释放

2)写锁是一个支持重进入的排它锁。

如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。
写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。
读锁的获取与释放

3)读锁是一个支持重进入的共享锁

它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。
如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护。在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态。
锁的升降级

4)锁降级指的是写锁降级成为读锁。

如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值