Java核心基础七:锁机制和并发工具类

一、锁

锁的定义

锁(Lock) 是多线程编程中用于控制对共享资源访问的同步机制,确保同一时刻只有一个或特定数量的线程能访问资源,避免数据不一致或竞态条件(Race Condition)。


锁的作用

  1. 互斥(Mutual Exclusion):防止多个线程同时修改共享资源。

  2. 可见性(Visibility):确保线程对共享资源的修改对其他线程立即可见。

  3. 有序性(Ordering):保证代码执行顺序符合预期(如通过锁同步代码块顺序执行)。


锁的分类

1. 按实现方式
  • 内置锁(隐式锁)

    • 如 synchronized 关键字,JVM 自动管理加锁/释放。

  • 显式锁

    • 如 ReentrantLock,需手动调用 lock() 和 unlock()

2. 按特性
  • 可重入锁

    • 同一线程可重复获取同一把锁(如 synchronizedReentrantLock)。

  • 公平锁/非公平锁

    • 公平锁按线程等待顺序分配锁(ReentrantLock(true));非公平锁允许插队(默认)。

3. 按共享策略
  • 独占锁(排他锁)

    • 一次仅一个线程持有锁(如 synchronizedReentrantLock)。

  • 共享锁

    • 允许多个线程同时持有锁(如读锁 ReadWriteLock.ReadLock)。

4. 按锁的优化思想
  • 悲观锁

    • 假定并发冲突高,访问资源前先加锁(如 synchronizedReentrantLock)。

  • 乐观锁

    • 假定冲突少,先操作再验证(如 StampedLock 的乐观读、CAS 原子类)。

5. 其他特殊锁
  • 读写锁(ReadWriteLock

    • 分离读锁(共享)和写锁(独占),适合读多写少场景。

  • 自旋锁(Spin Lock)

    • 线程通过循环(自旋)尝试获取锁,避免上下文切换(Java 中 Atomic 类基于 CAS 实现类似机制)。

  • 分段锁

    • 将资源分段加锁(如 ConcurrentHashMap 分段锁机制)。


总结

锁的核心是解决多线程并发访问的安全性问题,通过不同策略(互斥、共享、乐观/悲观)平衡性能与数据一致性。合理选择锁类型(如读多写少用读写锁,低竞争用乐观锁)能显著提升并发效率。


二、锁机制

1. synchronized 关键字

  • 作用:Java内置的互斥锁,用于方法或代码块,确保同一时间只有一个线程执行该代码。

  • 特点

    • 自动加锁/释放:进入同步块时获取锁,退出时释放(包括异常退出)。

    • 可重入性:同一线程可重复获取同一锁。

    • 非公平锁:默认不保证等待线程的获取顺序。

  • 示例

public synchronized void method() { ... }
// 或
synchronized (obj) { ... }

2. ReentrantLock

  • 作用:显式锁,提供更灵活的锁控制。

  • 特点

    • 可重入性:与synchronized类似。

    • 公平性选项:支持公平锁(按等待顺序获取)或非公平锁。

    • 锁中断:通过lockInterruptibly()支持中断等待。

    • 尝试锁tryLock()尝试非阻塞获取锁,或定时等待。

  • 示例

ReentrantLock lock = new ReentrantLock();
lock.lock();
try { ... } finally { lock.unlock(); }

3. ReadWriteLockReentrantReadWriteLock

  • 作用:读写分离锁,允许多个读线程并发,写线程独占。

  • 特点

    • 读锁共享:多个线程可同时持有读锁。

    • 写锁独占:写锁获取时,所有读/写锁均被阻塞。

  • 示例

ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();  // 读操作
rwLock.writeLock().lock(); // 写操作

4. StampedLock(Java 8+)

  • 作用:更高效的读写锁,支持乐观读。

  • 特点

    • 乐观读:尝试读取时不加锁,通过验证戳记(stamp)确认数据一致性。

    • 三种模式:写锁、悲观读锁、乐观读。

  • 示例

StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead();
if (!lock.validate(stamp)) {
    stamp = lock.readLock(); // 升级为悲观读
}

5. 锁对比

  • synchronized vs ReentrantLock

    • synchronized简单但功能有限;ReentrantLock更灵活,支持高级特性。

    • ReentrantLock需手动释放锁,避免死锁。

  • 读写锁选择:读多写少用ReadWriteLock;低竞争且读多写少用StampedLock


三、并发工具类

1. CountDownLatch

  • 作用:计数器,等待指定数量的线程完成操作。

  • 特点:一次性使用,计数器归零后无法重置。

  • 示例

CountDownLatch latch = new CountDownLatch(3);
// 子线程中:latch.countDown();
latch.await(); // 主线程等待

2. CyclicBarrier

  • 作用:线程到达屏障后等待,直到所有线程就绪后继续。

  • 特点:可重复使用,支持屏障后的回调任务。

  • 示例

CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All threads arrived"));
barrier.await(); // 每个线程调用

3. Semaphore(信号量)

  • 作用:控制同时访问资源的线程数量(限流)。

  • 特点:支持公平/非公平模式,可释放多个许可。

  • 示例

Semaphore semaphore = new Semaphore(5); // 允许5个线程并发
semaphore.acquire(); // 获取许可
semaphore.release();

4. Phaser

  • 作用:分阶段的多线程任务协调,动态调整参与的线程数。

  • 特点:替代CyclicBarrierCountDownLatch,更灵活。

  • 示例

Phaser phaser = new Phaser(3); // 初始3个线程
phaser.arriveAndAwaitAdvance(); // 到达并等待

5. Exchanger

  • 作用:两个线程间交换数据。

  • 示例

Exchanger exchanger = new Exchanger<>();
String data = exchanger.exchange("data"); // 阻塞直到另一个线程调用

6. 并发集合

  • ConcurrentHashMap:线程安全的哈希表(Java 8+使用CAS和synchronized优化)。

  • CopyOnWriteArrayList:写时复制列表,读操作无锁。

  • 阻塞队列:如ArrayBlockingQueueLinkedBlockingQueue,支持生产者-消费者模型。

7. 线程池与CompletableFuture

  • 线程池:通过Executors创建(如newFixedThreadPool),底层为ThreadPoolExecutor

  • CompletableFuture:异步编程,支持链式调用和组合多个任务。


四、总结与适用场景

  • 锁选择:简单场景用synchronized,需要高级功能用ReentrantLock,读多写少用读写锁。

  • 工具类

    • 等待多个任务完成 → CountDownLatch。例如服务启动检查:等待所有服务(数据库、缓存、日志)初始化完成再启动主服务。

    • 多阶段任务协调 → CyclicBarrierPhaser。例如并行计算:多线程处理数据,所有线程完成第一阶段计算后汇总结果。或多关卡游戏加载:每个关卡需加载资源,所有玩家加载完成后进入下一关。

    • 限流 → Semaphore。例如数据库连接池:限制同时获取连接的线程数。

    • 数据交换 → Exchanger。例如流水线处理:一个线程生产数据,另一个线程消费并返回结果。

  • 并发集合:高并发读写时选择ConcurrentHashMapCopyOnWriteArrayList

你可能感兴趣的:(Java开发,java,开发语言)