并发编程专题-基础-9、Java各种锁
1. Synchronized 8 锁问题⭐️🔴
示例代码:[[Lock_8.java]]
❕
1 |
|
1.1. 锁对象
1.1.1. 情况 1 和 2⭐️🔴
❕ ^tcdpjx
锁的是==当前对象 this==,被锁定后,其它的线程都不能进入到当前对象的其它的 synchronized 方法
一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个 synchronized 方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些 synchronized 方法❕
1.1.2. 情况 3
加个普通方法后发现和同步锁无关
1.1.3. 情况 4
换成两个对象后,不是同一把锁了,互不影响。
1.2. 锁类
1.2.1. 情况 5 和 6
对于静态同步方法,锁是当前类的 Class 对象。
1.2.2. 情况 7 和 8
synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁。
具体表现为以下 3 种形式:
- 对于普通同步方法,锁是当前实例对象,锁的是当前对象 this,
- 对于同步方法块,锁是 Synchonized 括号里配置的对象
- 对于静态同步方法,锁是当前类的 Class 对象。
1.3. 总结
synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁。
具体表现为以下 3 种形式。
对于普通同步方法,锁是当前实例对象,即 this,比如例子中的一部部手机。
对于静态同步方法,锁是当前类的 Class 对象,比如例子中的 Phone.class 这个 Class 对象。
对于同步方法块,锁是 Synchonized 括号里配置的对象。
1.4. 原理概括
2. 乐观锁和悲观锁
3. 公平锁和非公平锁
公平锁/非公平锁:并发包中 ReentrantLock 的创建可以指定构造函数的 boolean 类型来得到公平锁或非公平锁,默认是非公平锁。
关于两者区别:
公平锁:Threads acquire a fair lock in the order in which they requested it.
公平锁,就是很公平,在并发情况下,每个线程在获取锁时会查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中【比如 AQS 中的 CLH 队列】,以后会按照 FIFO 的规则从队列中取到自己。
非公平锁:非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采取类似公平锁那种方式。
3.1. Synchronized
是 非公平锁
3.2. ReentrantLock
默认创建非公平锁,可以设置为 true,创建公平锁
4. 可重入锁
4.1. ReentrantLock
ReentrantLock: 是基于 AQS 同步器实现的可重入锁,手动上几次锁,就需要手动释放多少次锁
5. 独占锁 共享锁
https://blog.csdn.net/varyall/article/details/80330216
https://zhuanlan.zhihu.com/p/105991128
5.1. 独占锁
#todo
5.1.1. Synchronized
5.1.2. ReentrantLock
5.1.3. CyclicBarrier
5.1.3.1. 原理
通过可重入独占锁所实现的同步用障碍锁,CyclicBarrier 基于 Condition 来实现的,是 ReentrantLock 和 Condition 的组合使用
5.1.3.2. 使用场景
用于多线程计算数据,最后合并计算结果的场景。每个 parter
负责一部分计算,最后的线程 barrierAction
线程进行数据汇总。
5.2. 共享锁
5.2.1. ReentrantReadWriteLock
读读可共享 读写 写写不共享
读写锁,读锁是共享锁,写锁是独占锁
5.2.2. CountDownLatch
==可重入共享锁,基于 AQS 的共享模式的使用==
5.2.2.1. 原理⭐️🔴
CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
CountDownLatch 是共享锁的一种实现,它==默认构造 AQS 的 state 值为 count==。当线程使用 countDown 方法时,其实使用了 tryReleaseShared
方法以 CAS 的操作来减少 state, 直至 state 为 0 就代表所有的线程都调用了 countDown 方法。当调用 await 方法的时候,如果 state 不为 0,就代表仍然有线程没有调用 countDown 方法,那么就把已经调用过 countDown 的线程都放入阻塞队列 Park,并自旋 CAS 判断 state == 0,直至最后一个线程调用了 countDown,使得 state == 0,于是阻塞的线程便判断成功,全部往下执行。
❕
5.2.2.2. Demo
thread0308:[[CountDownLatchDemo.java]]
case_java8 :[[TestCountDownLatch.java]]
5.2.2.3. 使用场景⭐️🔴
- 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :
new CountDownLatch(n)
,每当一个任务线程执行完毕,就将计数器减 1countdownlatch.countDown()
,当计数器的值变为 0 时,在CountDownLatch上 await()
的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。❕ - 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的
CountDownLatch
对象,将其计数器初始化为 1 :new CountDownLatch(1)
,多个线程在开始执行任务前首先coundownlatch.await()
,当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。
5.2.3. Semaphore
5.2.3.1. 原理⭐️🔴
==不可重入的共享锁==
基于计数的信号量,可以用来控制同时访问特定资源的线程数量,阈值范围内,多个线程争抢获取许可信号,完成自己的任务后归还许可信号。超过阈值,申请许可信号的线程将会被阻塞,直到有新的许可信号可以使用。
❕
5.2.3.2. demo
thread0308: [[CyclicBarrierDemo.java]]
case_java8 :[[TestCyclicBarrier.java]]
5.2.3.3. 使用场景⭐️🔴
Semaphore 可以用来做流量控制,特别公用资源有限的应用场景,比如数据库连接。假设有一个需求,要读取几万个文件的数据,因为都是 IO 密集型任务,我们可以启动几十个线程并发的读取,但是如果读到内存后,还需要进行存储到数据库中,而数据库的连接数只有 10 几个,这时我们必须控制只有十个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,我们就可以使用 Semaphore 来做流控。
❕
5.3. 比较 CyclicBarrier 与 CountDownLatch⭐️🔴
5.3.1. 相同点
这两个类都可以实现==一组线程在到达某个条件之前进行等待==,它们内部都有一个计数器,当计数器的值不断的减为 0 的时候所有阻塞的线程将会被唤醒。
5.3.2. 不同点
- CyclicBarrier 的计数器由 CyclicBarrier 自己控制,而 CountDownLatch 的计数器则由使用者通过调用 countDown 来控制,在 CyclicBarrier 中线程调用 await 方法不仅会将自己阻塞还会将计数器减 1,而在 CountDownLatch 中线程调用 await 方法只是将自己阻塞而不会减少计数器的值,减少计数器的值是通过调用 countDown 方法来完成的。
- CountDownLatch 只能拦截一轮,而 CyclicBarrier 可以实现循环拦截。一般来说用 CyclicBarrier 可以实现 CountDownLatch 的功能,而反之则不能,例如上面的赛马程序就只能使用 CyclicBarrier 来实现。
- CyclicBarrier 还提供了:reset()、getNumberWaiting()、isBroken() 等比较有用的方法。
- 对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。
- CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。
5.3.3. 案例
19 | CountDownLatch 和 CyclicBarrier:如何让多线程步调一致?
6. 读写锁⭐️🔴
❕
https://www.bilibili.com/video/BV1ar4y1x727?p=156&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204
6.1. ReentrantReadWriteLock
6.1.1. 锁降级⭐️🔴
6.1.1.1. 可以写后读
6.1.1.2. 不可读后写
如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略
线程获取读锁是不能直接升级为写入锁的。❕
写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。
因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。
因此,分析读写锁 ReentrantReadWriteLock,会发现它有个潜在的问题:
读锁全完,写锁有望;写锁独占,读写全堵;
如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,见前面 Case《code 演 LockDownGradingDemo》即 ReadWriteLock 读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须等待,这是一种悲观的读锁:还在读,就不能写,省的数据乱。 ❕
6.1.2. 缓存案例
以 使用 ReentrantReadWriteLock 更新缓存 为例说明,锁降级保证写操作可见性的原理
1 |
|
总结:释放写锁之前获取读锁,即锁降级的作用
❕ ^puhqp0
- 一个线程的锁降级过程,相当于锁重入。
- 保证当前线程在赋值完成后使用时,不会被其他线程变更
- 降级为读锁,其他线程也可以读取到新数据,保证数据可见性 ❕
❕
想写后读,那么必须要先获取读锁,然后再释放写锁。
6.1.3. 底层原理
#todo
[[并发编程_原理.pdf]]6.2. StampedLock
6.2.1. 是什么
ReentrantReadWriteLock 解决了ReentrantLock 读读不共享,影响效率的问题
而 StampedLock解决 ReentrantReadWriteLock 的写饥饿问题
同时 StampedLock 可以读写并发执行,读的过程中允许写锁介入,不同于获取 ReentrantReadWriteLock 必须读锁全部解锁才能获取写锁 ❕
StampedLock(邮戳锁,也叫票据锁)是 JDK1.8 中新增的一个读写锁,也是对 JDK1.5 中的读写锁 ReentrantReadWriteLock 的优化。
stamp(戳记,long 类型):代表了锁的状态。当 stamp 返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的 stamp 值。
6.2.2. 写饥饿问题⭐️🔴
ReentrantReadWriteLock 实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,假如当前 1000 个线程,999 个读,1 个写,有可能 999 个读取线程长时间抢到了锁,那 1 个写线程就悲剧了因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写。
如何解决锁饥饿问题?
- 使用“公平”策略可以一定程度上缓解这个问题
- new ReentrantReadWriteLock(true);
- 但是“公平”策略是以牺牲系统吞吐量为代价的
6.2.3. StampedLock 的优化⭐️🔴
📙❕ ^ev7eip
ReentrantReadWriteLock
允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,
读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的 synchronized 速度要快很多,原因就是在于 ReentrantReadWriteLock 支持读并发,读读可共享。
存在问题:
- 读写互斥,即读的时候,无法获取写锁
- 存在写饥饿问题
StampedLock
ReentrantReadWriteLock 的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是,StampedLock 采取乐观锁模式,获取读锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。
6.2.4. 作用原理
读的过程中也允许获取写锁介入
如果验戳失败则会由乐观读升级为读锁,使得其他线程无法获取写锁
6.2.5. 缺点⭐️🔴
- StampedLock 不支持重入,没有 Re 开头
- StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。
- 使用 StampedLock 一定不要调用中断操作,即不要调用 interrupt() 方法
6.2.6. Demo
黑马 case_java8 :[[TestStampedLock.java]]
锁优化
7. 死锁
❕ ^vvvrv6
7.1. Demo
1 |
|
7.2. 查找
7.3. 解决方案
7.3.1. 线上 dump 步骤和注意事项⭐️🔴
❕ ^a8qnjr
7.4. 面试题
面试专题-7、八股文8. 参考与感谢
8.1. 黑马程序员
8.1.1. Java 虚拟机
https://www.bilibili.com/video/BV1yE411Z7AP?p=177
资料已下载:013-DemoCode/jvm
8.1.2. JUC 并发编程
https://www.bilibili.com/video/BV16J411h7Rd?p=272&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204
资料已下载:heima-concurrent
8.2. 尚硅谷 - 周阳
https://www.bilibili.com/video/BV1ar4y1x727?p=156&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204
示例代码:013-DemoCode/thread0308
8.3. 网络笔记
https://blog.csdn.net/m0_46701838/article/details/111441290
https://blog.csdn.net/varyall/article/details/80330216
8.3.1. lockdowngrading
https://blog.csdn.net/qq_43478625/article/details/121598530