文章目录
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实现同步的关键数据结构之一。
- 当一个线程无法获取锁或条件不满足时,它会被加入到AQS的等待队列中,进入等待状态。等待队列中的线程按照先到先服务的顺序排列,即先加入等待队列的线程会先被唤醒。
- 等待队列的核心结构是一个双向链表,其中的每个节点表示一个等待线程。每个节点包含了等待线程的引用,以及前驱节点和后继节点的引用。这种双向链表的结构使得线程可以高效地加入和退出等待队列。
- 获取锁:当一个线程需要获取锁时,它会调用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操作包括三个操作数:内存位置(或称为变量)、旧的预期值和新的值。它的执行过程如下:
- 首先,读取内存位置的当前值作为旧的预期值。
- 比较旧的预期值与内存位置的当前值是否相等。
- 如果相等,说明没有其他线程修改过该值,将新的值写入内存位置。
- 如果不相等,说明其他线程已经修改了该值,操作失败。
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
- Exclusive-独占,只有一个线程能执行,如
- 可以被子类扩展: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的常用实现类
- ReentrantLock:ReentrantLock是基于AQS实现的可重入锁。它提供了与synchronized关键字类似的互斥性,但具有更多的功能和灵活性。ReentrantLock支持公平锁和非公平锁两种模式,并提供了可重入、条件变量等特性。
- CountDownLatch:CountDownLatch是AQS的一个应用,用于实现线程等待其他线程完成的场景。它的构造函数接收一个计数值,当计数值减为0时,等待线程就会被唤醒。CountDownLatch可以用于实现线程协作、任务并行等。
- Semaphore:Semaphore也是AQS的一个应用,用于实现限制资源访问数量的场景。它维护一个信号量,表示可用的许可证数量。线程可以通过acquire()方法获取许可证,如果许可证数量不足,则线程会进入等待状态;而通过release()方法释放许可证,使得其他线程可以获取。
- 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来实现可重入互斥锁的控制。
- Sync类:这是ReentrantLock的主要内部类,它扩展了AQS类并实现了可重入互斥锁的基本逻辑。Sync类维护了一个状态变量state来表示锁的状态,同时也记录了当前持有锁的线程和重入次数。
- 获取锁:当一个线程请求锁时,Sync会首先检查当前状态state。如果state为0,表示锁当前是未被持有的,线程可以获取锁并将state设为1,同时记录持有锁的线程为当前线程。如果state不为0且持有锁的线程为当前线程,表示当前线程已经持有锁,可以直接增加重入次数。如果state不为0且持有锁的线程不是当前线程,则当前线程会使用AQS的同步机制进入等待队列,直到获取到锁。
- 释放锁:当一个线程释放锁时,Sync会首先减少重入次数。如果重入次数不为0,表示当前线程仍然持有锁,不会释放锁。只有当重入次数为0时,才会将state设为0,并根据AQS的同步机制唤醒等待队列中的线程。
- 非公平性:ReentrantLock默认是非公平锁,即线程在竞争锁时不保证按照先到先得的顺序获取锁。在非公平锁的情况下,当一个线程释放锁时,可能会允许当前没有竞争的线程直接获取到锁。这样可以提高系统的吞吐量,但也可能导致某些线程长时间等待。
- 公平性: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。
- Sync类:这是ReentrantReadWriteLock的主要内部类,它扩展了AQS类并实现了读写锁的基本逻辑。Sync类维护了两个状态变量,一个表示写锁的数量(即写锁的重入次数),另一个表示读锁的数量。
- 获取读锁:当一个线程请求读锁时,Sync会首先检查当前线程是否已经持有写锁,如果是,则可以直接获取读锁,增加读锁的持有数。否则,Sync会使用AQS的同步机制,如果存在其他线程持有写锁,当前线程将会进入等待队列,直到获取到读锁。
- 释放读锁:当一个线程释放读锁时,Sync会减少读锁的持有数,并根据读锁的持有数决定是否需要唤醒等待队列中的线程。
- 获取写锁:当一个线程请求写锁时,Sync会首先判断当前线程是否已经持有写锁,如果是,则增加写锁的持有数。如果当前线程没有持有写锁,则会使用AQS的同步机制,如果存在其他线程持有读锁或写锁,当前线程将会进入等待队列,直到获取到写锁。
- 释放写锁:当一个线程释放写锁时,Sync会将写锁的持有数减少,并根据持有数决定是否需要唤醒等待队列中的线程。
- 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
......//省略部分结果
分析:可以看出,读-读并不是互斥的;