• 多线程---进阶



    目录

    🥬常见的锁策略

    🌵乐观锁和悲观锁

    🌵读写锁

    🌵重量级锁和轻量级锁

    🌵自旋锁和挂起等待锁

    🌵公平锁和非公平锁

    🌵可重入锁和不可重入锁

    🥬CAS

    🥬synchronized

    🥬Callable接口

    🥬JUC(java.util.concurrent) 的常见类

    🌵ReentrantLock

    🌵Semaphore

    🌵CountDownLatch

    🥬线程安全的集合类

    🌵ConcurrentHashMap

    🥬死锁

    🥬小结


    🥬常见的锁策略

    因为加锁是一个开销比较大的操作,所以我们希望在特定的场景下,针对场景做一些取舍,让锁更加高效一些。

    🌵乐观锁和悲观锁

    乐观锁:假设一般情况下都不会产生锁冲突,因此就尝试直接访问数据,发现了锁冲突,再去处理。

    悲观锁:假设一般情况下都会产生冲突,因此先进行处理,再去尝试访问数据。

    🌵读写锁

    当多个线程尝试修改同一个变量时,线程不安全。当多个线程同时读一个变量时,线程安全。两个读线程之间,不存在线程不安全问题,不必互斥;两个写线程之间,存在线程不安全问题,就需要互斥;一个读线程与一个写线程之间存在线程不安全,需要互斥。所以我们可以根据读写的不同场景,给读和写分别加上锁。

    synchronized没有对读写进行区分,只要使用了就一定互斥。

    这里的互斥是从CPU来的,CPU提供了一些特殊指令,通过这些指令来完成互斥,操作内核对这些指令进行了封装,并实现了阻塞等待。CPU提供一些特殊指令,操作系统对这些指令进行封装,提供了一个mutex(互斥量),JVM相当于对操作系统的mutex再封装一层,实现了synchronized这样的锁。如果当前的锁,是通过内核的mutex来完成的,开销往往比较大,如果当前的锁是在用户态通过一些其他的操作来完成的,开销往往比较小。

    🌵重量级锁和轻量级锁

    重量级锁:加锁解锁开销很大,往往是通过内核来完成。

    轻量级锁:加锁开锁开销更小,往往是通过用户态来完成。

    悲观锁做的工作更多,开销比较大,很大可能性是重量级锁;乐观锁做的工作更少,开销更小,很大可能性是轻量级锁。

    🌵自旋锁和挂起等待锁

    挂起等待锁:如果线程获取不到锁,就会阻塞等待,什么时候结束阻塞,取决于操作系统的具体调度,当线程挂起的时候不占用CPU。

    自旋锁:如果线程获取不到锁,不阻塞等待,而是循环快速的再试一次。(如果线程1得到了锁,线程2就会快速循环的尝试获取锁,一旦线程1把锁释放,线程2就能第一时间获取到锁)。

    自旋锁跟挂起等待锁相比,更节省操作系统调度线程的开销,但是更浪费CPU资源。

    🌵公平锁和非公平锁

    公平锁:遵守先来后到的规则。(如果线程1比线程2更先来,那么线程1会比线程2更先获取到锁)

    非公平锁:不遵守先来后到的规则,获取锁的概率取决于操作系统的调度。(synchronized是非公平锁)

    对于操作系统的调度器来说,默认是不公平的,默认获取锁的概率是均等的(不考虑线程的优先级情况下),要想实现公平锁,需要额外的数据结构(就比如说队列,用队列来记录线程的先来后到的过程)。大部分情况下,使用非公平锁就够用了,如果有些场景下,我们期望对于线程的调度的时间成本是可控的,这个时候就更需要公平锁了(比如我们需要通过10个线程并发执行10个任务,如果现在是非公平锁,此时若遇到前9个线程一直霸占着锁,则第10个线程始终拿不到锁,这时候用公平锁能更好的保证这10个任务能均衡的实行)。

    🌵可重入锁和不可重入锁

    可重入锁:之前有说过synchronized是可重入锁,就是针对同一把锁,连续加锁两次,此时不会死锁。为什么不会"死锁"呢?此时加锁的时候会让当前的锁记录一下这个锁是谁持有的,如果发现现在有同一个线程再次尝试获取锁,此时不会阻塞等待,代码会继续运行,此时这个锁里面会维护一个计算器,这个计算器就计算了当前这个线程针对这把锁尝试加锁了几次,每次解锁,计数器--,直到计数器为0,此时才真正的释放锁。

    不可重入锁:如果针对同一把锁,连续加锁两次,会造成"死锁"。

    1. //以下就是一个可重入锁的例子
    2. synchronized void func1(){
    3. func2();
    4. }
    5. synchronized void func2(){
    6. }
    7. //func1加锁操作,可以成功。接下来执行func2,再次尝试加锁,如果是不可重入锁,那么当前锁已经被占用
    8. //了,此时func2这个加锁就应该阻塞等待,等待func1锁释放,func2才能获取得到锁,但是由于func2在func1
    9. //内部,因此一旦func2阻塞,就会导致func1页阻塞,于是func1就无法执行到释放锁,这个时候代码就僵住了,
    10. //此时就会造成"死锁"现象

    🥬CAS

    CAS:compare and swap,比较和交换,拿两个内存进行比较,或者是拿寄存器的值和内存的值进行比较,然后交换比较的这两个东西。这个操作是一个"原子的",CPU提供了一组CAS相关的指令,使用一条指令就可以完成上面的比较和交换过程。

    例如:

    1. int num=1;
    2. num++;
    3. //线程不安全,如果此时多个线程并发执行i++,就需要加锁,但是加锁是低效的,此时就可以CSA
    4. //即可以保证高效,又能保证线程安全

    基于CSA实现了一些"原子类",这些原子类也相当于一个整数,就可以实现一些++、--操作运算

    都是不需要加锁就能保证线程安全。(java标准库中,也提供了原子类)

    标准库中:

    1. public static void main(String[] args) {
    2. AtomicInteger num = new AtomicInteger(10);
    3. // 这样的方法能够保证原子性. 内部就是通过 CAS 来实现的.
    4. // 这个操作就相当于 num++
    5. num.getAndIncrement();
    6. // 这个操作就相当于 ++num
    7. num.incrementAndGet();
    8. }

    模拟实现getAndIncrement

    1. private int value;
    2.    public int getAndIncrement() {
    3.        int oldValue = value;
    4.        while ( CAS(value, oldValue, oldValue+1) != true) {
    5.            oldValue = value;
    6.       }
    7.        return oldValue;
    8.   }

    如果有两个线程同时调用getAndIncrement,那它是怎么不用加锁就能保证线程安全的呢?

    CAS其实就是在感知这两个操作之间是否夹杂了其他线程的操作,是否有其他线程在这个过程中偷偷的修改了数据 ,如果没有,此时就能直接把数据给改了,如果其他线程已经修改了,那么就重新读取旧值,交给下次循环来判定。

    CAS中的ABA问题(经典面试题):

    ABA问题就是说我们在使用CAS的时候也无法区分这个数据A是始终没变,还是从A变成了B,然后又变回了A,大部分情况下这种问题影响不大,但是有时候却会引入bug。

    比如说我们去取钱,如果此时机子突然卡了,我们按了两下取钱,此时线程1和线程2都尝试进行-50操作

    1、线程1获取到当前值为100,线程2也获取到当前值为100

    2、线程1执行-50操作,比较当前值和oldval值是否一致,一致就扣钱。线程1执行完毕后,账户余额就变成了50

    3、在线程2执行-50操作之前,如果突然有人往账户里面又打了50,此时账户余额又变成100,此时线程2尝试-50,对比当前值和oldval值一致,于是又扣了一次钱

    此时本来只需要扣一次钱但是却扣了两次,这就是ABA引起的。

    怎么解决ABA问题呢?

    这时候我们可以引入"版本号",给100这个数据搭配一个版本号1,后续每次操作数据100都要对版本号+1,版本号相同才修改,不同就不修改。

    1、线程1获取当前版本号为1,线程2也获取到当前版本号为1

    2、线程1执行-50操作,对比当前版本号和之前读取的版本号一致就扣钱,同时把当前版本号改成了2

    3、线程3执行+50操作,账户余额变成了100,同时版本号变成了3

    4、线程2执行-50操作,当前版本号(3)和之前读取的版本号(1)不一致,这个操作不执行,此时最终扣钱的操作就执行了一次

    🥬synchronized

    特点:

    1、synchronized开始使用的时候是乐观锁,如果发现锁冲突概率比较高,就会自动转为悲观锁

    2、synchronized不是读写锁

    3、synchronized开始的时候是轻量级锁,如果被持有的时间较长或者锁的冲突较高,就会升级成重量级锁

    4、synchronized是一个非公平锁

    5、synchronized是一个可重入锁

    6、synchronized为轻量级锁的时候,大概率是一个自旋锁,为重量级锁的时候大概是一个挂起等待锁

    synchronized加锁工作过程:JVM将synchronized的锁分为无锁、偏向锁、轻量级锁、重量级锁,会根据情况依次升级

    偏向锁:

    偏向锁只是在对象头中设置了一个"偏向锁标记",这个只做标记,就比真正的加锁要高效很多。偏向锁其实也是一种乐观锁,就是相当于在赌,这个锁不会产生竞争,如果赌赢了,那么这次就不真正的加锁,直到锁的释放,这整个过程根本没有涉及到加锁解锁,因此就会更快;如果赌输了,那么在进行加锁操作,就比如线程1尝试获取这把锁,锁进入偏向状态,此时如果有一个线程2也尝试竞争这把锁,那么此时线程1就会抢先把这个锁拿到,然后线程2再去等待……

    轻量级锁状态:

    完全没有锁竞争的时候---偏向锁,如果出现了竞争,但是这个时候竞争比较小,此时就会进入到"轻量级锁"状态。此时的轻量级锁,就是基于CAS实现的自旋锁,是属于完全在用户状态完成的操作,因此里面不涉及到内核态和用户态的切换,也不涉及到线程的阻塞等待和调度,但是多用了以下CPU,能保证更高效的获取到锁(线程1一旦释放锁,线程2立即就能获取到)

    重量级锁状态:

    如果当前锁冲突概率太大了,如果再继续使用轻量级自旋锁,就会浪费大量的CPU(在等待的时候CPU是空转的),这时候就可以使用更重量的挂起等待锁,对于挂起等待锁来说,锁等待的过程是释放CPU(不占用CPU),但是也引入了线程的阻塞和调度开销

    锁的粗化:

    锁的粒度:就是synchronized代码块包含了多少代码,如果包含的代码比较多,认为锁的粒度比较粗;如果包含的代码比较少,认为锁的粒度比较细

    锁的粒度的粗细取决于使用场景,大部分情况下细点好,但是也有特殊情况。

    1. func(){
    2. synchronized(this){
    3. 任务1
    4. }
    5. synchronized(this){
    6. 任务2
    7. }
    8. synchronized(this){
    9. 任务3
    10. }
    11. }

    以上这种情况,三次加锁完后三个任务,锁的粒度细,但是效率比较低效,可以只加一次锁,就能完成三个任务,锁的粒度变粗,但是效率比较高效一些。

    1. func(){
    2. synchronized(this){
    3. 任务1
    4. 任务2
    5. 任务3
    6. }
    7. }

    以上过程就是锁的粗化。

    锁消除:

    有些程序中加了synchronized,但其实没有在多线程环境下,也就是此时不会产生锁竞争,加锁是多余操作,此时JVM和编译器会判断是否可以消除,如果可以,直接消除。(JVM和编译器并不能能完全判断正确)

    例如:

    1. StringBuffer sb = new StringBuffer();
    2. sb.append("a");
    3. sb.append("b");
    4. sb.append("c");
    5. sb.append("d");

    由于StringBuffer很多方法都加了synchronized,此时每个append的调用都会涉及到加锁解锁操作,如果此时在单线程中,就没有这个必要,白白浪费资源开销。所以在此之前我们通常会优先使用StringBuilder,因为不涉及线程安全,节省开销。

    🥬Callable接口

    之前在初阶的时候介绍了创建线程的五种方式,Callable也是创建线程的一种方式,它里面包含一个call方法,跟之前Runnable.run方法类似,都是描述一个具体的任务。不同点在于之前创建线程的方式不关注结果(run方法没有返回值),而Callable关注结果(call方法带返回值)。

    例如:创建一个线程计算1+~1000的和

    不用callable接口:

    1. public class ThreadDemo1 {
    2. static class Result {
    3. public int sum = 0;
    4. public Object locker = new Object();
    5. }
    6. public static void main(String[] args) throws InterruptedException {
    7. Result result=new Result();
    8. Thread t=new Thread(){
    9. @Override
    10. public void run() {
    11. int sum=0;
    12. for (int i = 1; i <= 1000; i++) {
    13. sum += i;
    14. }
    15. result.sum=sum;
    16. synchronized (result.locker){
    17. result.locker.notify();
    18. }
    19. }
    20. };
    21. t.start();
    22. //因为当前t线程和主线程是并发执行的关系
    23. //所以让main这个线程先等待 t线程执行完后在唤醒main线程即可
    24. synchronized (result.locker){
    25. while(result.sum==0){
    26. result.locker.wait();
    27. }
    28. }
    29. System.out.println(result.sum);
    30. }
    31. }

    使用callable接口:

    1. public class ThreadDemo2 {
    2. public static void main(String[] args) throws ExecutionException, InterruptedException {
    3. Callable callable=new Callable() {
    4. @Override
    5. public Integer call() throws Exception {
    6. int sum=0;
    7. for(int i=1;i<=1000;i++){
    8. sum+=i;
    9. }
    10. return sum;
    11. }
    12. };
    13. // 由于 Thread 不能直接传一个 callable 实例, 就需要一个辅助的类来包装一下.
    14. FutureTask futureTask = new FutureTask<>(callable);
    15. Thread t = new Thread(futureTask);
    16. t.start();
    17. //尝试在主线程中获取结果
    18. //如果FutureTask中的结果没有生成,此时就会阻塞等待
    19. //直到线程把结果算出来之后,get才会返回
    20. Integer result=futureTask.get();
    21. System.out.println(result);
    22. }
    23. }

    对比以上两段代码我们可以知道,如果我们期望创建一个线程,并关注这个线程产生的返回结果,使用Callable就会比较合适,如果不使用Callable我们可以知道会更麻烦一点,我们需要使用wait、notify、synchronized这些机制相互配合,才能完成这个工作。

    🥬JUC(java.util.concurrent) 的常见类

    concurrent是并发的意思,这个包里面就包含了很多与并发相关的操作(和多线程相关)。

    🌵ReentrantLock

    这是可重入锁的意思,我们平时说的"可重入锁"大部分情况下表示的是锁的一种特性,也就是我们之前提到的synchronized是可重入锁一样,少数情况下就是我们现在说的特指标准库中的这个类。

    1. static class Counter{
    2. public ReentrantLock locker = new ReentrantLock(true);
    3. public int count=0;
    4. public void increase() {
    5. locker.lock();
    6. count++;
    7. locker.unlock();
    8. }
    9. }

    根据以上代码我们可以知道ReentrantLock把加锁和解锁操作拆分开了。

    synchronized和ReentrantLock有什么不同?

    synchronized虽然已经非常先进了,但还有些功能是不具备的,ReentrantLock相对于synchronized进行了补充,synchronized和ReentrantLock是并列的关系,只是风格不同。

    1、ReentrantLock把加锁和解锁拆分成了两个方法,存在遗忘了解锁的风险,但是可以让代码更灵活。

    2、ReentrantLock除了lock和unlock还提供了一个trylock方法。对于lock来说,如果尝试加锁失败就会阻塞等待;对于unlock来说,如果尝试加锁失败,就会直接返回出错,不会阻塞等待。

    3、synchronized是一个非公平锁;ReentrantLock支持两种模式,既可以支持公平锁,也可以支持非公平锁。

    4、ReentrantLock提供了比synchronized更强大的等待唤醒机制。synchronized搭配wait、notify使用,ReentrantLock是搭配了Condition类来完成等待唤醒(能够显式制定唤醒哪个等待的线程)。

    🌵原子类

    作用:了解服务器的运行状态

    1、单位时间内接受了多少个请求

    2、服务器内部某个类被创建出多少个实例

    3、某个方法被调用了多少次

    4、某个代码片段执行了多少时间

    …………

    可以把这些统计到的数据汇总到一个地方(有一个监控服务器来收集这些数据),接下来就可以在监控服务器上看到这些服务器的运行状态。

    🌵Semaphore

    这是Java标准库提供的类。Semaphore---信号量,是一个计数器(int整数),用来描述可用资源的个数,也可以用来控制线程安全。

    创建信号量的时候,设置了一个初始值(可用资源个数),如果把初始值设为1,那么此时这个信号量就只有0,1两种取值,称为"二元信号量",二元信号量和锁的功能类似。P操作相当于加锁,V操作相当于解锁。

    例如:去停车场停车,门口会有一个显示板,上面会显示剩余多少个车位,这样的显示,就相当于"信号量",每次有车进去,相当于申请资源(P),数字就-1,每次有车出来,就相当于释放资源(V),数字就+1。其次,信号量的计数的值应该是一个>=0的值,当值为0的时候就会尝试获取申请资源,此时就会导致阻塞等待。

    代码示例:

    1. public class ThreadDemo4 {
    2. public static void main(String[] args) {
    3. Semaphore semaphore=new Semaphore(4);//初始值为4,表示有4个可用资源
    4. Runnable runnable=new Runnable() {
    5. @Override
    6. public void run() {
    7. try{
    8. System.out.println("准备申请资源");
    9. //先尝试申请资源
    10. semaphore.acquire();
    11. System.out.println("申请资源成功");
    12. Thread.sleep(1000);
    13. //释放资源
    14. semaphore.release();
    15. System.out.println("释放资源完毕");
    16. } catch (InterruptedException e) {
    17. throw new RuntimeException(e);
    18. }
    19. }
    20. };
    21. // 创建 20 个线程.
    22. // 让这 20 个线程来分别去尝试申请资源
    23. for (int i = 0; i < 5; i++) {
    24. Thread t = new Thread(runnable);
    25. t.start();
    26. }
    27. }
    28. }

    🌵CountDownLatch

    如果有6名选手参加100米跑,6个选手同时起跑,当这6名选手都到达终点之后,这场比赛就结束,当每次有选手撞线(CountDownLatch就相当于这个撞线操作),计数器就-1,一直减到0,就说明这场比赛结束,代码不再阻塞,继续往下跑。

    假设某个场景中需要通过多线程来计算一组数据,t0创建了4个线程 t1-t4,t0用来汇总4个线程的结果,这时候t0就可以用CountDownLatch,此时await方法就会阻塞等待,等待这些线程执行完毕,t1-t4每个线程执行完成任务的时候,都会使用countDown()方法来"撞线",当四个线程都"撞线"完成之后,await就返回,就自动往后执行。

    代码示例:

    1. public class ThreadDemo5 {
    2. public static void main(String[] args) throws InterruptedException {
    3. CountDownLatch latch = new CountDownLatch(8);
    4. Runnable runnable = new Runnable() {
    5. @Override
    6. public void run() {
    7. System.out.println("起跑!");
    8. // random 方法得到的是一个 [0, 1) 之间的浮点数.
    9. // sleep 的单位是 ms, 此处 * 10000 意思是 sleep [0, 10) 区间范围内的秒数
    10. try {
    11. Thread.sleep((long) (Math.random() * 10000));
    12. } catch (InterruptedException e) {
    13. e.printStackTrace();
    14. }
    15. latch.countDown();
    16. System.out.println("撞线完成!");
    17. }
    18. };
    19. for (int i = 0; i < 8; i++) {
    20. Thread t = new Thread(runnable);
    21. t.start();
    22. }
    23. latch.await();
    24. System.out.println("比赛结束");
    25. }
    26. }

    相关面试题:

    1、 线程同步的方式有哪些?

    synchronized, ReentrantLock, Semaphore 等都可以用于线程同步。
    2、 为什么有了 synchronized 还需要 juc 下的 lock
    以 juc 的 ReentrantLock 为例,synchronized 使用时不需要手动释放锁, ReentrantLock 使用时需要手动释放, 使用起来更灵活;synchronized 在申请锁失败时,会死等。 ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃;synchronized 是非公平锁,ReentrantLock 默认是非公平锁, 可以通过构造方法传入一个true 开启公平锁模式;synchronized 是通过 Object 的 wait / notify 实现等待-唤醒,每次唤醒的是一个随机等待的 线程, ReentrantLock 搭配 Condition 类实现等待-唤醒,可以更精确控制唤醒某个指定的线程。

    3、 信号量是什么?之前都用在过哪些场景下?

    信号量,用来表示 "可用资源的个数"。本质上就是一个计数器。使用信号量可以实现 "共享锁", 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回,后续线程再进行 P 操作就会阻塞等待,直到前面的线程执行了 V 操作。

    🥬线程安全的集合类

    其实我们之前所学到的集合类大多都是线程不安全的。
    比如说:ArrayList、LinkedList、TreeMap、HashMap、TreeSet、HashSet、Queue……这些都是线程不安全的。
    也有线程安全的,比如说Vector、Stack、HashTable等等。(不建议使用,比较低效
    如果我们需要在多线程环境下使用这些线程不安全的类,就需要做一些特殊处理,最典型的办法就是加锁。
    String也是线程安全的,但是它内部没有进行加锁,因为他是一个不可变对象,没有提供public的方法来修改String的内容。StringBuilder线程不安全,StringBuffer是线程安全的,它是因为内部许多方法都有加锁。
    CopyOnWriteArrayList:写时拷贝
    如果只有一个线程写,此时就只有一份实例,如果又来了一个线程写数据,此时就会在原来的基础上,生成一个新副本。

    🌵ConcurrentHashMap

    ConcurrentHashMap是使用synchronized加锁,ConcurrentHashMap的锁对象是针对数组的每个元素进行加锁(也是针对哈希桶来加锁),所以如果此时有10个线程并发修改哈希表,若当前线程之间计算出的数组位置(hashcode%数组长度)是不同的,则此时没有锁竞争,如果有两个线程修改的元素正好在同一个数组位置上,此时就会产生锁竞争。

    ConcurrentHashMap的特点:

    1、ConcurrentHashMap针对修改操作的加锁,使用的是粒度更小的锁,针对每个哈希桶分别设定锁,大大降低了锁冲突的概率。

    2、ConcurrentHashMap针对读操作,没加锁,而是直接使用volatile。

    3、ConcurrentHashMap更充分利用了CAS的特性,比如获取/修改元素个数(属性)。

    4、更优化的扩容方式,基本思路就是"化整为零",如果某个插入操作触发了扩容,那么这个扩容就需要创建一个更大的内存,然后搬运一部分元素过去,下次再对ConcurrentHashMap操作的时候,在搬运一部分,而不是一次性全部搬运过去,保证每次操作都不至于太慢。比较高效。在这个搬运过程中相当于维护了两套内存,一套是旧的数据,一套是新的数据,插入操作,就只会往新的数据中插入,查找操作的话就需要同时查找旧的和新的数据,当完全搬运完成,再删除旧数据。

    🥬死锁

    锁导致了线程被阻塞,该锁释放之后,对应的线程才会结束阻塞,但是如果有时候可能这个锁永远也释放不了,于是线程也就无法结束阻塞状态,这就是死锁现象。

    产生死锁的场景:

    1、如果一个线程针对一把锁,连续尝试加锁两次,并且该锁不是可重入锁的时候。

    2、两个线程,有两把锁。

    1. void func1(){
    2. synchronized(locker1){
    3. synchronized(locker2){
    4. }
    5. }
    6. }
    7. void func2(){
    8. synchronized(locker2){
    9. synchronized(locker1){
    10. }
    11. }
    12. }

               如果这两个方法并发执行,就可能出现死锁。若此时func1和func2同时获取到了locker1和locker2,locker1要进行锁释放,就要等到locker2的锁释放之后才能继续往下走进行锁释放,但是func2中locker2要进行锁释放,就要等到locker1的锁释放之后才能进行锁释放,于是就造成了死锁。

    3、很多个线程和很多把锁。如果五个人吃饭,但此时只有五根筷子,如果同一时刻,大家都同时拿起左手边的筷子,然后此时再同时尝试拿起右手边的筷子,此时大家都只有一根筷子,没有办法吃饭,此时就造成了死锁。

    死锁的四个必要条件(最后一个条件最重要):

    1、互斥使用,如果一个锁被一个线程占用的时候,别的线程就会阻塞等待。

    2、不可抢占/不可剥夺,线程1如果获取到一把锁,此时线程2不能强行把锁拖过来。

    3、请求和保持,当资源的请求者在请求其他资源的时候,同时要保持之前的资源(线程获取到锁1之后,再尝试获取锁2,此时仍保持对锁1的持有)。

    4、循环等待,此时若线程1先尝试获取锁1和锁2,线程2尝试获取锁2和锁1,这时候就会循环等待。

    如何避免死锁?

    1、尽量避免复杂的设计,避免在某个锁的代码中再尝试获取其他锁。

    2、如果实在需要嵌套使用,一方面要保证持有锁的时间足够断,代码足够简单,另一方面要保证按照统一的顺序来加锁。

    举个例子:假如现在有两把锁locker1和locker2,这时我们需要保证所有的线程都是按固定顺序来获取这两把锁,比如说让所有线程都是先获取locker1再获取locker2,这样的固定顺序其实是破坏了死锁的"循环等待"条件。

    分布式锁:

    不依赖synchronized,也不依赖编程语言,分布式锁是存在一个分布式系统中(存在很多台机器),能起到互斥的效果。

     分布式系统中,也有可能出现死锁的情况,避免死锁的方法:

    1、如上所说,约定锁的顺序

    2、为了防止lock过程,主机宕机锁无法释放,出现的死锁,此时就可以给这个锁加一个过期的时间。

    例题:有三个线程,分别只能打印A,B和C,要求按顺序打印ABC,打印10次。

    1. public class ThreadDemo3 {
    2. // 计数器
    3. private static volatile int COUNTER = 0;
    4. // 定义一个单独的锁对象
    5. private static Object lock = new Object();
    6. public static void main(String[] args) {
    7. // 创建三个线程,并指定线程名,每个线程名分别用A,B,C表示
    8. Thread t1 = new Thread(() -> {
    9. // 循环10次
    10. for (int i = 0; i < 10; i++) {
    11. // 执行的代码加锁
    12. synchronized (lock) {
    13. // 每次唤醒后都重新判断是否满足条件
    14. // 每条线程判断的条件不一样,注意线程t1,t2
    15. while (COUNTER % 3 != 0) {
    16. try {
    17. // 不满足输出条件时,主动等待并释放锁
    18. lock.wait();
    19. } catch (InterruptedException e) {
    20. e.printStackTrace();
    21. }
    22. }
    23. // 满足输出条件,打印线程名,每条线程打印的内容不同
    24. System.out.print(Thread.currentThread().getName());
    25. // 累加计数
    26. COUNTER++;
    27. // 唤醒其他线程
    28. lock.notifyAll();
    29. }
    30. }
    31. }, "A");
    32. Thread t2 = new Thread(() -> {
    33. for (int i = 0; i < 10; i++) {
    34. synchronized (lock) {
    35. while (COUNTER % 3 != 1) {
    36. try {
    37. lock.wait();
    38. } catch (InterruptedException e) {
    39. e.printStackTrace();
    40. }
    41. }
    42. System.out.print(Thread.currentThread().getName());
    43. COUNTER++;
    44. lock.notifyAll();
    45. }
    46. }
    47. }, "B");
    48. Thread t3 = new Thread(() -> {
    49. for (int i = 0; i < 10; i++) {
    50. synchronized (lock) {
    51. while (COUNTER % 3 != 2) {
    52. try {
    53. lock.wait();
    54. } catch (InterruptedException e) {
    55. e.printStackTrace();
    56. }
    57. }
    58. // 换行打印
    59. System.out.println(Thread.currentThread().getName());
    60. COUNTER++;
    61. lock.notifyAll();
    62. }
    63. }
    64. }, "C");
    65. // 启动线程
    66. t1.start();
    67. t2.start();
    68. t3.start();
    69. }
    70. }

    🥬小结

    以上就是今天的内容了,有什么问题都欢迎大家在评论区留言✌✌✌

  • 相关阅读:
    C++环形缓冲区设计与实现:从原理到应用的全方位解析
    Linux系统下的文件系统、各文件系统下目录结构及作用
    Selector和Epoll区别
    服务器数据恢复-linux+raid+VMwave ESX数据恢复案例
    Explaining Deepfake Detection by Analysing Image Matching 翻译
    第3章 Linux网络编程 01. 网络结构模式
    sm2多端加密解密,java,js,android,ios实战
    【JavaWeb】之Http协议
    数据结构 C语言 2.1 线性表抽象数据类型 2.2 小议顺序表
    【gcc】RtpTransportControllerSend学习笔记 3:gcc 算法
  • 原文地址:https://blog.csdn.net/m0_65673419/article/details/125577386