1. Synchronized 8 锁问题⭐️🔴

示例代码:[[Lock_8.java]]

%%
0915-🏡⭐️◼️阿里规约🔜MSTM📝 能用对象锁就不用类锁。尽可能使得加锁代码块工作量变小,避免在锁块中调用 RPC 方法◼️⭐️-point-202301230915%%

1
2
3
4
5
6
7
8
*1 标准访问,请问先打印邮件还是短信
*2 暂停4秒钟在邮件方法,请问先打印邮件还是短信
*3 新增普通sayHello方法,请问先打印邮件还是hello
*4 两部手机,请问先打印邮件还是短信
*5 两个静态同步方法,同一部手机,请问先打印邮件还是短信
*6 两个静态同步方法,2部手机,请问先打印邮件还是短信
*7 1个静态同步方法,1个普通同步方法,同一部手机,请问先打印邮件还是短信
*8 1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信

1.1. 锁对象

1.1.1. 情况 1 和 2⭐️🔴

%%
▶2.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230512-1442%%
❕ ^tcdpjx

锁的是==当前对象 this==,被锁定后,其它的线程都不能进入到当前对象的其它的 synchronized 方法
一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个 synchronized 方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些 synchronized 方法%%
0918-🏡⭐️◼️实例锁同一时刻🔜MSTM📝 只允许 1 个线程访问该实例的任何一个 synchronized 方法◼️⭐️-point-202301230918%%

image.png

1.1.2. 情况 3

加个普通方法后发现和同步锁无关

1.1.3. 情况 4

换成两个对象后,不是同一把锁了,互不影响。

1.2. 锁类

image.png

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

- [ ] 🚩 - 锁分类整理 - 🏡 2023-01-26 22:58

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

https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/AQS.md#42-countdownlatch-%E7%9A%84%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B

==可重入共享锁,基于 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,于是阻塞的线程便判断成功,全部往下执行。
%%
0918-🏡⭐️◼️CountDownLatch 原理🔜MSTM📝 JVM 构造一个 state 是 count 的 AQS,有线程调用 countdown 方法 state 就减一直至为 0,否则将所有已经调用过的线程 park 住◼️⭐️-point-202301230918%%

5.2.2.2. Demo

thread0308:[[CountDownLatchDemo.java]]
case_java8 :[[TestCountDownLatch.java]]

5.2.2.3. 使用场景⭐️🔴
  1. 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减 1 countdownlatch.countDown(),当计数器的值变为 0 时,在 CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。❕%%
    0920-🏡⭐️◼️CountDownLatch 的典型应用场景:在所有的资源、监控等辅助线程启动完毕之后启动程序的主线程◼️⭐️-point-202301230920%%
  2. 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 :new CountDownLatch(1),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。

5.2.3. Semaphore

5.2.3.1. 原理⭐️🔴

==不可重入的共享锁==

基于计数的信号量,可以用来控制同时访问特定资源的线程数量,阈值范围内,多个线程争抢获取许可信号,完成自己的任务后归还许可信号。超过阈值,申请许可信号的线程将会被阻塞,直到有新的许可信号可以使用
%%
0921-🏡⭐️◼️Semaphore 原理是什么🔜MSTM📝 基于计数的信号量,用于控制访问特定资源的线程数量。阈值范围内,多个线程争抢获取信号量,完成自己的功能后释放信号量。阈值范围外,争抢信号量的线程将会被阻塞,直到有信号量可以使用。◼️⭐️-point-202301230921%%

5.2.3.2. demo

thread0308: [[CyclicBarrierDemo.java]]
case_java8 :[[TestCyclicBarrier.java]]

5.2.3.3. 使用场景⭐️🔴

Semaphore 可以用来做流量控制,特别公用资源有限的应用场景,比如数据库连接。假设有一个需求,要读取几万个文件的数据,因为都是 IO 密集型任务,我们可以启动几十个线程并发的读取,但是如果读到内存后,还需要进行存储到数据库中,而数据库的连接数只有 10 几个,这时我们必须控制只有十个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,我们就可以使用 Semaphore 来做流控。
%%
0922-🏡⭐️◼️Semaphore 适合什么场景?共用资源有限的场景,比如数据库连接◼️⭐️-point-202301230922%%

5.3. 比较 CyclicBarrier 与 CountDownLatch⭐️🔴

5.3.1. 相同点

这两个类都可以实现==一组线程在到达某个条件之前进行等待==,它们内部都有一个计数器,当计数器的值不断的减为 0 的时候所有阻塞的线程将会被唤醒。

5.3.2. 不同点

  1. CyclicBarrier 的计数器由 CyclicBarrier 自己控制,而 CountDownLatch 的计数器则由使用者通过调用 countDown 来控制,在 CyclicBarrier 中线程调用 await 方法不仅会将自己阻塞还会将计数器减 1,而在 CountDownLatch 中线程调用 await 方法只是将自己阻塞而不会减少计数器的值,减少计数器的值是通过调用 countDown 方法来完成的。
  2. CountDownLatch 只能拦截一轮,而 CyclicBarrier 可以实现循环拦截。一般来说用 CyclicBarrier 可以实现 CountDownLatch 的功能,而反之则不能,例如上面的赛马程序就只能使用 CyclicBarrier 来实现。
  3. CyclicBarrier 还提供了:reset()、getNumberWaiting()、isBroken() 等比较有用的方法。
  4. 对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。
  5. CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。

5.3.3. 案例

19 | CountDownLatch 和 CyclicBarrier:如何让多线程步调一致?

6. 读写锁⭐️🔴

image.png
%%
1550-🏡⭐️◼️锁的发展过程:synchronized 太重,底层使用 mutex,发展出使用 CAS 的 ReentrantLock,但是它读读不共享,又出现了 ReentrantReadWriteLock,解决了 ReentrantLock 读读不共享问题,但是它有写饥饿和锁降级问题,所以又出现了 StampedLock◼️⭐️-point-202301241550%%

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. 不可读后写

如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略
线程获取读锁是不能直接升级为写入锁的。❕%%
0916-🏡⭐️◼️在 ReentrantReadWriteLock 中,如果读锁在使用?那么尝试获取写锁的线程都会被阻塞,直接所有读锁释放。这是一种悲观锁的策略。◼️⭐️-point-202301230916%%

写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。
因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。

因此,分析读写锁 ReentrantReadWriteLock,会发现它有个潜在的问题:
读锁全完,写锁有望;写锁独占,读写全堵;
如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,见前面 Case《code 演 LockDownGradingDemo》即 ReadWriteLock 读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须等待,这是一种悲观的读锁:还在读,就不能写,省的数据乱。%%
2301-🏡⭐️◼️ReentrantReadWriteLock 读锁不释放其他线程能否获取到写锁?◼️⭐️-point-202301262301%%

6.1.2. 缓存案例

使用 ReentrantReadWriteLock 更新缓存 为例说明,锁降级保证写操作可见性的原理
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class CacheData {  
Object data;
// 是否有效,如果失效,需要重新计算 data
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) { // 数据失效
// 获取写锁前必须释放读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新
if (!cacheValid) {
data = "xxx";
cacheValid = true;
}
// 释放写锁前降级为读锁,这样能够让其它线程读取缓存
// 读锁同时会锁住缓存,让其他线程无法获取到写锁,保证数据一致性
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock();
}
}
// 自己用完数据, 释放读锁
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}

总结:释放写锁之前获取读锁,即锁降级的作用
%%
▶80.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230306-1846%%
❕ ^puhqp0

  1. 一个线程的锁降级过程,相当于锁重入。
  2. 保证当前线程在赋值完成后使用时,不会被其他线程变更
  3. 降级为读锁,其他线程也可以读取到新数据,保证数据可见性%%
    2303-🏡⭐️◼️锁降级在缓存中的作用?相当于锁重入;加了读锁后,其他线程无法获取写锁,保证了当前数据的有效性;加了读锁降级为读锁,可以让其他线程进行读操作,保证数据的可见性◼️⭐️-point-202301262303%%

    %%
    0916-🏡⭐️◼️ReentrantReadWriteLock 在缓存更新中加解读写锁的顺序是什么?先获取读锁,然后需要写入的时候,释放读锁,获取写锁,写完之后,先获取读锁,再释放写锁,最后再释放读锁◼️⭐️-point-202301230916%%

    image.png

想写后读,那么必须要先获取读锁,然后再释放写锁。

6.1.3. 底层原理

#todo

- [ ] 🚩 - 底层原理深入学习 - 🏡 2023-01-27 06:35 [[并发编程_原理.pdf]]

6.2. StampedLock

image.png

6.2.1. 是什么

ReentrantReadWriteLock 解决了ReentrantLock 读读不共享,影响效率的问题
而 StampedLock解决 ReentrantReadWriteLock 的写饥饿问题

同时 StampedLock 可以读写并发执行,读的过程中允许写锁介入,不同于获取 ReentrantReadWriteLock 必须读锁全部解锁才能获取写锁%%
2306-🏡⭐️◼️StampedLock 解决的问题是什么?ReentrantLock 解决了 Synchronized 效率低的问题,ReentrantReadWriteLock 解决了 ReentrantLock 读读不共享问题,StampedLock 解决了 ReentrantReadWrite 读写互斥的问题。◼️⭐️-point-202301262306%%

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 的优化⭐️🔴

%%
▶81.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230306-1856%%
📙❕ ^ev7eip

ReentrantReadWriteLock
允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,
读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的 synchronized 速度要快很多,原因就是在于 ReentrantReadWriteLock 支持读并发,读读可共享。

存在问题:

  1. 读写互斥,即读的时候,无法获取写锁
  2. 存在写饥饿问题

StampedLock
ReentrantReadWriteLock 的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是,StampedLock 采取乐观锁模式,获取读锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。

6.2.4. 作用原理

读的过程中也允许获取写锁介入

如果验戳失败则会由乐观读升级为读锁,使得其他线程无法获取写锁

6.2.5. 缺点⭐️🔴

  • StampedLock 不支持重入,没有 Re 开头
  • StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。
  • 使用 StampedLock 一定不要调用中断操作,即不要调用 interrupt() 方法

image.png
image.png
image.png
image.png

6.2.6. Demo

黑马 case_java8 :[[TestStampedLock.java]]

锁优化

image.png

7. 死锁

%%
▶2.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230412-0904%%
❕ ^vvvrv6

7.1. Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/*
* 死锁是指两个或者两个以上的进程在执行过程中,因抢夺资源而造成的一种互相等待的现象,
* 若无外力干涉它们将都无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,
* 死锁出现的可能性也就很低,否则就会因争夺有限的资源而陷入死锁。
* */

import java.util.concurrent.TimeUnit;

class HoldLockThread implements Runnable{
private String lockA;
private String lockB;

public HoldLockThread(String lockA,String lockB){
this.lockA = lockA;
this.lockB = lockB;
}

public void run(){
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+"\t自己持有:"+lockA+"\t尝试获得:"+lockB);
//暂停一下
try{ TimeUnit.SECONDS.sleep(2); }catch (InterruptedException e){e.printStackTrace();}

synchronized (lockB){
System.out.println(Thread.currentThread().getName()+"\t自己持有:"+lockB+"\t尝试获得:"+lockA);
}
}
}
}

public class DeadLockDemo {
public static void main(String[] args){
String lockA = "lockA";
String lockB = "lockB";

new Thread(new HoldLockThread(lockA,lockB),"ThreadAAA").start();
new Thread(new HoldLockThread(lockB,lockA),"ThreadBBB").start();

/*
* linux ps -ef|grep xxxx ls -l查看当前进程的命令
* windows下的java运行程序,也有类似ps的查看进程的命令,但是目前我们需要查看的只是java
* jps = java ps jps -l
* jstack
* */
}
}

7.2. 查找

7.3. 解决方案

7.3.1. 线上 dump 步骤和注意事项⭐️🔴

%%
▶1.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230412-0818%%
❕ ^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

8.3.2. StampedLockDemo

https://www.yuque.com/liuyanntes/sibb9g/gkbe3l