[Java EE] 多线程编程进阶

Java EE\] 多线程初阶请看下面文章 [https://blog.csdn.net/Boop_wu/article/details/155076865?fromshare=blogdetail\&sharetype=blogdetail\&sharerId=155076865\&sharerefer=PC\&sharesource=Boop_wu\&sharefrom=from_link![](https://csdnimg.cn/release/blog_editor_html/release2.4.4/ckeditor/plugins/CsdnLink/icons/icon-default.png)https://blog.csdn.net/Boop_wu/article/details/155076865?fromshare=blogdetail\&sharetype=blogdetail\&sharerId=155076865\&sharerefer=PC\&sharesource=Boop_wu\&sharefrom=from_link](https://blog.csdn.net/Boop_wu/article/details/155076865?fromshare=blogdetail&sharetype=blogdetail&sharerId=155076865&sharerefer=PC&sharesource=Boop_wu&sharefrom=from_link "https://blog.csdn.net/Boop_wu/article/details/155076865?fromshare=blogdetail&sharetype=blogdetail&sharerId=155076865&sharerefer=PC&sharesource=Boop_wu&sharefrom=from_link") > 本文介绍了Java多线程编程中的核心概念与技术,主要包括: > > 1. 常见锁策略:包括乐观锁/悲观锁、重量级锁/轻量级锁、自旋锁/挂起等待锁等,以及synchronized的自适应锁机制 > 2. synchronized原理:详细解析其锁升级过程(无锁→偏向锁→轻量级锁→重量级锁)和优化策略(锁消除、锁粗化) > 3. CAS机制:深入讲解比较交换操作的实现原理、ABA问题及解决方案 > 4. JUC工具类:包括Callable接口、ReentrantLock、原子类、信号量等并发工具的使用 > 5. 线程安全集合:对比分析各种线程安全集合的实现原理和适用场景 > > 文章系统性地讲解了Java并发编程的核心技术,帮助开发者深入理解多线程编程的底层机制和最佳实践 ## 一.常见的锁策略 ### 1.乐观锁和悲观锁 描述的是加锁时遇到的场景 **悲观锁** : 加锁的时候 , 预测接下来的锁竞争会非常激烈 , 就需要针对这种情况进行一些额外的工作 **乐观锁** : 加锁的时候 , 预测接下来的锁竞争情况不激烈 , 就不需要额外的工作 **注意** : Synchronized 初始使用乐观锁策略 , 但发现竞争比较**频繁**的时候 , 就会自动切换成悲观锁策略 ### 2.重量级锁和轻量级锁 遇到加锁之后的解决方案 ![](https://i-blog.csdnimg.cn/img_convert/27351f5b65ccd3461acc3d423c197f92.png) **重量级锁**: 加锁机制重度依赖于 OS 的 mutex ; 会有大量的内核态用户切换 , 容易引发线程的调度(一般是用来应对悲观场景下的锁 , 需要付出更多的代价) **轻量级锁** : 加锁机制尽量不再使用 mutex , 而是在用户态代码完成 , 实在搞不定了 , 再使用 mutex ; 会有少量的内核态用户切换 , 不太容易引发线程调度 **用户态和内核态** : 是操作系统为权限隔离 , 保障安全而划分的两种 CPU 运行级别, 核心区别在于对硬件/系统资源的访问权限 , 所有进程运行都会在这两种状态之间切换 **注意** : synchronized 开始是一个轻量级锁 , 如果锁冲突比较**严重** , 就会变成重量级锁 ### 3.挂起等待锁和自旋锁 **挂起等待锁** : 获取锁失败后 , 线程放弃 CPU资源 并进入阻塞队列挂起 , 只有锁被释放后才被唤醒(重量级锁的典型实现) **自旋锁** : 获取锁失败后 , 线程不放弃 CPU 并 进入自旋(循环)检测锁是否被释放(轻量级锁的典型实现) (自旋锁的特点是不阻塞线程 , 而是通过循环消耗 CPU 资源来等待锁释放 , 适用于锁竞争不激烈 , 持有时间短的场景) **注意** : synchronized 中的轻量级锁策略大概率是通过自旋锁的方式实现的 ### 总结 : 悲观锁=\>重量级锁=\>挂起等待锁 乐观锁=\>轻量级锁=\>自旋锁 synchronzied 针对上述的锁策略是**自适应的** ### 4.互斥锁和读写锁 **互斥锁** : 任意时刻仅允许一个线程访问临界区(读/写均互斥) **读写锁** : ① 读锁 : 多线程可同时持有(读并发) ; ② 写锁 : 仅一个线程持有(写互斥) ; 可以提高效率 , 减少互斥的机会 > Java 标准库中提供了 ReentrantReadWriteLock 类 , 实现了读写锁 > > * ReenTrantReadWriteLock.ReadLock 类表示一个**读锁** , 这个对象提供了 lock/unlock 方法 > * ReenTrantReadWriteLock.WriteLock 类表示一个**写锁**, 实现了lock/unlock 方法 > * 其中 : ① 读加锁和读加锁之间 , 不互斥 ; ② 写加锁和写加锁之间 , 互斥 ; ③ 读加锁和写加锁之间 , 互斥 **注意** : synchronized 不是读写锁 ### 5.可重入锁和不可重入锁 **可重入锁** : 允许同一线程多次获取同一把锁(递归锁) ; 内部机制 : 维护 \[ 线程归属标记+锁计数器 \] , 解锁需要计数器归 0 **不可重入锁** : 同一线程持有锁时 , 再次请求锁会阻塞/死锁 > 在 Java 中只要以 Reentrant 开头命名的锁都是可重入锁 , 而且 JDK 提高的所有现成的 Lock 实现类 , 包括 synchronized 关键字锁都是可重入的 ; 而 Linux 系统中提供的 mutex 时不可重入的 **注意** : synchronized 是可重入锁 ### 6.公平锁和非公平锁(针对插队现象的公平和非公平) **公平锁** : 按照线程等待的**先后顺序获取锁** , 先等待的线程先执行 **非公平锁**: 线程请求锁时直接尝试抢锁 , 抢不到再进入等待队列(机会均等) > Java 标准库中 > > 公平锁 : ReentrantLock(**true**) , > > 非公平锁 : ReentrantLock(false)/synchronized **注意** : synchronized 是非公平锁 > 操作系统内部的线程调度就可以视为随机的 , 如果不做任何额外的限制 , 锁就是非公平锁 , 如果想要实现公平锁 , 就需要依赖额外的数据结构 , 来记录线程的先后顺序 ## 二.synchronized 原理 ### 1.核心特性 可重入 , 非公平 , 隐式加锁/解锁(JVM 自动处理) ![](https://i-blog.csdnimg.cn/img_convert/70269b6b0e5ea764ee5c7a16703257e1.png) ### 2.锁升级 无锁 =\> 偏向锁 =\> 轻量级锁=\> 重量级锁 **注意** : 锁升级是单项的(只能从低到高) , 无法降级(不会从重量级到轻量级) #### ① 无锁 此时没有锁竞争 , 无需加锁 #### ② 偏向锁(Biased Lock) -- 单线程无竞争场景 **目标** : 消除单线程重复加锁的开销(仅第一次加锁有少量开销) * **加锁逻辑**: ![](https://i-blog.csdnimg.cn/img_convert/64fd4080e080c0800a4e8e62d34a2159.png) * **解锁逻辑**: ![](https://i-blog.csdnimg.cn/img_convert/1529bf98f5888ed79d03aafb3c079196.png) #### ③ 轻量级锁(Lightweight Lock) -- 多线程交替竞争场景 **目标** : 用 CAS 自旋代替内核态阻塞 , 减少上下文切换开销 **触发条件**: 有多个线程竞争偏向锁 , JVM 撤销偏向锁 , 升级为轻量级锁 * **加锁逻辑**: ![](https://i-blog.csdnimg.cn/img_convert/a0e4a8576c23b1e41f7776ad3fbc7c36.png) * **解锁逻辑**: ![](https://i-blog.csdnimg.cn/img_convert/2c6fe650ec901e203edc85542b2c57b2.png) #### ④ 重量级锁(Heavuweight Lock) -- 多线程持续竞争场景 **目标**: 通过操作系统内核态的监视器锁保证互斥 , 牺牲性能换取稳定性 **触发条件**: 轻量级锁自旋次数达到阈值 , 或多个线程同时自旋 , JVM 升级为重量级锁 * **加锁逻辑**: ![](https://i-blog.csdnimg.cn/img_convert/d723d3fe7fb0df762d8c825a2c3dcb24.png) * **监视器锁** : ![](https://i-blog.csdnimg.cn/img_convert/524d3ede9b2edc57ada89ca6b151ae9c.png) ### 3.其他锁优化 #### 1.锁消除(Lock Elimination) - 移除"无竞争的锁" > JVM 的 JIT 编译器通过逃逸分析判断 : 若锁对象是线程私有的 , 则该线程不存在线程并发竞争 , JIT 会在编译阶段自动移除该锁的加锁操作 **代码示例**: ##### ① 局部变量作为锁对象 ```java public void lockEliminationDemo() { // 锁对象:局部变量 lockObj,仅当前线程可见(无逃逸) Object lockObj = new Object(); synchronized (lockObj) { // JIT 会消除此锁 System.out.println("无竞争的同步代码块"); } } ``` **优化后** : ```java public void lockEliminationDemo() { Object lockObj = new Object(); System.out.println("无竞争的同步代码块"); // 锁被消除 } ``` ##### ②JDK 内置类的隐式锁消除 > `StringBuffer` 的 `append()` 方法是同步方法(加了 `synchronized`),但如果 `StringBuffer` 是局部变量(无逃逸),JIT 会消除锁: ```java public String stringBufferDemo() { // sb 是局部变量,无逃逸 StringBuffer sb = new StringBuffer(); sb.append("Java"); // 同步方法,锁被消除 sb.append("Lock"); // 同步方法,锁被消除 return sb.toString(); } ``` > `sb` 仅在当前方法内使用,无其他线程竞争,JIT 消除 `append()` 方法的 `synchronized` 锁,性能等同于 `StringBuilder` #### 2. 锁粗化(Lock Coarsening) - 合并细粒度锁 > 当 JIT 检测到同一个锁对象被频繁,连续地加锁解锁 (如循环内加锁 , 连续调用同步方法) , 会见多次加解锁操作合并为一次 , 减少锁操作对底层的开销 **代码示例**: ##### ① 循环内细粒度的锁 ```java 'public class demo38 { private static int count = 0; public static void main(String[] args) { Object locker = new Object(); Thread t1 = new Thread(()->{ for (int i = 0; i < 5000; i++) { synchronized (locker){ count++; }synchronized (locker){ count++; }synchronized (locker){ count++; } } }); } } ``` 优化后 ```java public class demo38 { private static int count = 0; public static void main(String[] args) { Object locker = new Object(); Thread t1 = new Thread(()->{ for (int i = 0; i < 5000; i++) { synchronized (locker){ count++; count++; count++; } } }); } } ``` > **注意** : JIT 不会无限制的粗化锁 : 若合并后的锁持有时间过长 , 可能导致其他线程长期阻塞 , JIT 会平衡 加解锁次数 和 持有锁时间 , 仅对短时间内连续的锁操作进行粗优化 |--------|------------------|----------------------------| | **维度** | **synchronized** | **ReentrantLock** | | 锁类型 | 隐式锁(JVM 自动管理) | 显式锁(手动 lock ()/unlock ()) | | 公平性 | 仅非公平锁 | 可配置公平 / 非公平锁 | | 锁升级 | 自动(无锁→偏向→轻量→重量) | 无锁升级,默认非公平,底层依赖 CAS+AQS | | 功能扩展 | 无(仅基础加锁 / 解锁) | 支持可中断锁、超时锁、条件变量(Condition) | | | | | ## 三.CAS(Compara And Swap) > CAS (比较和交换)是 Java 实现无锁并发编程的核心底层机制 , 属于乐观锁的实现 , 通过 CPU 原子指令保证操作的原子性 , 无需传统锁(如 synchronized)的阻塞/唤醒开销 ### 1.核心操作 > * 一个 CAS 涉及到的操作 : 若内存地址 V 的值 == 预期值 A , 则将 V 更新为 B , 返回 true ; 否则不操作 , 返回 false > * 硬件层面的 CPU 原子指令 , JVM 通过 Unsafe 类调用底层指令 , 保证"比较 - 交换" , 不可中断 ; 当多个线程同时对某个资源进行 CAS 操作时 , 只能有一个线程操作成功 , 但是并不会阻塞其他线程 , 其他线程只会收到操作失败的信号 1. check and set 2. read and update #### 一个伪代码 **参数 : 内存地址(要操作变量的内存地址) , 预期值(变量的旧值) , 新值** ```java booleanCAS(address, expectValue, swapValue) { if (&address == expectedValue) { &address = swapValue; return true; } return false; } ``` ### 2.应用 #### ① 是java.util.concurrent.atomic 包 ```java import java.util.concurrent.atomic.AtomicInteger; public class demo39 { private static AtomicInteger count = new AtomicInteger(0);//赋初始值为0 public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ for (int i = 0; i < 5000; i++) { count.getAndIncrement();//count++; //count.incrementAndGet();//++count; //count.addAndGet(4);//count+=4; } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 5000; i++) { count.getAndIncrement();//count++; //count.incrementAndGet();//++count; //count.addAndGet(4);//count+=4; } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count = " +count); } } ``` ![](https://i-blog.csdnimg.cn/img_convert/e213d3d305e0dfd54c480153ce95dfc9.png) #### ② 基于 CAS 实现自旋锁 伪代码 ```java public class SpinLock { private Thread owner = null; public void lock(){ // 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就⾃旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){ } } public void unlock (){ this.owner = null; } } ``` 若 owner 为 null(锁未被持有) , 则将其设为当前线程 ; 若锁已经被其他线程持有 , 则通过 while 循环自旋等待 ### 3.ABA 问题 #### ① 问题描述 : 线程 1 操作是 \[初始值A =\> 经过修改操作值为B\] ; 线程 2 的操作是 \[数是指为 A =\> 修改为 C =\> 又退回 A\] ; 此时线程 1 执行 CAS 时 , 检测到变量仍然为 A , 误以为未被修改 , 会再次将其更新为 B #### ② 问题核心 : **CAS 仅校验最终值 , 忽略中间修改** #### ③ 解决方案 : **引入版本号**, 在 CAS 比较数据当前值和旧值的同时 , 也要比较版本号是否符合预期 当真正修改的时候 , 如果当前版本号 与 读到的版本号相同 , 则修改数据 , 并把版本号+1 , 如果当前版本号高于读到的版本号 , 就操作失败(认为数据已经被修改过了) ### 4.仅支持单个变量原子操作 问题 #### ① 问题描述 : CAS 只能保证单个变量操作的原子性 , 无法直接实现多变量的原子更新(如同时更新 a 和 b) #### ② 解决方案 : ##### **合并变量**: 将多个变量合并为一个对象 , (通过 AtomicReference 操作对象引用); ```java import java.util.concurrent.atomic.AtomicReference; class MulNum{ int a; int b; public MulNum(int a,int b){ this.a = a; this.b = b; } } public class demo40 { private static AtomicReference count = new AtomicReference<>(new MulNum(1,2)); public static void main(String[] args) { for (int i = 0; i < 5; i++) { Thread t1 = new Thread(()->{ while(true){ MulNum oldVar = count.get(); MulNum newVar = new MulNum(oldVar.a + 1, oldVar.b + 1); if(count.compareAndSet(oldVar,newVar)){ System.out.println("更新后:a=" + newVar.a + ", b=" + newVar.b); break; } } }); t1.start(); } } } ``` ##### **实现互斥锁**(ReentrantLock 为例) ```java import java.util.concurrent.locks.ReentrantLock; public class demo41 { private int a = 1; private int b = 2; private ReentrantLock lock = new ReentrantLock(); public void updateMultiVar() { lock.lock(); try { a++; b++; System.out.println("更新后:a=" + a + ", b=" + b); } finally { lock.unlock(); } } public static void main(String[] args) { demo41 demo = new demo41(); for (int i = 0; i < 5; i++) { new Thread(demo::updateMultiVar).start(); } } } ``` ## 四.JUC(java.util.concurrent) 常见类 Java 并发编程中的 JUC **是处理高并发场景的核心工具包** , 解决了原生 synchronized,wait/notify 等机制的局限性 , 包含了线程池 , 锁 , 原子锁 , 并发集合 , 同步器等核心组件 ### 1. 原生并发编程的痛点: * `Runnable` 无返回值,无法抛出受检异常 * `synchronized` 是隐式锁,无法中断、无法超时、只有非公平锁 * `Vector`/`Hashtable` 等同步集合性能差 * 手动管理线程(创建 / 销毁)开销大等 **JUC 的核心目标 :** **更灵活的同步控制、更高的并发性能、更简洁的异步编程** ### 2.Callable 接口 Callable 是 JUC 包下的函数式接口 , **核心作用** : 定义有返回值 , 可抛异常的线程 ; 弥补了 Runnable 接口的不足 , 是多线程异常执行并获取结果的核心工具 #### 示例:创建线程计算1+2+3+...+1000 #### ① 如果不使用 callable: ```java public class demo43 { private static volatile int result = 0; public static void main(String[] args) { Object locker = new Object(); Thread t1 = new Thread(()->{ synchronized (locker){ for (int i = 1; i < 100; i++) { result+=i; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } locker.notify(); } }); Thread t2 = new Thread(()->{ synchronized (locker) { try { while (result == 0) { locker.wait(); } System.out.println("result = " + result); } catch (InterruptedException e) { Thread.currentThread().interrupt();//恢复线程中断状态 e.printStackTrace(); } } }); t1.start(); t2.start(); } } ``` 或 ```java public class Demo { // 封装结果和锁对象的内部类 static class Result { public int sum = 0; public Object lock = new Object(); } public static void main(String[] args) throws InterruptedException { Result result = new Result(); // 线程t:计算1-1000的累加和 Thread t = new Thread() { @Override public void run() { int sum = 0; for (int i = 1; i <= 1000; i++) { sum += i; } // 必须在同步块内操作锁和结果 synchronized (result.lock) { result.sum = sum; result.lock.notify(); // 持有锁时调用notify } } }; t.start(); // 主线程:等待计算完成,打印结果 synchronized (result.lock) { // 循环判断(防虚假唤醒)+ 修正"==" while (result.sum == 0) { result.lock.wait(); } System.out.println("1-1000的累加和:" + result.sum); // 输出500500 } } } ``` 显然上述的代码操作复杂 , 容易出错 #### ② 使用 Callable 的版本(优化) * 创建一个匿名内部类,实现 Callable 接口.Callable 带有泛型参数。**泛型参数表示返回值的类型** * **重写 Callable 的 call 方法**,完成累加的过程。直接通过返回值返回计算结果 * **把 callable 实例使用 FutureTask 包装一下** * 创建线程,线程的构造方法传入 FutureTask. 此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法,完成计算。计算结果就放到了 FutureTask 对象中 * 在主线程中调用 **futureTask.get ()** 能够阻塞等待新线程计算完毕。并获取到 FutureTask 中的结果 ```java import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class demo42 { public static void main(String[] args) throws ExecutionException, InterruptedException { //1.此处Callable只是定义了一个带有返回值的任务 , 并没有在执行 //执行还是需要搭配Thread对象 Callable callable = new Callable() { @Override public Integer call() throws Exception { int result = 0; for(int i = 1;i<100;i++){ result+=i; } return result; } }; //2.封装为FutureTask FutureTask futureTask = new FutureTask<>(callable); //3.创建线程 Thread t = new Thread(futureTask); //4.启动线程 t.start(); System.out.println(futureTask.get());//此处只有get执行完毕,才能拿到返回结果,可能会陷入阻塞 } } ``` ### 3.ReentrantLock **可重入互斥锁**, 和 synchronized 定位类似 , 都是用来实现互斥效果的 , 保证线程安全 #### ①常用方法 |----------------------------------------------|------------------------------------------------------------------------------------------------------------| | void lock() | **加锁**;①若锁空闲,立即获取锁,锁计数器+1 ; ②若锁被占用,当时线程阻塞,直到获取锁 | | void unlock() | **解锁;①** 锁计数器-1 ; ②计数器归0,释放锁,唤醒等待队列线程**(必须在 finally 中调用)** | | void lockInterruptibly() | **可中断锁**;与 lock()逻辑一致 ; 等待锁是,线程若被中断,抛 InterruptedException(中断线程后需要恢复)(Thread.currentThread().interrupt();) | | boolean tryLock() | **尝试加锁**;①锁空闲-\>获取锁,返回true ; ② 锁被占用-\>立即返回 false | | boolean tryLock(long timeout, TimeUnit unit) | **超时加锁**;①在指定时间内尝试获取锁; ②超时 / 被中断→返回 false | #### 示例 : ```java import java.util.concurrent.locks.ReentrantLock; public class Demo44 { private static int count = 0; public static void main(String[] args) { // 开启公平锁(true),默认非公平锁(false) ReentrantLock locker = new ReentrantLock(true); Thread t1 = new Thread(() -> { // 简化Lambda写法 for (int i = 0; i < 50000; i++) { locker.lock(); // 获取锁(阻塞直到拿到锁) try { count++; // 临界区:原子操作 } finally { locker.unlock(); // 必须放finally,确保锁释放 } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { locker.lock(); try { // 修复:t2的unlock移到finally count++; } finally { locker.unlock(); } } }); t1.start(); t2.start(); try { // 等待t1、t2执行完毕,避免主线程提前打印结果 t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); // 中断后恢复中断状态(规范做法) Thread.currentThread().interrupt(); } System.out.println("Final count: " + count); // 预期输出 100000 } } ``` **注意** : 1. ReentrantLock locker = new ReentrantLock(true);**是一个公平锁的实例** ; 可以 保证先得到的先执行,每个线程最终都能获取锁 , **不会出现饥饿问题** ; **但是** 公平锁需要维护等待队列的有序性 , 即使临时锁空闲 , 也不会让请求的线程"插队" , 必须唤醒队列的头部线程 , 导致上下文切换增多 , **最终导致性能损耗大** 2. **locker.lock();放在 try{}外面** ; 会有风险如果 `lock()` 执行时抛出异常(比如 `lockInterruptibly()` 被中断、或 JVM 异常),`lock()` 并未成功获取锁,但 `finally` 块会**无条件执行** **unlock()**; 此时当前线程未持有锁,调用 `unlock()` 会直接抛出 `IllegalMonitorStateException`,导致程序崩溃 > #### ② 总结创建线程的写法 : > > 1. 继承 Thread 类(单独定义类/匿名内部类) > 2. 实现 Runnable 接口(定义单独类/匿名内部类) > 3. lambda > 4. 实现 Callable 接口(单独定义类/匿名内部类) > 5. 线程池 ThreadFactory > #### ③ReentrantLock 和 synchronized 的区别 > > 1. synchronized 是一个关键字 , 是**JVM 内部实现的** (基于 C++) ; ReentrantLock 是标准库的一个类 , **在 JVM 外部实现**(基于 Java) > 2. synchronized 使用时不需要手动释放锁 ; ReentrantLock 使用时需要**手动释放** , 使用起来更灵活 , 但也更容易遗漏 unlock() > 3. synchronized 在申请锁失败时 , 会死等 ; ReentrantLock 可以通过 **tryLock** 的方式等待一段时间就放弃 > 4. synchronized 是非公平锁 ; ReentrantLock 默认是非公平锁 , 可以通过构造方法传入一个 **true 开启公平锁** (ReentrantLock locker = new ReentrantLock(true);) > 5. synchronized 是通过 Object 的 wait/notify 来实现等待-唤醒 , 每次唤醒的是一个随机等待的线程 ; ReentrantLock 搭配 **Condition 类实现等待-唤醒** , 可以精准控制唤醒某个指定的线程 > 6. synchronized 可以不支持查询锁状态 ; ReentrantLock 支持`isLocked()`/`isHeldByCurrentThread()`等查询锁状态 > 7. synchronized 不可中断锁(获取锁时线程会一直阻塞) ; ReentrantLock 可中断锁(lockInterruptibly()) > 8. 都是可重入锁 ##### Condition 类示例 : ```java ReentrantLock lock = new ReentrantLock(); Condition conditionA = lock.newCondition(); // 条件A Condition conditionB = lock.newCondition(); // 条件B // 线程1等待条件A lock.lock(); try { conditionA.await(); } finally { lock.unlock(); } // 线程2唤醒等待条件A的线程 lock.lock(); try { conditionA.signal(); // 仅唤醒等待条件A的线程 } finally { lock.unlock(); } ``` ##### lockInterruptibly() 可中断锁示例: ```java Thread t = new Thread(() -> { try { lock.lockInterruptibly(); // 可中断获取锁 } catch (InterruptedException e) { System.out.println("线程被中断,放弃获取锁"); return; } try { // 临界区 } finally { lock.unlock(); } }); t.start(); t.interrupt(); // 中断线程,触发 InterruptedException ``` ### 4.原子类 在前面 CAS 中提到过原子类 , 此处结合上一篇来讲 原子类是java.util.concurrent.atomic 包下的一组工具类 , 基于 CAS 机制实现无锁化的线程安全操作 , 避免锁竞争和上下文切换 #### ① 原子类分类 |----------|-------------------------------------------------------|-----------------------------| | **类别** | **核心类** | **用途** | | 基本类型原子类 | AtomicInteger、AtomicLong、AtomicBoolean | 原子更新int / long / boolean 类型 | | 引用类型原子类 | AtomicReference、AtomicStampedReference | 原子更新对象引用(解决 ABA 问题) | | 数组类型原子类 | AtomicIntegerArray、AtomicLongArray | 原子更新数组中的元素 | | 字段更新器原子类 | AtomicIntegerFieldUpdater、AtomicReferenceFieldUpdater | 原子更新对象的非静态字段(需 volatile 修饰) | #### ② 基本类型原子类 **此处以 AtomicInterger 为例** |----------------------------------------|------------------------| | **方法** | **功能** | | `get()` | 获取当前值 | | `set(int newValue)` | 设置新值(非原子,仅赋值) | | `getAndSet(int newValue)` | 原子设置新值,返回旧值 | | `compareAndSet(int expect,int update)` | CAS 更新:预期值匹配则更新,返回是否成功 | | `getAndIncrement()` | 原子自增(i++),返回旧值 | | `incrementAndGet()` | 原子自增(++i),返回新值 | | `getAndAdd(int delta)` | 原子累加 delta,返回旧值 | | `addAndGet(int delta)` | 原子累加 delta,返回新值 | ##### 示例 : ```java import java.util.concurrent.atomic.AtomicInteger; public class demo45 { public static void main(String[] args) throws InterruptedException { AtomicInteger count = new AtomicInteger(0); for(int i = 0;i<10;i++){ Thread t1 = new Thread(()->{ for (int j = 0; j < 1000; j++) { count.incrementAndGet();//count++; } }); t1.start(); } Thread.sleep(1000); System.out.println("count = "+count); } } ``` #### ③ 引用类型原子类 * `AtomicReference`:原子更新对象引用(如自定义对象) * `AtomicStampedReference`:带版本号的原子引用,解决 CAS 的 ABA 问题 * `AtomicMarkableReference`:带标记的原子引用(标记值为 boolean,简化版版本号) ##### 示例 : ```java import java.util.concurrent.atomic.AtomicStampedReference; public class demo46 { public static void main(String[] args) { // 线程1:将 A→B→A(模拟 ABA 场景) AtomicStampedReference ars = new AtomicStampedReference<>("A",1); Thread t1 = new Thread(()->{ int stamp = ars.getStamp();//获取版本号 ars.compareAndSet("A","B",stamp,stamp+1); ars.compareAndSet("A","B",stamp+1,stamp+2); }); t1.start(); // 线程2:CAS 更新,需匹配引用+版本号 Thread t2 = new Thread(()->{ int stamp = ars.getStamp(); // 初始版本号 1 // 版本号不匹配(实际已变为 2),更新失败 boolean success = ars.compareAndSet("A", "C", stamp, stamp + 1); System.out.println("更新是否成功:" + success); // false System.out.println("当前值:" + ars.getReference()); // A System.out.println("当前版本号:" + ars.getStamp()); // 2 }); t2.start(); } } ``` #### ④ 数组原子类 `AtomicIntegerArray`/`AtomicLongArray`/`AtomicReferenceArray`:原子更新数组中的元素(数组本身是普通数组,仅元素操作原子化) ##### 示例 : ```java import java.util.concurrent.atomic.AtomicIntegerArray; public class demo47 { public static void main(String[] args) { int[] arr = {1, 2, 3}; AtomicIntegerArray atomicArr = new AtomicIntegerArray(arr); // 原子更新索引 1 的元素(2→20) atomicArr.compareAndSet(1, 2, 20); System.out.println(atomicArr.get(1)); // 20 // 原子自增索引 0 的元素(1→2) atomicArr.getAndIncrement(0); System.out.println(atomicArr.get(0)); // 2 } } ``` #### ⑤ 字段更新器原子类 `AtomicIntegerFieldUpdater`/`AtomicLongFieldUpdater`/`AtomicReferenceFieldUpdater`:原子更新**对象的非静态字段**(需满足 2 个条件): * 字段必须用 `volatile` 修饰(保证可见性) * 字段的访问权限:若更新器在外部类,字段需为 `public`/`protected`,或通过反射突破访问限制 ##### 示例 : ```java import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; public class demo48 { public static void main(String[] args) { class User { // 必须用 volatile 修饰 volatile int age; public User(int age) { this.age = age; } } // 创建字段更新器(参数:类、字段名) AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age"); User user = new User(18); // 原子自增 age(18→19) updater.getAndIncrement(user); System.out.println(user.age); // 19 // CAS 更新 age(19→20) updater.compareAndSet(user, 19, 20); System.out.println(user.age); // 20 } } ``` ### 5.Semaphone 信号量 Java 中的 Semaphone 是 juc 包下的并发工具类 , 核心作用 : **控制同时访问某个资源的线程数量** , 本质是 **通过维护一个"许可(permits)"计数器实现限流 / 资源管控 , 是实现"优先资源共享" 的经典方案** > #### 核心逻辑 : > > Semaphone 是基于 AQS(抽象队列同步器)实现的 > > 1. 初始化时指定许可数(允许同时访问的线程数) > 2. 线程通过 acquire()获取许可 : 许可次数\>0 则-1,且线程继续执行 ; 许可数 = 0 则线程阻塞,直到其他线程释放许可 > 3. 线程通过 release()释放许可 : 许可数+1 , 唤醒等待队列中的线程 #### 示例 : ```java import java.util.concurrent.Semaphore; public class demo49 { public static void main(String[] args) throws InterruptedException { Semaphore semaphore = new Semaphore(3); semaphore.acquire(); System.out.println("进行一次 P 操作"); semaphore.acquire(); System.out.println("进行一次 P 操作"); semaphore.acquire(); System.out.println("进行一次 P 操作"); semaphore.acquire(); System.out.println("进行一次 P 操作"); } } ``` **应用** : 当初始值为 1 的信号量(二元信号量) , 等价于锁(不可重入锁) ```java import java.util.concurrent.Semaphore; public class demo50 { private static int count = 0; public static void main(String[] args) throws InterruptedException { Semaphore semaphore = new Semaphore(1); Thread t1 = new Thread(()->{ for (int i = 0; i < 10000; i++) { try { semaphore.acquire(); count++; semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { try{ semaphore.acquire(); count++; semaphore.release(); }catch (InterruptedException e){ e.printStackTrace(); } } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count = "+count); } } ``` ### 6.CountDownLatch 是 JUC 的同步辅助工具类 , 核心作用 : 让一组线程等待其他线程完成指定操作后 , 再继续执行 > > #### 核心原理 : > > 1. 初始化计数器 : 创建 CountDownLatch 时指定计数器初始值 > 2. 线程等待 : 调用 await()的线程会阻塞 , 直到计数器值减为 0; > 3. 计数器递减 : 完成任务的线程调用 countDown() , 将计数器值减为 1; > 4. 放弃等待线程 : 但计数器值减为 0 时 , 所有调用 await()的线程被唤醒 , 继续执行 **注意** : 计数器只能递减 , 无法重置(一旦减为 0 , 后续调用 countDown()无效果 , await()也会立即返回) #### 示例 : ```java import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class demo57 { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(10); ExecutorService executor = Executors.newFixedThreadPool(4); for (int i = 0; i < 10; i++) { int id = i; executor.submit(() -> { System.out.println("子任务开始执行: " + id); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("子任务结束执行: " + id); latch.countDown(); }); } latch.await();//这个方法阻塞等待所有的任务结束 System.out.println("所有线程执行完毕"); executor.shutdown(); /*** * ①创建计数为 10 的 CountDownLatch,表示需要等待 10 个子任务完成; * ②用 4 核心的线程池批量提交 10 个子任务,每个任务执行 1 秒后调用 countDown() 递减计数器; * ③主线程调用 latch.await() 阻塞,直到 10 个任务全部完成(计数器归 0); * ④最后输出提示并关闭线程池。 */ } } ``` ## 五.Java 中线程安全的集合类 线程安全的集合类 , 其核心解决普通集合(如 ArrayList/HashMap)在并发读写时的数据错乱 , ConcurrentMidificationException , 此循环等问题 ### 1.线程安全集合的核心分类 |--------------|---------------------------------|------------------------------------------------------------------|-------------------------------| | **分类** | **实现原理** | **代表类** | **核心特点** | | **同步包装类** | `Collections.synchronizedXxx()` | `synchronizedList`/`synchronizedMap` | 全局加锁(`synchronized`),简单但并发性能低 | | **JUC 并发集合** | 分段锁 / CAS / 写时复制 / 阻塞队列 | `ConcurrentHashMap`/`CopyOnWriteArrayList`/`LinkedBlockingQueue` | 精细化锁 / 无锁设计,高并发下性能更优 | ### 2.多线程下使用 ArrayList #### ① 自己使用同步机制(synchronized 或 ReentrantLock) 此处不再说 #### ② 通过 Collection 工具类为普通集合套上 \[ 全局锁

本质是对集合所有方法加 synchronized

java 复制代码
List<String> syncList = Collections.synchronizedList(new ArrayList<>());

注意 : 迭代时需要手动加锁 , 否则抛异常(ConcurrentMidificationException)

java 复制代码
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        //多线程下的ArraryList
        List<String> syncList = Collections.synchronizedList(new ArrayList<>());
        syncList.add("Hello");
        syncList.add("World");
        new Thread(new Runnable() {
            @Override
            public void run() {
                syncList.add("Java");
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                syncList.add("Thread");
            }
        }).start();
        Thread.sleep(1000);
        synchronized (syncList) {//必须锁定集合对象本身
            for (String s : syncList) {
                System.out.println(s);
            }

        }

    }
}

③ 使用 JUC 包 , CopyOnWriteArrayList

核心实现 :

  • 写操作 : (add/remove/set) 复制一份新的数组 , 在数组上修改 , 修改完成后替换原数组 , 全程加锁
  • 读操作 : (get/iterator) 直接读原数组 , 无锁 , 性能极高

常见问题 :

问题 1 : 写操作性能地 , 内存开销大

解决方案 : 仅用于都铎写少的场景 ; 当频繁场景时改用 ReentrantLock 手动加锁的普通 List

问题 2 : 迭代过程中其他线程的修改不会反映到迭代器中

解决方案 : 需要加锁 ; 或者每次迭代前重新获取集合

3.多线程使用队列

① ArrayBlockingQueue

基于数组实现的阻塞队列

② LinkedBlockingQueue

基于链表实现的阻塞队列

③ PriorityBlockingQueue

基于堆实现的带优先级的阻塞队列

④ TransferQueue

最多只包含一个元素的阻塞队列

4.多线程使用哈希表

在多线程下使用哈希表(键值对储存) , 核心问题是解决线程安全和并发性能--普通 HashMap 线程不安全 , 并发读写会导致数据错乱 , 死循环 , ConcurrentModificationException 等问题

① 线程安全哈希表

|---------|--------------------------------------------|------------------------------------------|
| 类型 | 代表类 | 实现原理 |
| 全局锁哈希表 | Hashtable/ Collections.synchronizedMap | 所有方法加 synchronized;全局锁 |
| 精细化锁哈希表 | ConcurrentHashMap | JDK1.7:分段锁;JDK1.8:CAS + 局部synchronized |
| 写时复制哈希表 | ConcurrentSkipListMap(有序) | 跳表 + CAS,无锁设计 |

②Hashtable(低效)

只是简单的把关键的方法加上了 synchronized

这相当于直接对 Hashtable 对象本身加锁

  • 如果多线程访问同一个 Hashtable 就会直接造成锁冲突
  • 一旦触发扩容 , 就由该线程完成整个扩容过程

注意 : Hashtable 是 JDK1.0 的老旧类 , 性能差 , 已被ConcurrentHashMap 完全替代 , 仅作为了解

②ConcurrentHashMap(首选)

核心原理: 抛弃了 JDK1.7 的分段锁(把这些链表分成几组 , 每个组安排一个锁) , 改用更细粒度的锁机制 (锁桶)

  • 读操作 : 无锁 , 通过 volatile 保证数据可见性
  • 写操作 : 1)对空桶 : CAS 原子操作写入 , 无锁 ; 2) 对非空桶 : 对桶首届点加 synchronized 锁 , 仅阻塞当前桶的读写 , 其他桶可并发
  • 当桶元素超过 8 个转为红黑树 , 提升查找性能
java 复制代码
private static final Map<String, Integer> map = new ConcurrentHashMap<>();
扩容机制 : 化整为零
  • 发现扩容的线程只创建新数组,搬几个元素 : 1) 触发扩容的线程先标记 , 并且创建新数组(容量翻倍) ; 2) 按步长 (16)拆分原数组 , 该线程仅迁移自己负责的一小段桶(目的:避免的那现场一次迁移所有元素导致长时间阻塞)
  • 扩容期间新老数组同时存在 : 1)新数组作为全局变量 , 扩容全程与老数组共存 ; 2)迁移完成的桶做标记 , 未迁移的桶仍在老数组中(保证连续性)
  • 后续线程搬家 , 各搬一小部分 : 1)任何线程执行 put/remove 时 , 若检测到扩容中 , 会先暂停自身操作 , 协助迁移 ; 2)每个线程仅迁移自己"认领的桶段"(避免重复)
  • 搬完最后一个元素删除老数组 : 1)所有桶迁移完成 , 主线程将老数组替换 成新数组 , 并删除 (释放内存); 2)更新扩容阈值 , 并标记扩容完成
  • 插入只往新数组加 : 扩容期间 , put 操作先检查桶是否已经迁移 , 若已迁移 : 直接写入新数组 , 若未迁移 : 先协助迁移 , 再写入新数组(防止老数组数据冗余)
  • 查找需同时查找新老数组 : get 操作 , 先查老数组 , 若桶已迁移完(标记) , 则跳转到行数则查询 ; 若桶未迁移 : 直接查询老数组 ; 若新老数组都查不到 , 再返回 null(保证扩容期间读取数据不丢失 , 不遗漏)
相关推荐
w***37511 小时前
SpringBoot【实用篇】- 测试
java·spring boot·后端
深瞳智检1 小时前
学习应用 第001期-Windows 10 用 CMD 安装 MySQL 全流程解析(免安装版)
数据库·windows·mysql·压缩包·环境安装
q***61411 小时前
Java实战:Spring Boot实现WebSocket实时通信
java·spring boot·websocket
k***82511 小时前
Ubuntu介绍、与centos的区别、基于VMware安装Ubuntu Server 22.04、配置远程连接、安装jdk+Tomcat
java·ubuntu·centos
正在走向自律1 小时前
金仓数据库在发电行业的创新应用与实战案例
数据库·国产数据库·电力·kingbasees·电科金仓
华纳云IDC服务商1 小时前
MySQL数据库如何防止SQL注入攻击
数据库·sql·mysql
2301_815686452 小时前
extern
java·开发语言
q***56382 小时前
Java进阶-SPI机制
java·开发语言
合作小小程序员小小店2 小时前
桌面开发,在线%物品代送,代接管理%系统,基于vs2022,c#,winform,sql server数据。
开发语言·数据库·sql·microsoft·c#