【java并发编程】AQS框架

一、简介

 aqs全称抽象队列同步器是AbstractQueuedSynchronize抽象类。
它是一个用来构建锁和同步器的框架,它底层用了CAS技术来保证操作的原子性,同时利用FIFO队列实现线程间的锁竞争,将基础的同步相关抽象细节放在AQS,它能够成为实现大部分同步需求的基础,也是JUC并发包同步的核心基础组件。LockReadWriteLockCountDowndLatchCyclicBarrierSemaphoreThreadPoolExecutor等都是在AQS的基础上实现的。

二、AQS核心思想

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS使用一个volatile的state变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

1、具体加锁过程

1、执行tryAcquire方法尝试加锁

2、如果判断当前线程如果是自己,则把state+1,加锁成功。如果不是继续判断当前锁是否空闲(state是否为0),是的话通过cas加锁,返回加锁结果(由子类实现)

3、如果加锁失败,则把当前线程封装进Node对象中,把Node.prev指向tail组成链表结构,如果线程需要阻塞则执行LockSupport.park(this);

 

2、具体释放锁过程

1、执行tryRelease方法释放

2、State进行-1操作,如果是0,则把当前线程ExclusiveOwnerThread置为null。否则设置State-1值

3、如果有等待的线程,则使用LockSupport.unpark唤醒包装了Node的下一个节点的线程

3、aqs图示

主要包含4个字段

  • state(同步状态)
  • exclusiveOwnerThread(获得锁的 线程)
  • head(头节点)
  • tail尾节点

aqs分为两种“独占锁”和”共享锁“

  • 独占锁 -- 锁在一个时间点只能被一个线程锁占有。 例如:ReentrantLock可重入锁和ReentrantReadWriteLock.WriteLock写锁。

        独占锁又分为“公平锁”和“非公平锁”。

  1. 公平锁,是按照先来先得的规则,公平的获取锁;
  2. 非公平锁,则当线程要获取锁时,直接抢占获取锁
  • 共享锁 -- 能被多个线程同时拥有,能被共享的锁。例如:ReentrantReadWriteLock.ReadLock读锁,CyclicBarrier循环屏障, CountDownLatch计数器和Semaphore信号量

三、aqs实现子类

1、CountDownLatch计数器(常用)

CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,开门后所有线程一起跑”的感觉。
 

使用方法

首先建立CountDownLatch对象,并且传入参数即为count初始值。如果一个线程调用了await()方法,那么这个线程便进入阻塞状态,并进入阻塞队列。如果一个线程调用了countDown()方法,则会使count-1;当count的值为0时,这时候阻塞队列中调用await()方法的线程便会逐个被唤醒,从而进入后续的操作。

常用方法有:

  1. await()方法表示阻塞线程
  2. countDown()方法表示,计数器减一。在线程阻塞后,计数器减至0后才可以唤醒所有阻塞的线程。

使用场景

  1. 让多个线程等待:模拟并发,让并发线程一起执行
  2. 让单个线程等待:多个线程(任务)完成后,进行汇总合并

2、Semaphore信号量

Semaphore(信号量)可以通过其限制执行的线程数量(同时访问共享资源的线程数),达到限流的效果。

使用方法

当一个线程执行时先通过其方法进行获取许可操作,获取到许可的线程继续执行业务逻辑,当线程执行完成后进行释放许可操作,未获取达到许可的线程进行等待或者直接结束。

常用方法有:

  1. void acquire() throws InterruptedException 获取一个许可,会阻塞等待其他线程释放许可
  2. void acquire(int permits) throws InterruptedException 获取指定的许可数 ,会阻塞等待其他线程释放
  3. void acquireUninterruptibly() 获取一个许可 会阻塞等待其他线程释放许可 可被中断
  4. void acquireUninterruptibly(int permits) 获取指定的许可数 会阻塞等待其他线程释放许可 可被中断
  5. boolean tryAcquire() 尝试获取许可 不会进行阻塞等待
  6. boolean tryAcquire(int permits) 尝试获取指定的许可数 不会阻塞等待
  7. boolean tryAcquire(long timeout, TimeUnit unit) 尝试获取许可 可指定等待时间
  8. boolean tryAcquire(int permits, long timeout, TimeUnit unit) 尝试获取指定的许可数 可指定等待时间

使用场景

  1. 控制线程最大并发数(限流,例如数据库连接池)
  2. 异步任务,同步返回
  3. 加锁 (1个整形信号量即可模拟互斥锁)

3、CyclicBarrier循环屏障

CyclicBarrier字面意思是“可重复使用的栅栏”,CyclicBarrier 相比 CountDownLatch 来说,要简单很多,它是 ReentrantLock 和 Condition 的组合使用,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic)。

利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。

使用方法

  

常用方法有:

  • public CyclicBarrier(int parties);//parties表示屏障拦截的线程数量
  • public CyclicBarrier(int parties, Runnable barrierAction);//parties表示屏障拦截的线程数量,在线程到达屏障时,优先执行barrierAction
  • await()
  • isBroken()用来知道阻塞的线程是否被中断
  • reset()
  • getNumberWaiting()获得CyclicBarrier阻塞的线程数量

使用场景

  1. 用于多线程计算数据,最后合并计算结果的应用场景

与CountDownLatch区别
CountDownLatch是一个同步的辅助类,允许一个或多个线程,等待其他一组线程完成操作,再继续执行。
CyclicBarrier是一个同步的辅助类,允许一组线程相互之间等待,达到一个共同点,再继续执行。
区别:

  1. CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次
  2. CyclicBarrier还提供getNumberWaiting()、isBroken()等方法。
  3. CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。

4、Phaser阶段

Phaser允许并发多阶段任务。Phaser类机制是在每一步结束的位置对线程进行同步,当所有的线程都完成了这一步,才允许执行下一步。

使用场景

用来解决控制多个线程分阶段共同完成任务的情景问题。

5、Exchanger交换器

Exchanger用于线程间进行通信、数据交换。Exchanger提供了一个同步点exchange方法,两个线程调用exchange方法时,无论调用时间先后,两个线程会互相等到线程到达exchange方法调用点,此时两个线程可以交换数据,将本线程产出数据传递给对方。

6、ReentrantLock可重入锁、ReentrantReadWriteLock可重入读写锁

参考:【java并发编程】lock接口_现实、太残忍的博客-CSDN博客

7、ThreadPoolExecutor线程池

参考:java使用jdk的ThreadPoolExecutor线程池_现实、太残忍的博客-CSDN博客

猜你喜欢

转载自blog.csdn.net/sumengnan/article/details/125058951