java线程-并发编程

临界区(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 的核心作用

  1. 互斥性(Mutual Exclusion):确保同一时刻只有一个线程可以执行被 synchronized 保护的代码。
  2. 内存可见性(Visibility):释放锁时,会将修改的变量刷新到主内存;获取锁时,会从主内存重新读取变量,保证线程间的数据可见性。
  3. 防止指令重排序:在 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");
            }
        }
    }
}
  • 锁对象:括号中指定的任意对象(如 lockthisBankAccount.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)
  • IntegerLongDoubleBooleanCharacter 等
    • 线程安全原因:同样是不可变类,创建后值不可变。
    • ⚠️ 注意:它们的缓存机制(如 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 包)
  • AtomicIntegerAtomicLongAtomicBoolean
  • AtomicReference<T>AtomicIntegerArray 等
    • 线程安全原因:基于 CAS(Compare-And-Swap) 操作,提供原子的读-改-写操作。
    • ✅ 适用于计数器、状态标志等场景。
7. 日期时间类(Java 8+)
  • LocalDateTimeZonedDateTimeInstantDurationPeriod
    • 线程安全原因不可变类,所有修改操作返回新实例。
    • ❌ 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 操作流程

  1. 计算 key 的 hash 值,确定落在哪个 Segment。
  2. 获取该 Segment 的独占锁(ReentrantLock)。
  3. 在 Segment 内部的 HashEntry 数组中插入或更新。
  4. 释放锁。

4. 优点

  • 并发度高(默认16),比 Hashtable 全局锁性能好。

5. 缺点

  • 结构复杂,Segment 固定大小,扩容麻烦。
  • 每个 Segment 单独维护,内存占用大。
  • 锁还是较重(ReentrantLock)。

✅ JDK 1.8 的实现原理

核心思想:CAS + synchronized + 数组 + 链表 + 红黑树

完全抛弃了 Segment,改用更轻量的方式实现。

1. 数据结构

  • 和 HashMap 一样:Node 数组 + 链表 + 红黑树
  • 每个数组元素(桶)是一个链表或红黑树的头节点。

2. 加锁机制

  • 不再使用 ReentrantLock
  • 使用 synchronized 锁住链表或红黑树的头节点(桶级锁)。
  • 如果桶是空的,使用 CAS 操作插入第一个节点(无锁)。

锁粒度从“段”降到了“桶”,并发性能更高。

3. put 操作流程

  1. 计算 key 的 hash 值,定位到数组下标。
  2. 如果该位置为空,用 CAS 插入新节点(无锁)。
  3. 如果不为空:
    • 对该位置的头节点加 synchronized 锁
    • 然后插入链表或红黑树中。
  4. 如果链表长度 > 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锁粒度更细,性能更好
🔹 多线程扩容扩容不卡顿,支持并发迁移
🔹 CounterCellsize() 高效,无竞争
🔹 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 冲突


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值