1.AQS
AQS全称为AbstractQueuedSynchronizer,即队列同步器,是一个重要的并发编程基础组件。
AQS中存在两种队列和一个表示状态的变量,其中这个变量为state,是一个数值类型变量,如果其值为0说明当前的锁没有被其他线程获取,为1时说明有线程持有该锁且还未释放。
两种队列中的一个是同步队列,其作用就是等待获取锁的线程会进入到此队列竞争锁,进入此队列的条件是线程获取锁失败。
除了同步队列,还有一个等待队列,等待队列可以存在多个。等待队列和同步队列不同的是等待队列中的线程是在等待某种条件然后进行释放,而同步队列是获取锁失败排队获取锁。等待队列是基于内部类ConditionObject构建的,其原理涉及到几个方法,Object.wait() --等待,Object.notify() --唤醒,Object.notifyAll() --唤醒所有,Condition.signal() --条件唤醒,Condition.await() --条件等待,Condition.awaitAll() --条件唤醒所有。注意Condition和Object不同的是Condition需要提前创建一个条件变量。
Condition condition = lock.newCondtion();//创建条件变量
condition.await();//条件等待
AQS中还有一个重要的组件就是Node类,也就是队列中的一个节点类,Node类中定义了两种模式:共享模式和独占模式,很多基于AQS实现的同步器类都是基于这两种模式实现的。除此之外还定义了线程的四种等待状态的静态变量,分别是结束状态、等待唤醒状态、条件状态、可传播的同步状态。为了实现队列的功能,还定义了前驱节点、后继节点、获取前驱节点方法等。
AQS的作用实际上就是作为一个较为通用的模板,提供了默认的获取锁、释放锁等的方法实现,具体实现由子类去重写,如ReentranLock就是基于AQS实现的。
AQS提供了三类方法:
1.状态管理方法,用于获取和设置同步状态:
getState(); //获取线程当前同步状态
getState(int newState); //设置当前同步状态
compareAndSetState(int expect, int update); //使用CAS操作原子性地更新状态。
2.队列管理方法,用于管理队列中的线程:
enq(final Node node); //将节点插入队列,
addWaiter(Node node); //将当前线程封装为节点并加入等待队列。
3.阻塞和唤醒方法,用于对线程的阻塞和唤醒:
parkAndCheckInterrupt();//阻塞线程直到被唤醒或中断。
unparkSuccessor(Node node); //唤醒在节点上等待的线程。
注意:这些是AQS提供的默认实现方法,除此之外,还有 tryAcquire, tryRelease, tryAcquireShared, tryReleaseShared等抽象方法,AQS并没有对这些抽象方法具体定义,而是让子类可以自定义同步器去继承AQS并实现这些方法,自定义地实现复杂的同步需求。除此之外也可以使用基于AQS的常用的同步器如ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等。
2.ReentrantLock
和synchronized一样,ReentrantLock是一个可重入锁,且ReentrantLock是排他锁(独占锁),在AQS的基础上,实现了更加精细的锁控制,并且可以实现公平锁和非公平锁。
ReentrantLock有几个重要的特性:可重入、可中断线程视图获取锁的操作、可实现获取锁的超时、可实现公平锁和非公平锁。
public class myLock{
private final ReentrantLock lock = new ReentrantLock();
public void lockMethod(){
try{
lock.lock();
lock.lock();//可重入
//锁住的操作
}finally{
lock.unlock();
lock.unlock();
}
}
}
注意:如果是需要使得lock锁能够管理其他实例,那创建对象就应该在类中而不是方法中,因为方法是线程私有的,而类的元数据、常量、静态变量等在方法区中,是线程共享的。
3.Semaphore(信号量)
Semaphore是共享模式的同步器,用于控制同时访问某个特定资源的操作数量,例如限制数据库的最大连接数等。
Semaphore的几个关键特性为:初始化指定许可证数量、线程可申请释放一个或者多个许可、当信号量中许可数量为0时线程进入阻塞直至许可数大于0。
public class MyLockExam{
private final Semaphore semaphore = new Semaphore(3);
public void lockMethod(){
try{
semaphore.acquire();//申请一个许可证
//模拟操作
System.out.println(Thread.currentThread().getName() + " 获取到锁,正在执行操作");
}catch(InterruptedException e){
Thread.currentThread().interrupt();
}finally{
semaphore.release();//释放许可证
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
}
}
public static void main(String[] args) {
MyLockExam exam = new MyLockExam();
// 创建并启动多个线程
for (int i = 0; i < 5; i++) {
new Thread(exam::lockMethod).start();
}
}
}
输出结果可能为:
Thread-0 获取到锁,正在执行操作
Thread-1 获取到锁,正在执行操作
Thread-2 获取到锁,正在执行操作
Thread-0 释放锁
Thread-3 获取到锁,正在执行操作
Thread-1 释放锁
Thread-4 获取到锁,正在执行操作
Thread-2 释放锁
Thread-3 释放锁
Thread-4 释放锁
3.CountDownLatch(倒计时门闩)
CountDownLatch实现了一个或者多个线程等待一系列指定操作的完成,这个功能是一次性的,计数器无法重置。
CountDownLatch的几个关键特性为:初始化指定计数值、countDown()方法递减计数器、await()方法阻塞线程直到计数器为0。
public class MyLatchExam{
private final CountDownLatch latch = new CountDownLatch(3);
public void worker(){
new Thread(()->{
try{
//模拟操作
System.out.println(Thread.currentThread().getName() + " 执行操作");
latch.countDown();//递减计数
System.out.println(Thread.currentThread().getName() + " 完成操作,计数器减1");
}catch(InterruptedException e){
e.printStackTrace();
}
}).start();
}
public void waitForDone(){
latch.await();
//后续操作
System.out.println("执行完毕");
}
public static void main(String[] args){
MyLatchExam example = new MyLatchExam();
//多个线程执行操作
example.worker();
example.worker();
example.worker();
//等待全部执行完毕
example.waitForDone();
}
}
结果为:
Thread-0 执行操作
Thread-1 执行操作
Thread-2 执行操作
Thread-0 完成操作,计数器减1
Thread-1 完成操作,计数器减1
Thread-2 完成操作,计数器减1
执行完毕
4.CyclicBarrier(循环栅栏)
和CountdownLatch功能相似,使一定数量的线程等待,直到所有线程都到达栅栏位置,再执行接下来的Runnable任务,与CountDownLatch不同的是,CyclicBarrier是可重用的。
CyclicBarrier的几个关键特性为:初始化使指定等待的线程数量、所有线程必须都到达栅栏点之后,才可以重用。
public class BarrierExample{
private final CyclicBarrier barrier = new CyclicBarrier(3,()->System.out.println("都到达栅栏了"));
public void worker(){
new Thread(()->{
try{
//模拟操作
System.out.println(Thread.currentThread().getName() + " 到达栅栏前");
//等待其他线程到达栅栏
barrier.await();
// 栅栏放开后继续执行
System.out.println(Thread.currentThread().getName() + " 通过栅栏");
}catch(InterruptedException | BrokenBarrierException e){
e.printStackTrace();
}
}).start();
}
public static void main(String[] args){
BarrierExample example = new BarrierExample();
//多个线程执行操作
example.worker();
example.worker();
example.worker();
}
}
输出结果:
Thread-0 到达栅栏前
Thread-1 到达栅栏前
Thread-2 到达栅栏前
都到达栅栏了!
Thread-2 通过栅栏
Thread-1 通过栅栏
Thread-0 通过栅栏
注意:CountDownLatch和CyclicBarrier的区别在于前者是不可重用的,而后者可重用,因此CountDownLatch适用于一个线程等待多个线程的场景,比如主线程等待其他线程操作完成后再继续进行下一步操作,具体实现是在主线程中使用await()方法判断计数器的计数是否为0,为0再进行下一步操作;而CyclicBarrier则适用于多个线程互相等待的场景,比如多个线程都到达某一阶段后再进入下一阶段。