临界区(Critical Section) 和 竞态条件(Race Condition)
临界区
临界区是指一段代码,在这段代码中,线程会访问共享资源(如共享变量、文件、数据库连接等),并且必须保证在同一时刻只有一个线程可以执行这段代码。
🔑 简单说:临界区 = 访问共享资源的代码段 + 必须互斥执行
特点
- 共享资源:临界区操作的数据是多个线程共享的。
- 互斥性:必须确保同一时间只有一个线程进入临界区,否则可能导致数据不一致。
- 短小精悍:临界区应尽量小,只包含真正需要同步的代码,以减少性能开销。
竞态条件(Race Condition)
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
竞态条件是指多个线程以不可预测的顺序访问和修改共享资源,导致程序的最终结果依赖于线程调度的时序,从而可能产生错误或不一致的结果。
🔥 本质:“谁先谁后” 决定了结果是否正确。
竞态条件通常发生在:
- 多个线程访问同一个共享资源。
- 至少有一个线程在修改该资源。
- 没有适当的同步机制(如锁)来保护访问。
临界区与竞态条件的关系
如何避免竞态条件?
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
1. 互斥锁(Synchronized)使用 synchronized 关键字保护临界区:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++; // 临界区被锁保护
}
}
2. 使用 ReentrantLock
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
3. 使用原子类(Atomic Classes)
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作,无需显式锁
}
}
Synchronized
Synchronized 的核心作用
- 互斥性(Mutual Exclusion):确保同一时刻只有一个线程可以执行被
synchronized保护的代码。 - 内存可见性(Visibility):释放锁时,会将修改的变量刷新到主内存;获取锁时,会从主内存重新读取变量,保证线程间的数据可见性。
- 防止指令重排序:在 synchronized 块内,JVM 会进行一定的内存屏障处理。
Synchronized 的三种使用方式
1. 修饰实例方法(锁当前对象实例)
public class Counter {
private int count = 0;
// 锁的是 this(当前对象实例)
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
- 锁对象:调用该方法的实例对象(
this)。 - 适用场景:多个线程操作同一个对象实例时,保证其方法的线程安全。
- 注意:如果两个线程操作的是不同实例,则不会互斥。
Counter c1 = new Counter();
Counter c2 = new Counter();
// 线程1 调用 c1.increment()
// 线程2 调用 c2.increment()
// ✅ 不会阻塞,因为锁的是不同对象
2. 修饰静态方法(锁类对象)
public class Counter {
private static int totalCount = 0;
// 锁的是 Counter.class(类对象)
public static synchronized void addToTotal() {
totalCount++;
}
}
3. 修饰代码块(锁指定对象)
public class BankAccount {
private double balance = 0;
private final Object lock = new Object(); // 专用锁对象
public void deposit(double amount) {
synchronized (lock) { // 锁住 lock 对象
balance += amount;
System.out.println("Deposited: " + amount);
}
}
public void withdraw(double amount) {
synchronized (lock) {
if (balance >= amount) {
balance -= amount;
System.out.println("Withdrawn: " + amount);
} else {
System.out.println("Insufficient funds");
}
}
}
}
- 锁对象:括号中指定的任意对象(如
lock、this、BankAccount.class等)。 - 优点:
- 粒度更细,只锁住关键代码段,提升并发性能。
- 可以使用私有锁对象,避免外部代码干扰。
- 推荐用法:优先使用私有锁对象,避免锁定
this或String常量等可能被外部访问的对象。
示例:正确使用 synchronized 避免竞态
public class SafeCounter {
private int count = 0;
// 方式1:同步方法
public synchronized void increment() {
count++; // 原子性保证
}
// 方式2:同步代码块(推荐)
public void incrementBlock() {
synchronized (this) {
count++;
}
}
public synchronized int getCount() {
return count;
}
}
// 测试
SafeCounter counter = new SafeCounter();
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.submit(counter::increment);
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.SECONDS);
System.out.println("Final count: " + counter.getCount()); // 一定是 1000
Synchronized 的底层原理(简化)
- 每个 Java 对象都关联一个监视器锁(Monitor)。
- 当线程进入
synchronized块或方法时,会尝试获取该对象的 Monitor。- 如果 Monitor 空闲,线程获得锁,执行代码。
- 如果 Monitor 已被占用,线程进入
BLOCKED状态,等待锁释放。
- 执行完毕或异常退出时,自动释放锁。
Synchronized 的特性
| 特性 | 说明 |
|---|---|
| 可重入性 | ✅ 同一个线程可以多次获取同一把锁(如递归调用)。 |
| 不可中断 | ❌ 线程在等待 synchronized 锁时,不能被 interrupt() 中断(会一直阻塞)。 |
| 非公平锁 | ✅ synchronized 是非公平的,不保证等待时间最长的线程优先获取锁。 |
| 自动释放 | ✅ 异常或正常退出时,JVM 自动释放锁,不会死锁(编程层面)。 |
常见线程安全类
1. 字符串类
String- 线程安全原因:
String是不可变类(Immutable),一旦创建,其内容不可修改,因此天然线程安全。 - ✅ 所有操作都返回新对象,无状态变化。
- 线程安全原因:
2. 包装类(Wrapper Classes)
Integer,Long,Double,Boolean,Character等- 线程安全原因:同样是不可变类,创建后值不可变。
- ⚠️ 注意:它们的缓存机制(如
Integer.valueOf(127))是线程安全的,但仅适用于 -128 到 127 的范围。
3. 枚举类(Enum)
enum MyEnum { VALUE1, VALUE2 }- 线程安全原因:枚举实例在类加载时由 JVM 创建,且单例、不可变,天生线程安全。
- ✅ 常用于实现单例模式。
4. 不可变集合(Immutable Collections)
- Collections.unmodifiableList/Set/Map() 返回的视图
List.of(),Set.of(),Map.of()(Java 9+)- 线程安全原因:只读,不允许修改,因此线程安全。
- ❌ 一旦创建,不能添加、删除或修改元素。
5. 线程安全的集合类(java.util.concurrent 包)
| 类 | 用途 | 线程安全机制 |
|---|---|---|
ConcurrentHashMap<K,V> | 高并发 HashMap | 分段锁(JDK 7)或 CAS + synchronized(JDK 8+) |
CopyOnWriteArrayList<E> | 读多写少的 List | 写操作复制新数组,读不加锁 |
CopyOnWriteArraySet<E> | 读多写少的 Set | 基于 CopyOnWriteArrayList |
BlockingQueue 实现类 | 线程间数据传递 | |
– ArrayBlockingQueue | 有界阻塞队列 | ReentrantLock |
– LinkedBlockingQueue | 可选有界队列 | 两把锁(读/写分离) |
– PriorityBlockingQueue | 优先级阻塞队列 | ReentrantLock |
– SynchronousQueue | 不存储元素的队列 | 直接传递 |
ConcurrentLinkedQueue<E> | 高性能无界队列 | CAS 操作(无锁) |
ConcurrentLinkedDeque<E> | 双端无锁队列 | CAS |
6. 原子类(java.util.concurrent.atomic 包)
AtomicInteger,AtomicLong,AtomicBooleanAtomicReference<T>,AtomicIntegerArray等- 线程安全原因:基于 CAS(Compare-And-Swap) 操作,提供原子的读-改-写操作。
- ✅ 适用于计数器、状态标志等场景。
7. 日期时间类(Java 8+)
LocalDateTime,ZonedDateTime,Instant,Duration,Period- 线程安全原因:不可变类,所有修改操作返回新实例。
- ❌
java.util.Date和SimpleDateFormat是线程不安全的!
8. 其他线程安全类
StringBuilder❌ 线程不安全StringBuffer✅ 线程安全(方法用synchronized修饰)Random✅ 线程安全(使用 CAS)ThreadLocalRandom✅ 高性能线程安全随机数(推荐用于多线程)java.time.format.DateTimeFormatter✅ 不可变,线程安全
volitle
volatile 是 Java 中的一个关键字,用来修饰成员变量,主要作用是:
保证变量的可见性 和 禁止指令重排序,但 不保证原子性。
volatile的作用:
- ✅ 可见性:一个线程改,其他线程马上知道
- ✅ 禁止重排序:保证执行顺序
- ❌ 不保证原子性
1. 保证可见性
- 多线程下,每个线程有自己的工作内存(缓存),可能看不到其他线程对变量的修改。
volatile变量一旦被修改,会立即写回主内存,并让其他线程的缓存失效,必须重新读主内存。
volatile boolean running = true;
// 线程1
while (running) {
// 运行任务
}
// 线程2
running = false; // 线程1会立刻看到,退出循环
没有 volatile,线程1可能一直用缓存中的 true,无法退出。
2. 禁止指令重排序
- 编译器和 CPU 为了优化性能,可能会调整代码执行顺序。
volatile会插入内存屏障,防止重排序。
典型应用:双重检查单例模式
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // volatile 防止对象创建过程被重排序
}
}
}
return instance;
}
没有 volatile,可能导致其他线程拿到一个还没初始化完的对象。
不保证原子性
volatile只保证“读/写单个变量”是可见的。- 像
i++(读 → 改 → 写)这种复合操作,依然不是线程安全的。 - 要保证原子性,得用
synchronized或AtomicInteger。
currenthashmap
讲讲currenthashmap原理,分别讲了1.7和1.8的原理
✅ JDK 1.7 的实现原理
核心思想:分段锁(Segment)
1. 数据结构
ConcurrentHashMap被分成多个 Segment(段),每个 Segment 相当于一个小型的HashMap。- 每个 Segment 独立加锁,不同 Segment 之间互不影响。
ConcurrentHashMap ├── Segment[0] → HashEntry 数组(带锁) ├── Segment[1] → HashEntry 数组(带锁) ├── ... └── Segment[15]
- 默认有 16 个 Segment,意味着最多支持 16 个线程并发写(不同段)。
2. 加锁机制
- 写操作(put)时,只锁定当前 Segment,其他 Segment 可以并发操作。
- 读操作(get)不加锁,通过
volatile保证可见性。3. put 操作流程
- 计算 key 的 hash 值,确定落在哪个 Segment。
- 获取该 Segment 的独占锁(ReentrantLock)。
- 在 Segment 内部的 HashEntry 数组中插入或更新。
- 释放锁。
4. 优点
- 并发度高(默认16),比
Hashtable全局锁性能好。5. 缺点
- 结构复杂,Segment 固定大小,扩容麻烦。
- 每个 Segment 单独维护,内存占用大。
- 锁还是较重(ReentrantLock)。
✅ JDK 1.8 的实现原理
核心思想:CAS + synchronized + 数组 + 链表 + 红黑树
完全抛弃了 Segment,改用更轻量的方式实现。
1. 数据结构
- 和
HashMap一样:Node 数组 + 链表 + 红黑树- 每个数组元素(桶)是一个链表或红黑树的头节点。
2. 加锁机制
- 不再使用 ReentrantLock。
- 使用 synchronized 锁住链表或红黑树的头节点(桶级锁)。
- 如果桶是空的,使用 CAS 操作插入第一个节点(无锁)。
锁粒度从“段”降到了“桶”,并发性能更高。
3. put 操作流程
- 计算 key 的 hash 值,定位到数组下标。
- 如果该位置为空,用 CAS 插入新节点(无锁)。
- 如果不为空:
- 对该位置的头节点加 synchronized 锁。
- 然后插入链表或红黑树中。
- 如果链表长度 > 8 且数组长度 > 64,转为红黑树。
4. 扩容机制(transfer)
- 支持多线程并发扩容!
- 每个线程负责迁移一部分数据,效率高。
5. size() 实现
- 不再直接统计,而是用 CounterCell 数组 分段计数。
- 类似 LongAdder,避免多线程竞争一个变量。
- 最终求和得到总 size。
✅ 为什么 JDK 1.8 改了?
synchronized在 JVM 层面做了大量优化(偏向锁、轻量级锁),性能大幅提升。- CAS + synchronized 组合更轻量、简洁。
- 红黑树优化长链表查询性能。
- 多线程扩容提升大表性能。
java1.8的currenthashmap还有什么优化,红黑树
Java 8 的
ConcurrentHashMap在性能和并发性上做了多项重要优化,除了 使用synchronized替代ReentrantLock和 放弃 Segment 分段锁 外,还有以下几个关键优化,其中 红黑树 是最核心的之一。JDK 1.8 的主要优化
🔹红黑树 高冲突下提升查询性能(O(n) → O(log n)) 🔹 CAS + synchronized 锁粒度更细,性能更好 🔹 多线程扩容 扩容不卡顿,支持并发迁移 🔹 CounterCell size()高效,无竞争🔹 volatile 读 get()无锁,高性能🔹 扰动函数 减少哈希冲突 ✅ 1. 链表转红黑树(Treeify)—— 核心优化
背景:
- 当多个 key 的 hash 值冲突,会形成链表。
- 链表查找是 O(n),当链表很长时,性能急剧下降。
解决方案:
- 当某个桶(bucket)的链表长度 ≥ 8,并且 Node 数组长度 ≥ 64 时,链表会自动转换为 红黑树。
- 当树中节点数减少到 ≤ 6 时,又会转回链表(避免小树开销大)。
✅ 2. CAS + synchronized 替代 ReentrantLock
- CAS(Compare and Swap):用于无锁插入第一个节点(头节点为空时)。
- synchronized:只锁当前桶的头节点,锁粒度极小。
- JVM 对
synchronized做了深度优化(偏向锁、轻量级锁、自旋锁),性能不输ReentrantLock,甚至更好。✅ 3. 多线程并发扩容(Transfer)
- 当数据量大、负载过高时,需要扩容数组。
- JDK 1.8 支持 多线程一起参与扩容!
- 每个线程负责迁移一部分 bucket 的数据。
- 使用
ForwardingNode标记已迁移的桶,其他线程看到后会跳过或协助迁移。4. CounterCell 分段计数 —— 优化 size()
size()不再遍历整个 map 统计。- 使用一个
CounterCell[]数组,每个线程更新自己的槽位(类似LongAdder)。- 最终把所有槽位加起来得到总数。
✅ 5. volatile + CAS 实现无锁读操作
get()方法完全不加锁。- 通过
volatile保证 Node 节点的val和next的可见性。- 读操作直接遍历链表或红黑树,性能极高。
如果超过8的元素都放在一个桶上,这时候转成了红黑树,我再将元素移除,红黑树会变成链表吗,如果我反复添加移除,且都在一个桶上,红黑树和链表一直互相转换吗,怎么解决
| 问题 | 回答 |
|---|---|
| 删除后红黑树会变链表吗? | ✅ 会,节点 ≤ 6 时自动转回 |
| 会频繁互相转换吗? | ⚠️ 可能,但 Java 用 8 和 6 错开 防止抖动 |
| 如何避免? | ✅ 优化 hash、合理初始化容量、避免 key 冲突 |
2660

被折叠的 条评论
为什么被折叠?



