AQS
全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
特点:
- 用 state 属性来表示可用资源数(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
- getState - 获取 state 状态
- setState - 设置 state 状态
- compareAndSetState - cas 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet(AQS 是 Java 实现,sychronized 是 c++ 实现)
- AQS 要实现的功能目标
- 阻塞版本获取锁 acquire (cas 失败会 park 并添加到等待队列) 和 非阻塞的版本尝试获取锁 tryAcquire(cas 失败直接返回 false)
- 获取锁超时机制
- 通过打断取消机制
- 独占机制及共享机制
- 条件不满足时的等待机制
state 设计
- state 使用 volatile 配合 cas 保证其修改时的原子性(cas 使用的前提是一定要用 volatile 保证可见性)
- state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想
阻塞恢复设计
- 早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume 那么 suspend 将感知不到
- 解决方法是使用 park & unpark 来实现线程的暂停和恢复,具体原理在之前讲过了,先 unpark 再 park 也没问题 。park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
- park 线程还可以通过 interrupt 打断
队列设计
- 使用了 FIFO 先入先出队列,并不支持优先级队列
- 设计时借鉴了 CLH 队列,它是一种单向无锁队列
- 队列中有 head 和 tail 两个指针节点,都用 volatile 修饰配合 cas 使用,每个节点有:
- state 维护节点状态(注意和 AQS 的状态区分。这个时等待链表中每个节点的状态)
- 等待的线程(用于唤醒线程 / 可重入时判断是否本线程,每次重入计数+1,重入计数减为0才真正释放)
等待队列节点状态
/** 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;
可重入锁
有一个重入计数
- 加锁时:如果加锁失败,判断锁的 owner 是否是当前线程,如果是,重入计数加一。
- 解锁时:释放一次,计数减一。只有重入计数减为0,才代表解锁成功
可打断(是说 park 等待时是否可打断)
-
不可打断模式:park 等待时时默认情况下是不可打断的。在此模式下,即使它被打断,仍会驻留在 AQS 队列中,仅会设置一个一个打断标志,一直要等到获得锁后根据打断标志得知自己被打断了,才真正打断(本线程重新把自己打断)
-
可打断模式:park 时被打断了,直接抛出打断异常
公平锁
- 非公平锁实现:如果线程一进来时 AQS 状态为0(无锁),就 直接尝试用 CAS 获取锁,这里体现了非公平性,不去检查之前已经在排队的 AQS 队列
- 公平锁实现:如果线程一进来时 AQS 状态为 0(无锁),不会先直接尝试用 CAS 获取锁,而时先检查之前已经在排队的 AQS 队列,如果没有人排队,或者自己就在最前面,才会尝试用 CAS 竞争锁
用 AQS 自定义不可重入锁
tryAcquire 在 AQS 原代码里是可重入锁,这里继承了 AQS,将其重写为不可重入锁了
不可重入锁:本线程加了锁,那么后续本线程还想继续加锁,加锁是不成功的
final class MySync extends AbstractQueuedSynchronizer { @Override protected boolean tryAcquire(int acquires) { // int acquires 是可重入锁用来计数的。现在不可重入锁暂时用不到 if (acquires == 1){ // 通过设置 aqs 的 state 获得锁,初始值0,改为1代表获得了锁 // 为了防止其它线程一起来修改 state 状态,所以用 compareAndSet if (compareAndSetState(0, 1)) { // 加锁成功后,设置 owner 线程为当前线程 setExclusiveOwnerThread(Thread.currentThread()); return true; } } // CAS 失败,即加锁失败 return false; }
@Override protected boolean tryRelease(int acquires) { if(acquires == 1) { // state=0表示当前没加锁,却要释放,抛出异常 if(getState() == 0) { throw new IllegalMonitorStateException(); } // 置空当前线程。ExclusiveOwnerThread 在 AQS 里不是 volatile 的 setExclusiveOwnerThread(null); // state 在 AQS 里是 volatile 的,所以要放在后面,加写屏障,防止之前的指令重排 setState(0); return true; } return false; }
protected Condition newCondition() { // aqs 提供的类 return new ConditionObject(); }
@Override protected boolean isHeldExclusively() { // 是否持有独占锁 return getState() == 1; } }
原代码里的 acquire ,用 tryAcquire 尝试获取锁不成功,就放入等待队列中(添加到一个链表中,并 LockSupport.park(this))
我们重写了 tryAcquire 方法为不可重入锁,所以这里的尝试获取锁会调用我们重写的方法,即获取不可重入锁
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
原代码里的 release,用 tryRelease 尝试释放锁成功后,还会唤醒等待队列上的线程(从链表中移除,并用 LockSupport.unpark(node.thread) 唤醒)
我们重写了 tryRelease 方法为不可重入锁释放,所以这里的尝试释放锁会调用我们重写的方法,即释放不可重入锁
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
对 MySync 的封装
class MyLock implements Lock { static MySync sync = new MySync(); @Override // 尝试获取锁(被重写的tryAcquire),不成功,进入等待队列(AQS的逻辑) public void lock() { sync.acquire(1); } @Override // 尝试,不成功,进入等待队列,可打断 public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } @Override // 尝试一次,不成功返回,不进入队列(acquire 是 tryAcquire 进行的封装,失败会进入等待队列) public boolean tryLock() { return sync.tryAcquire(1); } @Override // 尝试,不成功,进入等待队列,有时限 public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(time)); } @Override // 释放锁。tryRelease 只会释放锁,release 还会唤醒等待的线程 public void unlock() { sync.release(1); } @Override // 生成条件变量 public Condition newCondition() { return sync.newCondition(); } }
测试
MyLock lock = new MyLock(); new Thread(() -> { lock.lock(); try { log.debug("locking..."); sleep(1); } finally { log.debug("unlocking..."); lock.unlock(); }
},"t1").start();
new Thread(() -> { lock.lock(); try { log.debug("locking..."); } finally { log.debug("unlocking..."); lock.unlock(); } },"t2").start();
ReentrantLock
见 《ReentrantLock》
ConcurrentHashMap
Semaphore
1.基本使用
信号量,用来限制能同时访问共享资源的线程上限。
暂时没获得许可的线程,会通过 AQS 等待
public static void main(String[] args) { // 1. 创建 semaphore 对象 Semaphore semaphore = new Semaphore(3); // 2. 10个线程同时运行 for (int i = 0; i < 10; i++) { new Thread(() -> { // 3. 获取许可 try { semaphore.acquire(); } catch (InterruptedException e) { e.printStackTrace(); }
try { log.debug("running..."); sleep(1); log.debug("end..."); } finally { // 4. 释放许可 semaphore.release(); } }).start(); } }
2.应用:单机限流/连接池
semaphore 实现
- 使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机 线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现)
- 用 Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好, 注意下面的实现中线程数和数据库连接数是相等的
之前:没有空闲连接 lock.wait() ,有空闲连接了 notify()
@Slf4j(topic = "c.Pool") class Pool { // 1. 连接池大小 private final int poolSize; // 2. 连接对象数组 private Connection[] connections; // 3. 连接状态数组 0 表示空闲, 1 表示繁忙 private AtomicIntegerArray states; private Semaphore semaphore;
// 4. 构造方法初始化 public Pool(int poolSize) { this.poolSize = poolSize; // 让许可数与资源数一致 this.semaphore = new Semaphore(poolSize); this.connections = new Connection[poolSize]; this.states = new AtomicIntegerArray(new int[poolSize]); for (int i = 0; i < poolSize; i++) { connections[i] = new MockConnection("连接" + (i+1)); } } // 5. 借连接 public Connection borrow() {// t1, t2, t3 // 获取许可 try { semaphore.acquire(); // 没有许可的线程,会在此等待 } catch (InterruptedException e) { e.printStackTrace(); }
for (int i = 0; i < poolSize; i++) { // 获取空闲连接 if(states.get(i) == 0) { if (states.compareAndSet(i, 0, 1)) { log.debug("borrow {}", connections[i]); return connections[i]; } } } // 永远不会执行到这里,semaphore 保证了走到下面总能获得一个可用连接。这里只是为了语法不出错 return null; }
// 6. 归还连接 public void free(Connection conn) { for (int i = 0; i < poolSize; i++) { if (connections[i] == conn) { states.set(i, 0); log.debug("free {}", conn); semaphore.release(); break; } } } }
3.原理-acquire
Semaphore 有点像一个停车场,permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后 停车场显示空余车位减一
刚开始,permits(state)为 3,这时 5 个线程来获取资源
假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列 park 阻塞
AQS 的 state 表示可用资源数
源码
static final class NonfairSync extends Sync { private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) { // permits 即 state super(permits); } // Semaphore 方法, 方便阅读, 放在此处 public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); }
// AQS 继承过来的方法, 方便阅读, 放在此处 public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException();
// >=0 表明获取锁成功 if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); } // 尝试获得共享锁 protected int tryAcquireShared(int acquires) { return nonfairTryAcquireShared(acquires); } // Sync 继承过来的方法, 方便阅读, 放在此处 final int nonfairTryAcquireShared(int acquires) { for (;;) {
// AQS 的 state 表示可用资源数 int available = getState(); int remaining = available - acquires; if ( // 一次获取的许可 acquires 超过了剩余的, 获取失败 remaining < 0 || // 如果 cas 重试成功, 返回 >=0 的数, 表示获取成功 compareAndSetState(available, remaining) ) { return remaining; } } } // AQS 继承过来的方法, 方便阅读, 放在此处
// 获取失败时,进入这里 private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
// 添加到等待队列 队尾 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) {
// 获取当前节点的前驱节点 final Node p = node.predecessor();
// 如果前驱节点是头节点,说明它是老二(头节点是虚的,Thread一直为null) if (p == head) { // 老二可以,再次尝试获取许可 int r = tryAcquireShared(arg);
// r>=0 表示获取成功,r 表示剩余可用资源数 if (r >= 0) { // 成功后本线程出队(AQS), 所在 Node设置为 head // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark // 如果 head.waitStatus == 0 ==> Node.PROPAGATE // r 表示可用资源数, 为 0 则不会继续传播 setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } }
// 1.不是老二 或 2.老二再次获取资源不成功, 设置上一个节点 waitStatus = Node.SIGNAL(-1 有责任唤醒下一个), 此节点下轮进入 park 阻塞 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } } // Semaphore 方法, 方便阅读, 放在此处 public void release() { sync.releaseShared(1); } // AQS 继承过来的方法, 方便阅读, 放在此处 public final boolean releaseShared(int arg) {
// CAS 修改 AQS state if (tryReleaseShared(arg)) {
// 做释放后续工作:头节点获得资源,并唤醒后继节点等 doReleaseShared(); return true; } return false; } // Sync 继承过来的方法, 方便阅读, 放在此处 protected final boolean tryReleaseShared(int releases) { for (;;) { int current = getState();
// 释放 release 个,AQS 的 state 由 current 变为 current + release 个 int next = current + releases; if (next < current) // overflow throw new Error("Maximum permit count exceeded"); if (compareAndSetState(current, next)) return true; } } }
tryAcquire 里,如果获取成功后,还有剩余的
太难了,先放弃。。。。
propagate 是什么?
CountdownLatch(倒计时锁)
用法
用来进行线程同步协作,等待所有线程完成倒计时。
其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一
public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> { log.debug("begin..."); sleep(1); latch.countDown(); log.debug("end...{}", latch.getCount()); }).start();
new Thread(() -> { log.debug("begin..."); sleep(2); latch.countDown(); log.debug("end...{}", latch.getCount()); }).start();
new Thread(() -> { log.debug("begin..."); sleep(1.5); latch.countDown(); log.debug("end...{}", latch.getCount()); }).start();
log.debug("waiting..."); latch.await(); log.debug("wait end..."); }
输出结果:主线程会在 latch.await() 阻塞住,等待三个线程都运行完了,计数减为0后,才恢复运行
18:44:00.778 c.TestCountDownLatch [main] - waiting... 18:44:00.778 c.TestCountDownLatch [Thread-2] - begin... 18:44:00.778 c.TestCountDownLatch [Thread-0] - begin... 18:44:00.778 c.TestCountDownLatch [Thread-1] - begin... 18:44:01.782 c.TestCountDownLatch [Thread-0] - end...2 18:44:02.283 c.TestCountDownLatch [Thread-2] - end...1 18:44:02.782 c.TestCountDownLatch [Thread-1] - end...0 18:44:02.782 c.TestCountDownLatch [main] - wait end...
源码
public class CountDownLatch { /** * Synchronization control For CountDownLatch. * Uses AQS state to represent count. */ private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { setState(count); } int getCount() { return getState(); }
// state 为 0 才算是获取成功,返回1。state大于表明等待的线程还没减完,会继续阻塞 protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; } protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } } }
}
countDownLatch.await()
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); } public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException();
// state 不为0,就会返回 -1,进入下面的阻塞等待。直到 state 减为 0 if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }
countDownLatch.countdown()
将 state 减少 1
public void countDown() { sync.releaseShared(1); }
比 join 好在哪里?
join 是等待线程运行结束。而线程池里的线程一般都会重用不会主动结束。
而 CountDownLatch 可以在代码里主动调用,当线程的主要逻辑运行完了就可以主动调用,不必等待线程运行结束。
应用-等待多线程准备完毕
AtomicInteger num = new AtomicInteger(0); ExecutorService service = Executors.newFixedThreadPool(10, (r) -> { return new Thread(r, "t" + num.getAndIncrement()); }); CountDownLatch latch = new CountDownLatch(10); String[] all = new String[10]; Random r = new Random();
// 一共十个线程 for (int j = 0; j < 10; j++) { int x = j; service.submit(() -> {
// 每个线程有一百个任务 for (int i = 0; i <= 100; i++) { try { Thread.sleep(r.nextInt(100)); } catch (InterruptedException e) { }
// all[x] 表示第 j 个线程的执行进度:一百个任务完成到多少了 all[x] = Thread.currentThread().getName() + "(" + (i + "%") + ")"; System.out.print("\r" + Arrays.toString(all)); } latch.countDown(); }); } latch.await(); System.out.println("\n游戏开始..."); service.shutdown();
中间输出
[t0(52%), t1(47%), t2(51%), t3(40%), t4(49%), t5(44%), t6(49%), t7(52%), t8(46%), t9(46%)]
最后输出
[t0(100%), t1(100%), t2(100%), t3(100%), t4(100%), t5(100%), t6(100%), t7(100%), t8(100%), t9(100%)] 游戏开始...
应用-等待远程调用结束-用 FutureTask 替代
CountDownLatch 只是简单的等待结束,FutureTask 可以等待接收最后的结果
RestTemplate restTemplate = new RestTemplate(); log.debug("begin"); ExecutorService service = Executors.newCachedThreadPool(); Future<Map<String,Object>> f1 = service.submit(() -> { Map<String, Object> r = restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1); return r; }); Future<Map<String, Object>> f2 = service.submit(() -> { Map<String, Object> r = restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1); return r; }); Future<Map<String, Object>> f3 = service.submit(() -> { Map<String, Object> r = restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2); return r; }); Future<Map<String, Object>> f4 = service.submit(() -> { Map<String, Object> r = restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1); return r; });
System.out.println(f1.get()); System.out.println(f2.get()); System.out.println(f3.get()); System.out.println(f4.get()); log.debug("执行完毕"); service.shutdown();
CyclicBarrier
CountDownLatch 只可以在 new 的时候指定数量,后面只能 countdown(),而不能修改计数。
CyclicBarrier 循环栅栏改进了这一点,计数减为 0 后,再次调用 await(),计数就是回到了开始时指定的那个。
ExecutorService service = Executors.newFixedThreadPool(2); CyclicBarrier cb = new CyclicBarrier(2, ()->{ log.debug("task1 task2 finish......")
}); // 个数为2时才会继续执行
// 第二个循环计数减为 0 后,cb 从 2 重新开始 for (int i=0;i<3;i++) { service.submit(()->{ System.out.println("线程1开始.."+new Date()); try { cb.await(); // 计数 2-1=1。同时当计数不为0时,等待 } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } System.out.println("线程1继续向下运行..."+new Date()); }); service.submit(()->{ System.out.println("线程2开始.."+new Date()); try { Thread.sleep(2000); } catch (InterruptedException e) { } try { cb.await(); // 计数 1-1=0。等于 0 了,这个和上面那个线程可以恢复继续运行 } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } System.out.println("线程2继续向下运行..."+new Date()); }); }