目录
1. 简介
2. Atomic包
2.1 什么是原子类
2.2 Atomic包里的类
3. CAS
3.1 CAS是什么
3.2 Java中对CAS的实现
3.3 CAS的缺陷
4. JUC里面的常见锁
4.1 锁分类
4.1.1 按上锁方式划分
4.1.2 按特性划分
4.1.3 其他锁
4.2 Synchronized和JUC的锁对比
5. 锁原理分析
5.1 AQS
5.2 ReentrantLock源码分析-锁的获取
5.3 ReentrantLock源码分析-锁的释放
5.4 公平锁和非公平锁源码实现区别
5.5 读写锁ReentrantReadWriteLock
5.6 锁优化
6. 线程协作工具类
6.1 CountDownLatch计数门闩
6.2 Semaphore信号量
6.3 CyclicBarrier循环栅栏
6.4 Condition接口
7. 并发容器
7.1 什么是并发容器
7.2 常见并发容器特点总结
7.3 ConcurrentHashMap
7.4 CopyOnWriteArrayList
8. 并发队列
8.1 为什么要用队列
8.2 什么是阻塞队列
8.3 常用阻塞队列
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基于链表实现的双向无界阻塞队列