JAVA SE 多线程(下)

06-02 1147阅读

文章目录

    • 📕1. 常见的锁策略
        • ✏️1.1 乐观锁VS悲观锁
        • ✏️1.2 轻量级锁VS重量级锁
        • ✏️1.3 自旋锁
        • ✏️1.4 公平锁VS非公平锁
        • ✏️1.5 可重入锁和不可重入锁
        • ✏️1.6 读写锁
        • 📕2. 死锁
            • ✏️2.1 哲学家就餐问题
            • ✏️2.2 形成死锁的必要条件
            • ✏️2.3 如何避免死锁
            • 📕3. JUC(java.util.concurrent) 的常见类
                • ✏️3.1 Callable 接口
                • ✏️3.2 ReentrantLock
                • ✏️3.3 原子类
                • ✏️3.4 信号量 Semaphore
                • ✏️3.5 CountDownLatch
                • 📕4. synchronized 原理
                    • ✏️4.1 加锁工作过程
                    • ✏️4.2 其他的优化操作
                    • 📕5. CAS
                        • ✏️5.1 CAS的ABA问题

                          📕1. 常见的锁策略

                          注意 : 以下所介绍的锁策略, 不仅仅局限于Java这一种语言 , 这些性质通常也是给锁的实现者参考的.当然我们普通人也是可以了解一下的 , 这或许对使用锁也有帮助.

                          ✏️1.1 乐观锁VS悲观锁

                          悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

                          乐观锁: 假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

                          🌰举个栗子: 同学 A 和 同学 B 想请教老师一个问题.

                          同学 A 认为 “老师是比较忙的, 我来问问题, 老师不一定有空解答”. 因此同学 A 会先给老师发消息: “老师你忙嘛? 我下午两点能来找你问个问题嘛?” (相当于加锁操作) 得到肯定的答复之后, 才会真的来问问题.如果得到了否定的答复, 那就等一段时间, 下次再来和老师确定时间. 这个是悲观锁.

                          同学 B 认为 “老师是比较闲的, 我来问问题, 老师大概率是有空解答的”. 因此同学 B 直接就来找老师.(没加锁, 直接访问资源) 如果老师确实比较闲, 那么直接问题就解决了. 如果老师这会确实很忙, 那么同学 B也不会打扰老师, 就下次再来(虽然没加锁, 但是能识别出数据访问冲突). 这个是乐观锁.

                          Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略.

                          就好比同学 C 开始认为 “老师比较闲的”, 问问题都会直接去找老师.但是直接来找两次老师之后, 发现老师都挺忙的, 于是下次再来问问题, 就先发个消息问问老师忙不忙,再决定是否来问问题.

                          ✏️1.2 轻量级锁VS重量级锁

                          轻量级锁: 当一个线程尝试获取某个对象的锁时,如果该对象没有被其他线程锁定,则当前线程会将对象头中的Mark Word设置为指向当前线程栈帧的一个指针,这个过程称为“偏向锁”。如果多个线程同时竞争同一个锁,那么JVM会升级锁的状态,从偏向锁升级到轻量级锁。此时,每个线程都会尝试使用CAS操作来获取锁,如果成功则获得锁并进入临界区;如果失败,则自旋等待一段时间后再次尝试。

                          特定:

                          1. 减少了操作系统上下文切换的开销。
                          2. 在线程间竞争不激烈的情况下表现良好。
                          3. 如果竞争过于激烈,可能会导致频繁的自旋,浪费CPU资源。

                          重量级锁: 传统的Java锁机制,如synchronized关键字所实现的锁,通常被称为重量级锁。当一个线程获取了某个对象的锁后,其他试图获取同一对象锁的线程会被阻塞,直到第一个线程释放锁为止。被阻塞的线程将进入等待队列,由操作系统负责管理这些线程的调度。

                          特点:

                          1. 线程阻塞和唤醒的代价较高。
                          2. 更适用于线程竞争激烈的场景,因为它可以避免CPU空转浪费资源。
                          3. 相比轻量级锁,重量级锁的实现更加简单直接。
                          ✏️1.3 自旋锁

                          按之前的方式,线程在抢锁失败后进⼊阻塞状态,放弃 CPU,需要过很久才能再次被调度 . 但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题 , 如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止.

                          理解⾃旋锁 vs 挂起等待锁
                          想象⼀下, 去追求⼀个⼥神. 当男⽣向⼥神表⽩后, ⼥神说: 你是个好⼈, 但是我有男朋友了~~
                          挂起等待锁: 陷⼊沉沦不能⾃拔.... 过了很久很久之后, 突然⼥神发来消息, '咱俩要不试试?' (注意, 这
                          个很⻓的时间间隔⾥, ⼥神可能已经换了好⼏个男朋友了).
                          ⾃旋锁: 死⽪赖脸坚韧不拔. 仍然每天持续的和⼥神说早安晚安. ⼀旦⼥神和上⼀任分⼿, 那么就能⽴刻
                          抓住机会上位.
                          

                          优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.

                          缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是不消耗 CPU 的).

                          ✏️1.4 公平锁VS非公平锁

                          假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后 C也尝试获取锁, C 也获取失败, 也阻塞等待. 当线程 A 释放锁的时候, 会发生啥呢?

                          公平锁 : 遵守 “先来后到”. B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.

                          非公平锁 : 不遵守 “先来后到”. B 和 C 都有可能获取到锁

                          这就好⽐⼀群男⽣追同⼀个⼥神. 当⼥神和前任分⼿之后, 先来追⼥神的男⽣上位, 这就是公平锁; 
                          如果是⼥神不按先后顺序挑⼀个⾃⼰看的顺眼的, 就是⾮公平锁.
                          

                          JAVA SE 多线程(下)

                          公平锁

                          JAVA SE 多线程(下)

                          非公平锁

                          JAVA SE 多线程(下)

                          💡💡💡注意 :

                          1. 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.
                          2. 公平锁和非公平锁没有好坏之分, 关键还是看适用场景.
                          3. synchronized 是非公平锁
                          ✏️1.5 可重入锁和不可重入锁

                          可重入锁: 简单来说就是一个线程如果抢占到了互斥锁资源,在锁释放之前再去该竞争同一把锁的时候,不需要等待,只需要记录重入次数。在多线程并发编程里面,绝大部分锁都是可重入的,比如Synchronized、ReentrantLock等,但是也有不支持重入的锁,比如JDK8里面提供的读写锁StampedLock。锁的可重入性,主要解决的问题是避免线程死锁的问题。

                          ✏️1.6 读写锁

                          多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同⼀个锁,就会产生极大的性能损耗。所以读写锁因此而产生。

                          读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。

                          ⼀个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.

                          两个线程都只是读⼀个数据, 此时并没有线程安全问题. 直接并发的读取即可.

                          两个线程都要写⼀个数据, 有线程安全问题.

                          ⼀个线程读另外⼀个线程写, 也有线程安全问题

                          读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁

                          ReentrantReadWriteLock.ReadLock 类表示⼀个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.

                          ReentrantReadWriteLock.WriteLock 类表示⼀个写锁. 这个对象也提供了 lock / unlock方法进行加锁解锁.

                          其中,

                          读加锁和读加锁之间, 不互斥.

                          写加锁和写加锁之间, 互斥.

                          读加锁和写加锁之间, 互斥

                          读写锁特别适合于 “频繁读, 不频繁写” 的场景中.

                          📕2. 死锁

                          什么是死锁?

                          死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

                          🌰举个栗子理解死锁 :

                          当我和我对象(当然现在还没有😭)⼀起去饺子馆吃饺子时 , 吃饺子需要酱油和醋.

                          我拿起了酱油瓶, 我对象拿起了醋瓶.

                          我 : 你先把醋瓶给我, 我用完了就把酱油瓶给你.

                          我对象 : 你先把酱油瓶给我, 我用完了就把醋瓶给你.

                          如果我们俩彼此之间互不相让, 就构成了死锁.

                          酱油和醋相当于是两把锁, 我们两个人就是两个线程.

                          ✏️2.1 哲学家就餐问题

                          有个桌子 , 围着一圈哲学家 , 桌子中间放着一盘意大利面 . 每个哲学家两两之间, 放着一根筷子.

                          JAVA SE 多线程(下)

                          每个哲学家只做两件事 : 思考人生或者吃面条. 思考人生的时候就会放下筷子. 吃⾯条就会拿起左右两边的筷子(先拿起左边, 再拿起右边).

                          JAVA SE 多线程(下)

                          如果哲学家发现筷子拿不起来了 , 就会阻塞等待

                          JAVA SE 多线程(下)

                          关键点 : 如果5位哲学家同时拿起左手边的筷子时 , 然后再尝试拿右手的筷子, 就会发现右手的筷子都被占用了. 由于哲学家们互不相让, 这个时候就形成了死锁

                          JAVA SE 多线程(下)

                          死锁是一种严重的 BUG!! 导致一个程序的线程 “卡死”, 无法正常工作!

                          ✏️2.2 形成死锁的必要条件

                          死锁产生的四个必要条件:

                          1. 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

                          2. 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

                          3. 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

                          4. 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

                          当上述四个条件都成⽴的时候,便形成死锁。当然,死锁的情况下如果打破上述任何⼀个条件,便可让死锁消失。

                          ✏️2.3 如何避免死锁

                          破坏循环等待

                          最常用的一种死锁阻止技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号 (1, 2, 3…M).

                          N 个线程尝试获取锁的时候, 都按照固定的按编号由小到⼤顺序来获取锁. 这样就可以避免环路等待

                          可能产生循环等待死锁的代码

                          //产生环路等待不是100%发生的,这只是概率问题,哲学家就餐产生死锁也是概率问题
                          public class Test {
                              private static Object locker1 = new Object();
                              private static Object lockre2 = new Object();
                              public static void main(String[] args) {
                                  Thread t1 = new Thread(() -> {
                                      synchronized (locker1) {
                                          synchronized (lockre2) {
                                              System.out.println("this is thread t1");
                                          }
                                      }
                                  });
                                  Thread t2 = new Thread(() -> {
                                      synchronized (lockre2) {
                                          synchronized (locker1) {
                                              System.out.println("this is thread t2");
                                          }
                                      }
                                  });
                                  t1.start();
                                  t2.start();
                              }
                          }
                          

                          不会产生循环等待的代码

                          public class Test {
                              private static Object locker1 = new Object();
                              private static Object lockre2 = new Object();
                              public static void main(String[] args) {
                                  Thread t1 = new Thread(() -> {
                                      synchronized (locker1) {
                                          synchronized (lockre2) {
                                              System.out.println("this is thread t1");
                                          }
                                      }
                                  });
                                  Thread t2 = new Thread(() -> {
                                      synchronized (locker1) {
                                          synchronized (lockre2) {
                                              System.out.println("this is thread t2");
                                          }
                                      }
                                  });
                                  t1.start();
                                  t2.start();
                              }
                          }
                          

                          📕3. JUC(java.util.concurrent) 的常见类

                          ✏️3.1 Callable 接口

                          Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便我们借助多线程的方式计算结果

                          🌰代码示例: 创建线程计算 1 + 2 + 3 + … + 1000, 使用 Callable 版本

                          1. 创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
                          2. 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
                          3. 把 callable 实例使用 FutureTask 包装一下
                          4. 创建线程, 线程的构造方法传入FutureTask . 此时新线程就会执行FutureTask 内部的 Callable 的call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
                          5. 在主线程中调用futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
                          import java.util.concurrent.Callable;
                          import java.util.concurrent.ExecutionException;
                          import java.util.concurrent.FutureTask;
                          public class Test {
                              public static void main(String[] args) throws ExecutionException, InterruptedException {
                                  Callable callable = new Callable() {
                                      @Override
                                      public Integer call() throws Exception {
                                          int result = 0;
                                          for (int i = 0; i 
                                              result+=i;
                                          }
                                          return result;
                                      }
                                  };
                                  FutureTask
                              public static void main(String[] args) {
                                  Semaphore semaphore = new Semaphore(4);
                                  Runnable runnable = new Runnable() {
                                      @Override
                                      public void run() {
                                          System.out.println("apply");
                                          try {
                                              semaphore.acquire();
                                              System.out.println("accussful");
                                              Thread.sleep(10000);
                                          } catch (InterruptedException e) {
                                              e.printStackTrace();
                                          }
                                          semaphore.release();
                                          System.out.println("release");
                                      }
                                  };
                                  for (int i = 0; i 
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码