深刻理解Java并发框架AQS系列(一):线程
深刻理解Java并发框架AQS系列(二):AQS框架简介及锁概念html
AQS诞生于Jdk1.5,在当时低效且功能单一的
synchronized
的年代,某种意义上讲,她拯救了Javajava
注:本系列文章全部测试用例均基于jdk1.8,操做系统为macOS架构
咱们去学习一个知识点或开启一个新课题时,最好是带着问题去学习,这样针对性比较强,且印象比较深入,主动思考带给咱们带来了无穷的好处并发
抛开AQS,设想如下问题:框架
咱们发现,一个简单的等待资源的问题,牵扯出后续诸多庞杂且无头绪的问题;加锁不只依赖一套完善的框架体系,还要具体根据使用场景而定,才能接近最优解;那咱们即将要引出的AQS能完美解决上述这些问题吗?工具
答案是确定的:不能性能
其实Doug Lea也意识到问题的复杂性,不可能出一个超级工具来解决全部问题,因此他把AQS设计为一个abstract类,并提供一系列子类去解决不一样场景的问题,例如ReentrantLock
、Semaphore
等;当咱们发现这些子类也不能知足咱们加锁需求时,咱们能够定义本身的子类,经过重写两三个方法,寥寥几行代码,实现强大的功能,这一切都得益于AQS做者敏锐的前瞻性学习
指的一提的是,虽然咱们能够用某个子类去实现另外一个子类所提供的功能(例如使用Semaphore
替代CountDownLatch
),但其易用、简洁、高效性等可否达到理想效果,都值得商榷;就比如在陆地上穿着雪橇走路,虽能前进,却低效易摔跤测试
本小节仅带你们对AQS架构有个初步了解,在后文的独占锁、共享锁等中会详细阐述。下图为AQS框架的主体结构
优化
从上图中咱们看到了AQS中很是关键的一个概念:“阻塞队列”。即AQS的理念是当线程没法获取资源时,提供一个FIFO类型的有序队列,用来维护全部处于“等待中”的线程。看似无解可击的框架设计,同时也牵出另外的一个问题:阻塞队列必定高效吗?
当“同步块逻辑”执行很快时,咱们列出两种场景
ReentrantLock
,遇到资源争抢,放阻塞队列针对这2种场景,咱们写测试用例比较一下
package org.xijiu.share.aqs.compare; import org.junit.Test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; import java.util.concurrent.locks.ReentrantLock; /** * @author likangning * @since 2021/3/9 上午8:58 */ public class CompareTest { private class MyReentrantLock extends AbstractQueuedSynchronizer { protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); while (true) { int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } } } protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } } /** * 使用AQS框架 */ @Test public void test1() throws InterruptedException { ReentrantLock reentrantLock = new ReentrantLock(); long begin = System.currentTimeMillis(); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 2; i++) { executorService.submit(() -> { for (int j = 0; j < 50000000; j++) { reentrantLock.lock(); doBusiness(); reentrantLock.unlock(); } }); } executorService.shutdown(); executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); System.out.println("ReentrantLock cost : " + (System.currentTimeMillis() - begin)); } /** * 无限重试 */ @Test public void test2() throws InterruptedException { MyReentrantLock myReentrantLock = new MyReentrantLock(); long begin = System.currentTimeMillis(); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 2; i++) { executorService.submit(() -> { for (int j = 0; j < 50000000; j++) { myReentrantLock.tryAcquire(1); doBusiness(); myReentrantLock.tryRelease(1); } }); } executorService.shutdown(); executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); System.out.println("MyReentrantLock cost : " + (System.currentTimeMillis() - begin)); } private void doBusiness() { // 空实现,模拟程序快速运行 } }
上例,虽然MyReentrantLock
继承了AbstractQueuedSynchronizer
,但没有使用其阻塞队列。咱们每种状况跑5次,看下二者在耗时层面的表现
类 | 耗时1 | 耗时2 | 耗时3 | 耗时4 | 耗时5 | 平均耗时(ms) |
---|---|---|---|---|---|---|
ReentrantLock |
11425 | 12301 | 12289 | 10262 | 11461 | 11548 |
MyReentrantLock |
8717 | 8957 | 10283 | 8445 | 8928 | 9066 |
上例只是拿独占锁举例,共享锁也同理。能够简单归纳为:线程挂起、唤醒的时间占整个加锁周期比重较大,致使每次挂起、唤醒已经成为一种负担。固然此处并非说AQS设计有什么缺陷,只是想表达并无一种万能的框架能应对全部状况,一切都要靠使用者灵活理解、应用
咱们经常使用的锁并发类,基本上都是AQS的子类或经过组合方式实现,可见AQS在Java并发体系的重要性
至于如何使用,是须要区分子类是想实现独占锁仍是共享锁
独占锁
tryAcquire()
tryRelease()
isHeldExclusively()
-- 可不实现共享锁
tryAcquireShared()
tryReleaseShared()
AQS自己是一个abstract
类,将主要并发逻辑进行了封装,咱们定义本身的并发控制类,仅须要实现其中的两三个方法便可。而在对外(public
方法)表现形式上,可依据本身的业务特性来定义;例如Semaphore
定义为acquire
、release
,而ReentrantLock
定义为lock
、unlock
相信你们常常会被各类各样锁的定义搞乱,叫法儿也五花八门,为了后续行文的方便,此章咱们把一些锁概念阐述一下
独占锁,顾名思义,即在同一时刻,仅容许一个线程执行同步块代码。比如一伙儿人想要过河,但只有一根独木桥,且只能承受一人的重量
JDK支持的典型独占锁:ReentrantLock
、ReentrantReadWriteLock
共享锁实际上是相对独占锁而言的,涉及到共享锁就要聊到并发度,即同一时刻最多容许同时执行线程的数量。上图所述的并发度为3,即在同一时刻,最多可有3我的在同时过河。
但共享锁的并发度也能够设置为1,此时它能够看做是独占锁
JDK支持的典型独占锁:Semaphore
、CountDownLatch
虽然叫作公平锁,但咱们知道任何事情都是相对的,此处也不例外,咱们也只能作到相对公平,后文会涉及,此处再也不赘述
线程在进入时,首先要检查阻塞队列中是否为空,若是发现已有线程在排队,那么主动添加至队尾并等待被逐一唤起;若是发现阻塞队列为空,才会尝试去获取资源。公平锁相对非公平锁效率较低,一般来说,加锁时间越短,表现越明显
任何一个刚进入的线程,都会尝试去获取资源,释放资源后,还会通知头节点去尝试获取资源,这样可能致使饥饿发生,即某一个阻塞队列中的线程一直得不到调度。
那为何咱们会说,非公平锁的效率要高于公平锁呢?假设一个独占锁,阻塞队列中已经有10个线程在排队,线程A抢到资源并执行完毕后,去唤醒头结点head,head线程唤醒须要时间,head唤醒后才尝试去获取资源,而在整个过程当中,没有线程在执行加锁代码
由于线程唤起须要引起用户态及内核态的切换,故是一个相对比较耗时的操做。
咱们再举一个不恰当的例子:行政部在操场上为同窗们办理业务,由于天气炎热,故让排队的同窗在场边一个凉亭等待,凉亭距离业务点约300米,且没法直接看到业务点,须要等待上一个办理完毕的同窗来通知。假定平均办理一个业务耗时约30秒AQS框架是支持公平、非公平两种模式的,使用者能够根据自身的状况作选择,而Java中的内置锁synchronized
是非公平锁
即某个线程获取到锁后、在释放锁以前,再次尝试获取锁,能成功获取到,不会出现死锁,即是可重入锁;须要注意的是,加锁次数须要跟释放次数同样
synchronized
、ReentrantLock
均为可重入锁
之因此将这三个锁放在一块儿论述,是由于它们都是synchronized
引入的概念,为了描述流畅,咱们把它们放在一块儿
synchronized
锁只有一个线程,并且老是被这一个线程反复加锁、解锁;故引入偏向锁,且向对象头的MarkWord
部分中, 标记上线程id,值得一提的是,在线程加锁结束后,并无解锁的动做,这样带来的好处首先是少了一次CAS操做,其次当这个线程再次尝试加锁时,仅仅比较MarkWord
部分中的线程id与当前线程的id是否一致,若是一致则加锁成功。偏向锁所以而得名,它偏向于占有它的线程,对其很是友好。当上一个线程释放锁后,若是有另外一个线程尝试加锁,偏向锁会从新偏向新的线程。而当一个线程正占有锁,又有一个新的线程试图加锁时,便进入了轻量级锁这3层锁是逐级膨胀的,且过程不可回逆,即某个锁一旦进入重量级锁,便不可回退至轻量级锁或偏向锁。虽然synchronized
不是本文的重点,但既然提起来了,咱们能够把其特性简单罗列一下
synchronized
独占锁、非公平锁、可重入;内部作了不少优化那synchronized
锁的性能究竟如何呢?咱们跟AQS框架中的ReentrantLock
作个简单对比
public class SynchronizedAndReentrant { private static int THREAD_NUM = 5; private static int EXECUTE_COUNT = 30000000; /** * 模拟ReentrantLock处理业务 */ @Test public void test() throws InterruptedException { ReentrantLock reentrantLock = new ReentrantLock(); long begin = System.currentTimeMillis(); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < THREAD_NUM; i++) { executorService.submit(() -> { for (int j = 0; j < EXECUTE_COUNT; j++) { reentrantLock.lock(); doBusiness(); reentrantLock.unlock(); } }); } executorService.shutdown(); executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); System.out.println("ReentrantLock cost : " + (System.currentTimeMillis() - begin)); } private void doBusiness() { } /** * 模拟synchronized处理业务 */ @Test public void test2() throws InterruptedException { long begin = System.currentTimeMillis(); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < THREAD_NUM; i++) { executorService.submit(() -> { for (int j = 0; j < EXECUTE_COUNT; j++) { synchronized (SynchronizedAndReentrant.class) { doBusiness(); } } }); } executorService.shutdown(); executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); System.out.println("synchronized cost : " + (System.currentTimeMillis() - begin)); } }
锁 | 耗时1 | 耗时2 | 耗时3 | 耗时4 | 耗时5 | 平均耗时(ms) |
---|---|---|---|---|---|---|
ReentrantLock |
5876 | 5879 | 5601 | 5939 | 5925 | 5844 |
synchronized |
5551 | 5611 | 5794 | 5397 | 5445 | 5559 |
在JDK1.8的ConcurrentHashMap
中,做者已经将分段锁摒弃,进而采用synchronized
为分桶加锁。synchronized
已日趋成熟,咱们应该摒弃对它低性能的偏见,放心大胆地去使用它