并发编程四(AQS、ReentrantLock、ReadWriteLock接口和ReetrantReadWriteLock实现类)

Java并发编程核心在于java.concurrent.util包而juc当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。

1. AQS

概述:AQS(AbstractQueuedSynchronizer,抽象队列同步器)是Java中用于构建各种同步器的抽象公共类。它提供了一种通用的同步器框架,可以用于实现互斥锁、读写锁、信号量等不同类型的同步器。

原理: FIFO等待队列 + 同步状态state字段,AQS使用了一个FIFO(先进先出)的等待队列来管理等待线程。它通过维护一个同步状态volatile int state(synchronization state)来表示共享资源的状态,并提供了一组原子操作方法来管理同步状态和等待队列。子类通过继承AQS实现它的方法来管理其状态。

  • 同步状态state字段:AQS内部维护一个同步状态(synchronization state),通过一个整型变量表示。同步状态代表了共享资源的状态,可以是0或正整数,也可以是其他自定义值。
  • 等待队列(Wait Queue):AQS使用一个FIFO(先进先出)的等待队列来管理等待线程。等待队列是一个双向链表,每个节点表示一个等待线程,通过前驱和后继节点的引用连接起来。

AQS(AbstractQueuedSynchronizer)内部使用了一个FIFO(先进先出)的等待队列来管理等待线程。这个等待队列是AQS实现同步的关键数据结构之一。

  1. 当一个线程无法获取锁或条件不满足时,它会被加入到AQS的等待队列中,进入等待状态。等待队列中的线程按照先到先服务的顺序排列,即先加入等待队列的线程会先被唤醒。
  2. 等待队列的核心结构是一个双向链表,其中的每个节点表示一个等待线程。每个节点包含了等待线程的引用,以及前驱节点和后继节点的引用。这种双向链表的结构使得线程可以高效地加入和退出等待队列。
  • 获取锁:当一个线程需要获取锁时,它会调用AQS的acquire()方法。这个方法会尝试获取同步状态,如果同步状态state字段满足条件(例如为0),线程就可以获取锁,并继续执行;否则,线程会被加入到等待队列中,进入等待状态。
  • 释放锁:当一个线程需要释放锁时,它会调用AQS的release()方法。这个方法会释放同步状态,并唤醒等待队列中的一个或多个线程,使它们可以尝试获取锁。
  • 状态更新和线程调度:在AQS中,同步状态的更新是通过 CAS(Compare and Swap)操作来保证原子性和线程安全性的。当一个线程成功获取锁或释放锁时,会更新同步状态,并相应地调整等待队列中的线程状态。同时,AQS使用自旋(spin)来避免线程阻塞和唤醒操作的开销,提高性能。

通过同步状态、等待队列、获取锁和释放锁的机制,AQS实现了一种通用的同步器框架。在AQS的基础上,Java提供了许多具体的同步器,如ReentrantLock、CountDownLatch、Semaphore等,开发人员可以根据需要选择合适的同步器,并通过继承AQS并实现特定的方法来创建自定义的同步器。

总之,AQS的原理涉及同步状态的管理、等待队列的管理以及获取锁和释放锁的过程。它为Java并发编程提供了灵活而强大的同步和并发控制机制。

1.1 CAS

CAS(Compare and Swap)是AQS(AbstractQueuedSynchronizer)中用于实现同步状态的更新的关键操作。CAS是一种原子操作,用于解决多线程并发访问共享数据时的线程安全问题。

CAS操作包括三个操作数:内存位置(或称为变量)、旧的预期值和新的值。它的执行过程如下:

  1. 首先,读取内存位置的当前值作为旧的预期值。
  2. 比较旧的预期值与内存位置的当前值是否相等。
  3. 如果相等,说明没有其他线程修改过该值,将新的值写入内存位置。
  4. 如果不相等,说明其他线程已经修改了该值,操作失败。

CAS操作是原子的,意味着在CAS过程中不会被其他线程中断或修改内存位置的值。因此,CAS操作可以保证在多线程环境下的数据一致性和线程安全性。

AQS利用CAS操作来更新同步状态,实现并发控制和线程同步。例如,在获取锁时,使用CAS操作来判断同步状态是否为0,如果为0,则将同步状态设置为1表示获取成功;如果不为0,说明有其他线程已经获取了锁,当前线程需要加入等待队列等待。类似地,在释放锁时,也使用CAS操作来将同步状态设置为0,并唤醒等待队列中的线程。

CAS操作在并发编程中具有较低的开销,因为它不需要加锁或阻塞线程。然而,CAS操作也存在一些问题,如ABA问题(当内存位置的值在操作过程中被修改多次,但最终又恢复为原始值时,CAS操作无法感知到这个变化)。为了解决这些问题,Java提供了带有标记(或版本号)的CAS操作,如AtomicStampedReference和AtomicMarkableReference。

总之,CAS是AQS中用于实现同步状态更新的关键操作,它通过比较预期值和当前值来判断是否可以更新,并保证在多线程环境下的线程安全性和数据一致性。

1.2 AQS特性

AQS的设计具有以下特点

  • 支持独占模式和共享模式:AQS可以根据具体需求实现独占锁或共享锁,从而支持不同类型的同步器。
    • Exclusive-独占,只有一个线程能执行,如ReentrantLock
    • Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
  • 可以被子类扩展:AQS使用模板方法模式,允许子类通过实现特定方法来创建自定义的同步器。
  • 提供了等待/通知机制:AQS内部提供了Condition接口,支持线程间的等待和通知机制。
  • 内置的公平性保证:AQS的等待队列采用FIFO顺序,从而天然具备公平性。

应用场景:AQS在Java并发编程中具有广泛的应用场景。一些常见的使用方式包括:

  • ReentrantLock:ReentrantLock是基于AQS实现的可重入锁,提供了更多功能和灵活性,可以替代synchronized关键字进行线程同步。
  • CountDownLatch:CountDownLatch是AQS的一个应用,用于实现等待其他线程完成的场景。
  • Semaphore:Semaphore也是AQS的一个应用,用于实现限制资源访问数量的场景。

除了Lock外,Java.concurrent.util当中同步器的实现如Latch、Barrier、BlockingQueue等,都是基于AQS框架实现

  • 一般通过定义内部类Sync继承AQS
  • 将同步器所有调用都映射到Sync对应的方法

AQS内部维护属性volatile int state (32位),state表示资源的同步状态。state三种访问方式

  • getState()
  • setState()
  • compareAndSetState()

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

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

1.3 AQS源码分析

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    protected AbstractQueuedSynchronizer() { }

    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;
	volatile Thread thread;
	Node nextWaiter;
	
	final boolean isShared() {
    	     return nextWaiter == SHARED;
	}

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

    private transient volatile Node head;

    private transient volatile Node tail;

    private volatile int state;

    protected final int getState() {
        return state;
    }

    protected final void setState(int newState) {
        state = newState;
    }

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

    ......

}

1.4 AQS的常用实现类

  1. ReentrantLock:ReentrantLock是基于AQS实现的可重入锁。它提供了与synchronized关键字类似的互斥性,但具有更多的功能和灵活性。ReentrantLock支持公平锁和非公平锁两种模式,并提供了可重入、条件变量等特性。
  2. CountDownLatch:CountDownLatch是AQS的一个应用,用于实现线程等待其他线程完成的场景。它的构造函数接收一个计数值,当计数值减为0时,等待线程就会被唤醒。CountDownLatch可以用于实现线程协作、任务并行等。
  3. Semaphore:Semaphore也是AQS的一个应用,用于实现限制资源访问数量的场景。它维护一个信号量,表示可用的许可证数量。线程可以通过acquire()方法获取许可证,如果许可证数量不足,则线程会进入等待状态;而通过release()方法释放许可证,使得其他线程可以获取。
  4. ReentrantReadWriteLock:ReentrantReadWriteLock是基于AQS实现的读写锁。它允许多个线程同时读取共享数据,但在写操作时需要独占访问。ReentrantReadWriteLock提供了更高的并发性,适用于读多写少的场景。

这些是AQS的一些常见实现类,它们通过继承AQS并实现特定的方法来创建不同类型的同步器。这些实现类提供了不同的并发控制机制,可以根据具体需求选择合适的同步器,并结合AQS的机制进行使用。通过这些实现类,开发人员可以在Java并发编程中实现线程同步、并发控制和协作等功能。

2. ReentrantLock

2.1 ReentrantLock特性

ReentrantLock是一种基于AQS的实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。而且它具有比synchronized更多的特性,比如它支持手动加锁与解锁,支持加锁的公平性。ReentrantLock是Lock的实现类之一,是一个同步的互斥器,它具有扩展的能力。

使用ReentrantLock进行同步
ReentrantLock lock = new ReentrantLock(false);//false为非公平锁,
true为公平锁
lock.lock() //加锁
lock.unlock() //解锁

注意:可能会忘记释放锁。(必须在finally块中保证每次用完都释放锁)

2.2 ReentrantLock底层原理

ReentrantLock通过AQS的子类Sync来实现可重入互斥锁的控制。

  1. Sync类:这是ReentrantLock的主要内部类,它扩展了AQS类并实现了可重入互斥锁的基本逻辑。Sync类维护了一个状态变量state来表示锁的状态,同时也记录了当前持有锁的线程和重入次数。
    • 获取锁:当一个线程请求锁时,Sync会首先检查当前状态state。如果state为0,表示锁当前是未被持有的,线程可以获取锁并将state设为1,同时记录持有锁的线程为当前线程。如果state不为0且持有锁的线程为当前线程,表示当前线程已经持有锁,可以直接增加重入次数。如果state不为0且持有锁的线程不是当前线程,则当前线程会使用AQS的同步机制进入等待队列,直到获取到锁。
    • 释放锁:当一个线程释放锁时,Sync会首先减少重入次数。如果重入次数不为0,表示当前线程仍然持有锁,不会释放锁。只有当重入次数为0时,才会将state设为0,并根据AQS的同步机制唤醒等待队列中的线程。
  2. 非公平性:ReentrantLock默认是非公平锁,即线程在竞争锁时不保证按照先到先得的顺序获取锁。在非公平锁的情况下,当一个线程释放锁时,可能会允许当前没有竞争的线程直接获取到锁。这样可以提高系统的吞吐量,但也可能导致某些线程长时间等待。
  3. 公平性:ReentrantLock也支持公平锁,可以通过在构造ReentrantLock对象时传入true来指定使用公平锁。在公平锁的情况下,线程获取锁的顺序将按照先到先得的顺序进行。

ReentrantLock使用Sync类来实现可重入互斥锁的逻辑,通过AQS的同步机制来管理线程的等待和唤醒。

2.3 ReentrantLock和synchronized的区别

相同点:

  • 它们都是加锁方式同步
  • 都是可重入锁:同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
  • 阻塞式的同步;也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的

不同点

  • ReentrantLock可以获取锁的各种信息;
  • 加锁:synchronized是Java语言层面的关键字,加锁解锁依赖于JVM实现;ReentrantLock是jdk1.5之后提供的API层面的实现类,必须手动加锁和释放锁
  • 异常释放锁:发生异常时,synchronized会自动释放锁,而Lock的对象不会自动释放锁,需要手动来保证释放锁(这也是为什么必须把unlock()方法放在finally块中的原因)。
  • 线程通信:ReentrantLock可以灵活的实现多路通知(ReentrantLock和Condition结合可以实现“选择性通知”)。synchronized相当于只有一个单一的Condition对象,被通知的线程是由JVM随机选择的。
  • 是否公平锁:synchronized是非公平锁,不可改变。ReentrantLock默认也是非公平锁,但是ReentrantLock的构造器可以传入boolean值来指定是否是公平锁:true为公平锁,false为非公平锁(公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO,公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足,通常情况下没有非公平锁的效率高)
  • ReentrantLock可以进行尝试锁定tryLock(),如果无法锁定。或者在指定时间内无法锁定,线程可以决定是否继续等待。

3. ReadWriteLock接口和ReetrantReadWriteLock实现类

ReadWriteLock接口和ReentrantReadWriteLock实现类是Java中用于实现读写锁的关键组件。

ReadWriteLock接口提供了readLock(读锁)和writeLock(写锁)两种锁的操作机制:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程或写写线程。ReadWriteLock接口定义了读写锁的基本操作。它包含两个关键方法:

  • Lock readLock(): 返回一个用于读操作的锁。多个线程可以同时获取读锁,以实现共享读取。
  • Lock writeLock(): 返回一个用于写操作的锁。写锁是独占锁,一次只能有一个线程获取写锁,以实现独占写入。

ReentrantReadWriteLock是ReadWriteLock接口的主要实现类。它是基于AQS(AbstractQueuedSynchronizer)实现的,提供了读写锁的功能。

3.1 ReetrantReadWriteLock实现类的特性、方法

  • 读锁共享,写锁独占:多个线程可以同时获取读锁,但写锁只能被一个线程独占。 读写锁的机制: 读-读不互斥,读-写互斥、写-写互斥
  • 公平性:ReentrantReadWriteLock提供了公平和非公平两种模式。在公平模式下,等待时间较长的线程会优先获取锁。默认是非公平锁,这个和ReentrantLock独占锁是一样的。但对于多个读线程之间没有锁竞争,所以读操作没有公平性与非公平性,但是写操作时是存在锁竞争的,就可以通过传入参数来决定是公平锁还是非公平锁;
  • 可重入:与ReentrantLock类似,ReentrantReadWriteLock的读锁和写锁都支持可重入,同一个线程可以多次获取同一个锁。写线程释放了锁,读线程才能获取重入锁。写线程获取写锁后可以再次获取锁,但是读线程获取读锁后不能获得写锁。读写锁最多支持65535个递归写锁和65535个递归读锁。
  • 锁降级:写线程获取写入锁后可以获取读取锁,然后释放写锁,这样就从写锁变成了读锁,从而实现锁降级的特性。
  • 锁升级:读锁是不能直接升级为写锁的。因为获得一个写锁需要释放所有读锁,所以如果有两个读锁试图获得写锁,且都不释放读锁时就会发生死锁。
  • 锁获取中断:读锁和写锁都支持获取锁期间被打断。这个和独占锁ReentrantLock一致。

方法:

  • readLock(): 返回一个用于读操作的锁。
  • writeLock(): 返回一个用于写操作的锁。
  • getReadLockCount(): 获取当前持有读锁的线程数。
  • getWriteHoldCount(): 获取当前持有写锁的线程数。
  • getQueueLength(): 获取正在等待获取锁的线程数。

ReentrantReadWriteLock的设计目的是优化读多写少的场景。通过允许多个线程同时获取读锁,可以提高并发性能。只有在需要进行写操作时,才需要独占写锁。

读写锁适用于数据读多写少、读操作不会修改数据的场景。它可以提供更高的并发性和吞吐量,避免了多个读线程之间的互斥,但在写操作时需要独占锁,确保数据的一致性。

总之,ReadWriteLock接口和ReentrantReadWriteLock实现类提供了读写锁的功能,可以在多线程环境下实现读多写少的数据访问控制,提高并发性能和吞吐量。

  • ReetrantReadWriteLock 读写锁的效率明显高于synchronized和ReentrantLock。
  • 读-读不互斥,读-写互斥,写-写互斥: ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的;

3.2 ReetrantReadWriteLock原理

ReentrantReadWriteLock的底层实现是基于AbstractQueuedSynchronizer (AQS)类。AQS是Java中用于实现同步器(synchronizer)的框架,它提供了一个灵活的基础,可以支持各种同步机制,包括互斥锁、条件变量等。

ReentrantReadWriteLock通过AQS的子类Sync来实现读写锁的控制。具体来说,ReentrantReadWriteLock包含两个内部类:Sync和FairSync。

  1. Sync类:这是ReentrantReadWriteLock的主要内部类,它扩展了AQS类并实现了读写锁的基本逻辑。Sync类维护了两个状态变量,一个表示写锁的数量(即写锁的重入次数),另一个表示读锁的数量。
    • 获取读锁:当一个线程请求读锁时,Sync会首先检查当前线程是否已经持有写锁,如果是,则可以直接获取读锁,增加读锁的持有数。否则,Sync会使用AQS的同步机制,如果存在其他线程持有写锁,当前线程将会进入等待队列,直到获取到读锁。
    • 释放读锁:当一个线程释放读锁时,Sync会减少读锁的持有数,并根据读锁的持有数决定是否需要唤醒等待队列中的线程。
    • 获取写锁:当一个线程请求写锁时,Sync会首先判断当前线程是否已经持有写锁,如果是,则增加写锁的持有数。如果当前线程没有持有写锁,则会使用AQS的同步机制,如果存在其他线程持有读锁或写锁,当前线程将会进入等待队列,直到获取到写锁。
    • 释放写锁:当一个线程释放写锁时,Sync会将写锁的持有数减少,并根据持有数决定是否需要唤醒等待队列中的线程。
  2. FairSync类:这是Sync类的子类,它实现了公平锁的逻辑。公平锁会按照线程的请求顺序来获取锁,即先到先得。在ReentrantReadWriteLock中,默认使用的是非公平锁,FairSync类的存在是为了提供公平锁的选择。

ReentrantReadWriteLock使用Sync类(或FairSync类)来实现读写锁的控制逻辑,通过AQS的同步机制来管理线程的等待和唤醒。这种设计使得ReentrantReadWriteLock能够灵活地支持读锁和写锁的并发访问,以及重入性的特性。同时,AQS提供了高效的线程同步机制,使得ReentrantReadWriteLock在高并发场景下表现出色。

3.3 ReetrantReadWriteLock简单使用

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {

    public static void main(String[] args) {
        ReadWriteLockDemo rw = new ReadWriteLockDemo();

        //100个线程并发读
        for(int i=0;i<100;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    rw.getI();
                }
            }).start();
        }

        //1个线程写
        new Thread(new Runnable() {
            @Override
            public void run() {
                rw.setI(60);
            }
        },"Write: ").start();
    }
}

class ReadWriteLockDemo{

    private int number = 18;

    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //读
    public void getI(){
        //获取读锁
        readWriteLock.readLock().lock();

        try{
            System.out.println(Thread.currentThread().getName()+" 正在读取number,,,  ");
            System.out.println(Thread.currentThread().getName()+"   "+number);
        }finally {
            readWriteLock.readLock().unlock();//释放读锁
        }

    }

    //写
    public void setI(int number){
        //获取写锁
        readWriteLock.writeLock().lock();

        try{
            System.out.println(Thread.currentThread().getName()+"设置的值为"+number);
            this.number = number;
        }finally {
            readWriteLock.writeLock().unlock();//释放写锁
        }
    }

}

运行结果如下:

......//省略部分结果
Thread-98 正在读取number,,,  
Thread-98   18
Thread-46 正在读取number,,,  
Thread-46   18
Thread-30 正在读取number,,,  
Thread-30   18
Write: 设置的值为60
Thread-90 正在读取number,,,  
Thread-90   60
Thread-89 正在读取number,,,  
Thread-82 正在读取number,,,  
Thread-92 正在读取number,,,  
Thread-92   60
......//省略部分结果

分析:可以看出,读-读并不是互斥的;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值