目录
Java juc下的并发工具类大部分都是基于AQS设计的
AQS是AbstractQueuedSynchronizer的缩写,是Java中的一个同步工具类。AQS提供了一种基于FIFO队列的同步机制,可以用于构建锁、信号量、倒计时器等并发工具。AQS的实现方式是通过一个volatile int state来表示同步状态,当state等于0时表示未被占用,当state大于0时表示被占用。AQS提供了一些基本的操作方法,如acquire、release、tryAcquire等,用于协调多个线程对同一资源的访问。AQS的使用可以大大简化并发编程中锁的实现过程,提高代码可读性和可维护性。
思考: 如何设计一把独占锁?
1. 管程 — Java同步的设计思想
在现代操作系统中,管程已经成为一种被广泛采用的同步和通信机制。管程作为高级抽象层,可以基于底层机制(如信号量)实现更加简洁、直观的同步和通信接口,方便程序员进行并发编程。管程不是操作系统必须要实现的技术,但它是现代操作系统中非常重要的一种并发控制方法。
管程是由一个对象和一组方法组成,管程对象是负责协调各个线程访问共享资源的主体,而其中的方法是提供给线程调用的,只有获得管程对象锁的线程才可以执行管程中的方法。这样能够保证同一时刻只有一个线程可以进入管程执行,并且其他线程需要等待该线程执行完毕后才能进行执行。
管程的实现方式中,对于获取管程对象锁失败的线程,可以选择等待或者阻塞,避免线程占用过多的系统资源。同时,管程可以提供条件变量(Condition)的机制,让等待的线程可以在满足某些条件后自动被唤醒。
管程:是一种多线程同步的方式,它提供了一种自动控制多个线程互斥访问共享资源的机制。管理共享变量以及对共享变量的操作过程,让他们支持并发。
互斥:同一时刻只允许一个线程访问共享资源;
同步:线程之间如何通信、协作。
1.1.MESA模型
在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型。
AQS基于这种模型,用代码实现
管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。
Java中针对管程有两种实现
- 一种是基于Object的Monitor机制,用于synchronized内置锁的实现
- 一种是抽象队列同步器AQS,用于JUC包下Lock锁机制的实现
共享变量V:理解为独占锁的加锁标记、上锁=1,未上锁=0。同步机制中资源标记,资源来了加1,使用减1
入口等待队列:就绪队列
条件队列:阻塞队列。条件可理解为消费者和生产者的例子
如何设计一把独占锁:
- 定义一个state,0,1表示是否加锁
- 多线程来时,如何保证只有一个线程抢锁成功?用CAS。
- 等待队列,存放竞争锁失败的线程数据。一般用数组和链表。若是链表需要存放当前线程信息,被唤醒时出列
- 此处需要一个等待换醒机制,两种方案。一种是sychronized 和 object.wait()....;但有个缺陷,被调用方法时需要先获取锁,而且无法保证具体唤醒哪个线程,即无法保证链表的首个结点出列。另一种时ReentrantLock + Condition.await()/.singal()/.singalAll()..
- 此处用LockSupport.park()/unpark(),一个线程发许可一个线程获取,可以换醒需要的某一个线程
public class ParkAndUnparkDemo { public static void main(String[] args) { ParkAndUnparkThread myThread = new ParkAndUnparkThread(Thread.currentThread()); myThread.start(); System.out.println("before park"); // 获取许可 LockSupport.park(); System.out.println("after park"); } } class ParkAndUnparkThread extends Thread { private Object object; public ParkAndUnparkThread(Object object) { this.object = object; } public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("before unpark"); // 释放许可 LockSupport.unpark((Thread) object); System.out.println("after unpark"); } }
- 第4点所说的ReentrantLock 等待唤醒机制底层用的是LockSupport.park()/unpark。sychronized的wait()和notify.all()是对应在jvm层面的LockSupport.park()/unpark
- 为入口队列和等待队列定义一个模板类(技术大牛Doug Lea就用模板方法,他喜欢设计东西前先规范能力即先写接口、抽象类)
- 借助AbstractQueuedSynchronizer类,自己实现内部逻辑(代码TulingLock,SyncDemo),
2. AQS原理分析
2.1 什么是AQS
java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这些行为的抽象就是基于AbstractQueuedSynchronizer(简称AQS)实现的,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。
JDK中提供的大多数的同步器如Lock, Latch, Barrier等,都是基于AQS框架来实现的
- 一般是通过一个内部类Sync继承 AQS
- 将同步器所有调用都映射到Sync对应的方法
AQS具备的特性:
- 阻塞等待队列
- 共享/独占
- 公平/非公平
- 可重入
- 允许中断
2.2 AQS核心结构
AQS内部维护属性volatile int state
- state表示资源的可用状态
State三种访问方式:
- getState()
- setState()
- compareAndSetState()
定义了两种资源访问方式:
- Exclusive-独占,只有一个线程能执行,如ReentrantLock
- Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
AQS实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
2.3 AQS定义两种队列
- 同步等待队列: 主要用于维护获取锁失败时入队的线程。
- 条件等待队列: 调用await()的时候会释放锁,然后线程会加入到条件队列,调用signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁。
AQS 定义了5个队列中节点状态:
- 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
- CANCELLED,值为1,表示当前的线程被取消;
- SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;
- CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
- PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;
3. ReentrantLock源码分析
Demo 代码如下
package sync;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
/**
* 模拟抢票场景
*/
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();//默认非公平
private static int tickets = 8; // 总票数
public void buyTicket() {
lock.lock(); // 获取锁
try {
if (tickets > 0) { // 还有票 读
try {
Thread.sleep(10); // 休眠10ms
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "购买了第" + tickets-- + "张票"); //写
buyTicket();
} else {
System.out.println("票已经卖完了," + Thread.currentThread().getName() + "抢票失败");
}
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
ReentrantLockDemo ticketSystem = new ReentrantLockDemo();
for (int i = 1; i <= 10; i++) {
Thread thread = new Thread(() -> {
ticketSystem.buyTicket(); // 抢票
}, "线程" + i);
// 启动线程
thread.start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("剩余票数:" + tickets);
}
}
3.1 公平和非公平锁,可重入锁是如何实现的
开始断点:
同时进到源码里面,断住lock(),和unlock()方法
选择线程查看
流程如下:
了解下:ReentrantLock里有个内部类FairSync:
3.2 设计的精髓:并发场景下入队和出队操作是如何设计的
线程竞争锁失败入队阻塞逻辑实现
往下走到进acquireQueued(final Node node, int arg);方法
源码中 Node.SIGNAL = -1
执行完后发现线程被挂起(这里应该是操作系统挂起的,因为唤醒后条件不足),等待锁。若有其他线程给当前线程设置中断异常,则阻塞失败。(这一步感觉会有点奇怪,但它源码非要这样设置一下。或者也有可能我理解错了,欢迎大家指正)
之后线程状态类似
释放锁的线程唤醒阻塞线程出队竞争锁的逻辑实现
代码差不多,把status=0,可重入锁标志exclusiveOwnerThread=null。设置当前线程就是head结点,就可以执行抢锁操作,前驱执行出队操作