目录
1. 简介
从JDK1.5起,Java API 中提供了java.util.concurrent(简称JUC)包,在此包中定义了并发 编程中很常用的工具。
JUC是 JSR 166 标准规范的一个实现,JSR 166 以及 JUC 包的作者是同一个人 Doug Lea 。
2. Atomic包
2.1 什么是原子类
JDK1.5之后,JUC的atomic包中,提供了一系列用法简单、性能高效、线程安全的更新一个变量的类,这些称之为原子类。
作用:保证共享变量操作的原子性、可见性,可以解决volatile原子性操作变量的BUG
2.2 Atomic包里的类
➢基本类型:AtomicInteger整形原子类…
➢引用类型:AtomicReference引用类型原子类…
➢数组类型:AtomicIntegerArray整形数组原子类…
➢对象属性修改类型:AtomicIntegerFieldUpdater原子更新整形字段的更新器…➢JDK1.8新增:DoubleAdder双浮点型原子类、LongAdder长整型原子类…
虽然原子类很多,但原理几乎都差不多,其核心是采用CAS进行原子操作
3. CAS
3.1 CAS是什么
CAS即compare and swap(比较再替换),同步组件中大量使用CAS技术实现了Java多线程的并发操作。整个AQS、Atomic原子类底层操作,都可以看见CAS。甚至ConcurrentHashMap在1.8的版本中也调整为了CAS+Synchronized。可以说CAS是整个JUC的基石。
CAS本质是一条CPU的原子指令,可以保证共享变量修改的原子性
其实,CAS本不难,它只是一个方法而已,这个方法长这样:执行函数:CAS(V,E,N)
➢V:要读写的内存地址
➢E:进行比较的值(预期值)
➢N:拟写入的新值
➢当且仅当内存地址的V 中的值等于预期值E 时,将内存地址的V中的值改为N,否则会进行自旋操作,即不断的重试。
3.2 Java中对CAS的实现
3.3 CAS的缺陷
CAS虽然很好的解决了共享变量的原子操作问题,但还是有一些缺陷:
➢循环时间不可控:如果CAS一直不成功,那么CAS自旋就是个死循环。会给CPU造成负担 ReentrantReadWriteLock读写锁:它维护了一对锁,ReadLock读锁和WriteLock写锁。读写锁适合读多写少的场景基本原则:读锁可以被多个线程同时持有进行访问,而写锁只能被一个线程持有。可以这 么理解:读写锁是个混合体,它既是一个共享锁,也是一个独享锁。 ➢StampedLock重入读写锁,JDK1.8引入的锁类型,是对读写锁ReentrantReadWriteLock的增强版。 ➢只能保证一个共享变量原子操作
➢ABA问题:CAS检查操作的值有没有发生改变,如果没有则更新。这就存在一种情况:如果原来的值是A,然后变成了B,然后又变为A了,那么CAS检测不到数据发生了变化,但是其实数据已经改变了。
4. JUC里面的常见锁
JUC包提供了种类丰富的锁,每种锁特性各不相同
➢ReentrantLock重入锁:它具有与使用synchronized 相同的一些基本行为和语义,但是它的API功能更强大,重入锁相当于synchronized 的增强版,具有synchronized很多所没有的功能。它是一种独享锁(互斥锁),可以是公平锁,也可以是非公平的锁。
➢ReentrantReadWriteLock读写锁:它维护了一对锁,ReadLock读锁和WriteLock写锁。读写锁适合读多写少的场景。基本原则:读锁可以被多个线程同时持有进行访问,而写锁只能被一个线程持有。可以这么理解:读写锁是个混合体,它既是一个共享锁,也是一个独享锁。
➢StampedLock重入读写锁,JDK1.8引入的锁类型,是对读写锁ReentrantReadWriteLock的增强版。
4.1 锁分类
4.1.1 按上锁方式划分
①隐式锁:synchronized,不需要显示加锁和解锁
②显式锁:JUC包中提供的锁,需要显示加锁和解锁
4.1.2 按特性划分
悲观锁/乐观锁:按照线程在使用共享资源时,要不要锁住同步资源,划分为悲观锁和乐观锁
- 悲观锁:JUC锁,synchronized
- 乐观锁:CAS,关系型数据库的版本号机制
重入锁/不可重入锁:按照同一个线程是否可以重复获取同一把锁,划分为重入锁和不可重入锁
- 重入锁:ReentrantLock、synchronized
- 不可重入锁:不可重入锁,与可重入锁相反,线程获取锁之后不可重复获取锁,重复获取会发生死锁
公平锁/非公平锁:按照多个线程竞争同一锁时需不需要排队,能不能插队,划分为公平锁和非公平锁。
- 公平锁:new ReentrantLock(true)多个线程按照申请锁的顺序获取锁
- 非公平锁:new ReentrantLock(false)多个线程获取锁的顺序不是按照申请锁的顺序(可以插队) synchronized
独享锁/共享锁:按照多个线程能不能同时共享同一个锁,锁被划分为独享锁和共享锁
- 独享锁:独享锁也叫排他锁,synchronized,ReentrantLock,ReentrantReadWriteLock的WriteLock写锁
- 共享锁:ReentrantReadWriteLock的ReadLock读锁
4.1.3 其他锁
自旋锁:
- 实现:CAS、轻量级锁
分段锁:
- 实现:ConcurrentHashMap ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
无锁/偏向锁/轻量级锁/重量级锁
- 这四个锁是synchronized独有的四种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。
- 它们是JVM为了提高synchronized锁的获取与释放效率而做的优化
- 四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级
4.2 Synchronized和JUC的锁对比
Synchronize的缺陷:
➢ 第一: Synchronized无法控制阻塞时长,阻塞不可中断
◼ 使用Synchronized,假如占有锁的线程被长时间阻塞(IO、sleep、join),由于线程阻塞时没法释放锁,会导致大 量线程堆积,轻则影响性能,重则服务雪崩
◼ JUC的锁可以解决这两个缺陷
➢ 第二:读多写少的场景中,多个读线程同时操作共享资源时不需要加锁
◼ Synchronized不论是读还是写,均需要同步操作,这种做法并不是最优解
◼ JUC的ReentrantReadWriteLock锁可以解决这个问题
5. 锁原理分析
在重入锁ReentrantLock类关系图中,可以看到NonfairSync和FairSync都继承自抽象类Sync,而Sync类继 承自抽象类AbstractQueuedSynchronizer(简称AQS)。
5.1 AQS
AQS即队列同步器,是JUC并发包中的核心基础组件,其本身只是一个抽象类。其实现原理与前面介绍的 Monitor管程是一样的,AQS中也用到了CAS和Volatile。
由类图可以看到,AQS是一个FIFO的双向队列,队列中存储的是thread,其内部通过节点head和tail记录队首 和队尾元素,队列元素的类型为Node
AQS中的内部静态类Node为链表节点,AQS会在线程获取锁失败后,线程会被阻塞并被封装成Node加入到 AQS队列中;当获取锁的线程释放锁后,会从AQS队列中的唤醒一个线程(节点)。
- 线程抢夺锁失败时,AQS队列的变化【加锁】
① AQS的head、tail分别代表同步队列头节点和尾节点指针默认为null
② 当第一个线程抢夺锁失败,同步队列会先初始化,随后线程会被封装成Node节点追加到AQS队列中。
➢ 假设:当前独占锁的的线程为ThreadA,抢占锁失败的线程为ThreadB。
➢ 2.1 同步队列初始化,首先在队列中添加Node,thread=null
➢ 2.2 将ThreadB封装成为Node,追加到AQS队列
③ 当下一个线程抢夺锁失败时,继续重复上面步骤。假设:ThreadC抢占线程失败
- 线程被唤醒时,AQS队列的变化【解锁】
① ReentrantLock唤醒阻塞线程时,会按照FIFO的原则从AQS中head头部开始唤醒首个节点中线程。
② head节点表示当前获取锁成功的线程ThreadA节点。
③ 当ThreadA释放锁时,它会唤醒后继节点线程ThreadB,ThreadB开始尝试获得锁,如果ThreadB获得锁成功,会将自 己设置为AQS的头节点。ThreadB获取锁成功后,AQS变化如下:
5.2 ReentrantLock源码分析-锁的获取
ReentrantLock锁获取源码分析:
5.3 ReentrantLock源码分析-锁的释放
ReentrantLock锁释放源码分析
5.4 公平锁和非公平锁源码实现区别
公平锁/非公平锁:按照多个线程竞争同一锁时需不需要排队,能不能插队
获取锁的两处差异:
① lock方法差异
② tryAcquire差异
5.5 读写锁ReentrantReadWriteLock
读写锁:维护着一对锁(读锁和写锁),通过分离读锁和写锁,使得并发能力比一般的互斥锁有较大 提升。同一时间,可以允许多个读线程同时访问,但在写线程访问时,所有读写线程都会阻塞。 所以说,读锁是共享的,写锁是排他的。
主要特性:
➢ 支持公平和非公平锁
➢ 支持重入
➢ 锁降级:写锁可以降级为读锁,但是读锁不能升级为写锁
5.6 锁优化
如何优化锁?
➢ 减少锁的持有时间
➢ 减少锁粒度
◆ 将大对象拆分为小对象,增加并行度,降低锁的竞争
◆ 例如:早期ConcurrentHashMap的分段锁
➢ 锁分离
◆ 根据功能场景进行锁分离
◆ 例如:读多写少的场景,使用读写锁可以提高性能
➢ 锁消除:锁消除是编译器自动的一种优化方式
➢ 锁粗化
◆ 增加锁的范围,降低加解锁的频次
6. 线程协作工具类
6.1 CountDownLatch计数门闩
◆ 倒数结束之前,一直处于等待状态,直到数到0,等待线程才继续工作。
◆ 场景:购物拼团、分布式锁
◆ 方法:
① new CountDownLatch(int count)
② await():调用此方法的线程会阻塞,支持多个线程调用,当计数为0,则唤醒线程
③ countdown():其他线程调用此方法,计数减1
6.2 Semaphore信号量
◆ 限制和管理数量有限的资源的使用
◆ 场景:Hystrix、Sentinel限流
◆ 方法:
① new Semaphore ((int permits) 可以创建公平的非公平的策略
② acquire():获取许可证,获取许可证,要么获取成功,信号量减1,要么阻塞等待唤醒
③ release():释放许可证,信号量加1,然后唤醒等待的线程
6.3 CyclicBarrier循环栅栏
◆ 线程会等待,直到线程到了事先规定的数目,然后触发执行条件进行下一步动作
◆ 场景:并行计算
◆ 方法:
① new CyclicBarrier(int parties, Runnable barrierAction)参数1集结线程数,参数2凑齐之后执行的任务
② await():阻塞当前线程,待凑齐线程数量之后继续执行
6.4 Condition接口
◆ 控制线程的“等待”和“唤醒”
◆ 方法:
① await():阻塞线程
② signal():唤醒被阻塞的线程
③ signalAll()会唤起所有正在等待的线程。
◆ 注意:
① 调用await()方法时必须持有锁,否则会抛出异常
② Condition和Object#await/notify方法用法一样,两者await方法都会释放锁
7. 并发容器
7.1 什么是并发容器
针对多线程并发访问来进行设计的集合,称为并发容器
➢ JDK1.5之前,JDK提供了线程安全的集合都是同步容器,线程安全,只能串行执行,性能很差。
➢ JDK1.5之后,JUC并发包提供了很多并发容器,优化性能,替代同步容器
什么是同步容器?线程安全的集合与非安全集合有什么关系?
每次只有一个线程可以访问的集合(同步),称为线程安全的集合,也叫同步容器
➢ Java集合主要为4类:List、Map、Set、Queue,线程不安全的:ArrayList、HashMap..
➢ JDK早期线程安全的集合Vector、Stack、HashTable。
➢ JDK1.2中,还为Collections增加内部Synchronized类创建出线程安全的集合,实现原理synchronized
7.2 常见并发容器特点总结
➢ List容器
① Vector:synchronized实现的同步容器,性能差,适合于对数据有强一致性要求的场景
② CopyOnWriteArrayList :底层数组实现,使用复制副本进行有锁写操作(数据不一致问题),适合读多写少,允 许短暂的数据不一致的场景
➢ Map容器
① Hashtable : synchronized实现的同步容器,性能差,适合于对数据有强一致性要求的场景
② ConcurrentHashMap :底层数组+链表+红黑树(JDK1.8)实现,对table数组entry加锁( synchronized ), 存在一致性问题。适合存储数据量小,读多写少,允许短暂的数据不一致的场景
③ ConcurrentSkipListMap :底层跳表实现,使用CAS实现无锁读写操作。适合与存储数据量大,读写频繁,允许短 暂的数据不一致的场景
➢ Set容器
① CopyOnWriteArraySet :底层数组实现的无序Set
② ConcurrentSkipListSet :底层基于跳表实现的有序Set
7.3 ConcurrentHashMap
JDK1.7结构图
JDK1.8结构图
➢ 底层采用数组+链表+红黑树数据结构
➢ 存入key值,使用hashCode映射数组索引
➢ 集合会自动扩容:加载因子0.75f
➢ 链表长度超过8时,链表转换为红黑树
7.4 CopyOnWriteArrayList
CopyOnWriteArrayList底层数组实现,使用复制副本进行有锁写操作,适合读多写少,允许短 暂的数据不一致的场景。
CopyOnWrite思想:平时查询时,不加锁,更新时从原来的数据copy副本,然后修改副本,最后把原数据 替换为副本。修改时,不阻塞读操作,读到的是旧数据
优缺点
➢ 优点:对于读多写少的场景, CopyOnWrite这种无锁操作性能更好,相比于其它同步容器
➢ 缺点:①数据一致性问题,②内存占用问题及导致更多的GC次数
8. 并发队列
8.1 为什么要用队列
队列是线程协作的利器,通过队列可以很容易的实现数据共享,并且解决上下游处理速度不匹配的问题,典型的生 产者消费者模式
8.2 什么是阻塞队列
➢ 带阻塞能力的队列,阻塞队列一端是给生产者put数据使用,另一端给消费者take数据使用
➢ 阻塞队列是线程安全的,生产者和消费者都可以是多线程
➢ take方法:获取并移除头元素,如果队列无数据,则阻塞
➢ put方法:插入元素,如果队列已满,则阻塞
➢ 阻塞队列又分为有界和无界队列,无界队列不是无限队列,最大值Integer.MAX_VALUE
8.3 常用阻塞队列
- ArrayBlockingQueue 基于数组实现的有界阻塞队列
- LinkedBlockingQueue 基于链表实现的无界阻塞队列
- SynchronousQueue不存储元素的阻塞队列
- PriorityBlockingQueue 支持按优先级排序的无界阻塞队列
- DelayQueue优先级队列实现的双向无界阻塞队列
- LinkedTransferQueue基于链表实现的无界阻塞队列
- LinkedBlockingDeque基于链表实现的双向无界阻塞队列