深入Java多线程进阶:从锁策略到并发工具全解析

带你深入了解更高级的多线程知识,包括各种锁策略、CAS机制、synchronized原理、JUC工具类等核心内容。这些知识是成为Java高级开发者的必经之路,也是面试中经常考察的重点。

1. 常见的锁策略

乐观锁 vs 悲观锁

这是两种截然不同的并发控制思路:

  • 悲观锁 :总是假设最坏情况,每次访问共享资源前都会加锁。比喻是:同学A认为老师很忙,会先发消息确认老师是否有空(加锁操作),得到肯定答复后才去问问题。典型的实现是synchronizedReentrantLock

  • 乐观锁:假设冲突不常发生,访问数据时不加锁,只在更新时检查是否有冲突。比喻是:同学B认为老师很闲,直接去找老师问问题。如果老师确实忙,就下次再来。典型实现是CAS机制。

适用场景

  • 锁竞争激烈时,悲观锁更合适

  • 锁竞争不激烈时,乐观锁效率更高

重量级锁 vs 轻量级锁

  • 重量级锁 :依赖操作系统提供的mutex互斥锁实现,涉及大量内核态/用户态切换,容易引发线程调度,成本较高。

  • 轻量级锁 :尽量在用户态完成加锁操作,减少系统调用开销。synchronized开始是轻量级锁,冲突严重时升级为重量级锁。

内核态 vs 用户态的比喻:在银行窗口外自己办理业务是用户态(效率可控),在窗口内由工作人员办理是内核态(效率不可控)。

自旋锁

传统锁在获取失败时,线程会进入阻塞状态,放弃CPU。自旋锁采用不同策略:

复制代码
// 自旋锁伪代码
while(抢锁(lock) == 失败) {}

特点

  • 优点:不放弃CPU,一旦锁释放能立即获取

  • 缺点:如果锁持有时间长,会持续消耗CPU资源

比喻:追求女神时,死皮赖脸每天问候(自旋锁) vs 陷入沉沦很久后再尝试(挂起等待锁)。

公平锁 vs 非公平锁

  • 公平锁:遵守"先来后到",按请求顺序分配锁

  • 非公平锁:不按顺序,允许插队

synchronized是非公平锁。公平锁需要额外数据结构记录线程顺序,可能降低吞吐量。

可重入锁 vs 不可重入锁

  • 可重入锁:允许同一线程多次获取同一把锁

  • 不可重入锁:不允许,会导致死锁

"把自己锁死"的场景:线程持有锁后再次尝试获取同一把锁,如果是不可重入锁就会死锁。Java中的synchronizedReentrantLock都是可重入锁。

读写锁

针对"读多写少"场景的优化锁:

  • 读锁与读锁:不互斥

  • 写锁与写锁:互斥

  • 读锁与写锁:互斥

Java的ReentrantReadWriteLock实现了读写锁。举例:教务系统中,查看同学列表(读操作)频繁,修改同学列表(写操作)不频繁。

2. CAS(Compare and Swap)

什么是CAS

CAS是一种无锁编程技术,包含三个操作:

  1. 比较内存值V与预期值A

  2. 如果相等,将新值B写入V

  3. 返回操作是否成功

    // CAS伪代码
    boolean CAS(address, expectValue, swapValue) {
    if (&address == expectedValue) {
    &address = swapValue;
    return true;
    }
    return false;
    }

CAS是原子的硬件指令,可视为乐观锁的一种实现。

CAS的应用

1. 实现原子类

复制代码
AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.getAndIncrement();  // 线程安全的i++

2. 实现自旋锁

基于CAS可以实现更灵活的自旋锁。

ABA问题

问题描述

线程t1读取值A,准备改为Z。在此期间,t2将值从A改为B又改回A。t1的CAS操作会成功,但无法感知中间的变化。

解决方案:引入版本号

  • 每次修改版本号+1

  • CAS比较值和版本号

  • Java提供AtomicStampedReference解决此问题

"翻新手机"的比喻:无法区分是全新手机还是翻新后又恢复原样的手机。

3. synchronized原理

synchronized的特性(JDK 1.8)

  1. 开始是乐观锁,冲突频繁时转为悲观锁

  2. 开始是轻量级锁,持有时间长时转为重量级锁

  3. 轻量级锁实现用自旋锁策略

  4. 非公平锁

  5. 可重入锁

  6. 不是读写锁

加锁工作过程

JVM将锁状态分为四级,逐步升级:

1. 偏向锁

  • 第一个加锁线程进入偏向状态

  • 只是做标记,不真正加锁

  • 后续无竞争则避免加锁开销

  • 有竞争时取消偏向,进入轻量级锁

2. 轻量级锁

  • 通过CAS实现

  • 竞争不激烈时使用

  • 自适应自旋:根据情况调整自旋次数

3. 重量级锁

  • 竞争激烈时使用

  • 依赖操作系统mutex

  • 涉及内核态切换,成本高

其他优化

锁消除:JVM检测到不可能存在共享数据竞争时,消除不必要的锁。

锁粗化:将多次连续的加锁解锁合并为一次,减少开销。

4. JUC(java.util.concurrent)常见类

Callable接口

Runnable相比,Callable可以有返回值和抛出异常。

复制代码
Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 1000; i++) {
            sum += i;
        }
        return sum;
    }
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();  // 阻塞等待结果

ReentrantLock

可重入互斥锁,比synchronized更灵活:

复制代码
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // working
} finally {
    lock.unlock();  // 必须手动释放
}

与synchronized的区别

  1. 手动释放锁

  2. 可尝试获取锁(tryLock

  3. 可配置公平锁

  4. 更精确的等待-唤醒机制

原子类

基于CAS实现的高性能原子操作类:

  • AtomicIntegerAtomicLongAtomicBoolean

  • 性能远高于加锁实现

线程池

核心参数理解(文档用"开公司"比喻):

  • corePoolSize:正式员工数(永不辞退)

  • maximumPoolSize:正式员工+临时工数

  • keepAliveTime:临时工空闲时间

  • workQueue:任务队列

  • RejectedExecutionHandler:拒绝策略

四种拒绝策略

  1. AbortPolicy:抛出异常

  2. CallerRunsPolicy:调用者执行

  3. DiscardOldestPolicy:丢弃最老任务

  4. DiscardPolicy:丢弃新任务

信号量(Semaphore)

控制同时访问特定资源的线程数量:

复制代码
Semaphore semaphore = new Semaphore(4);  // 4个可用资源
semaphore.acquire();  // 申请资源(P操作)
// 访问资源
semaphore.release();  // 释放资源(V操作)

比喻:停车场车位展示牌。

CountDownLatch

等待多个任务完成:

复制代码
CountDownLatch latch = new CountDownLatch(10);
// 每个任务完成后调用
latch.countDown();
// 主线程等待
latch.await();

比喻:跑步比赛,所有选手到达终点才公布成绩。

5. 线程安全的集合类

CopyOnWriteArrayList

写时复制的线程安全List:

  • 写操作复制新数组,不影响读操作

  • 读多写少时性能高

  • 读操作不需要加锁

ConcurrentHashMap

线程安全的HashMap,相比Hashtable的优化:

  1. 锁粒度更细:锁每个桶(链表头节点),而非整个表

  2. CAS优化size等属性用CAS更新

  3. 扩容优化:化整为零,多线程协助扩容

  4. 结构优化:链表过长转红黑树(Java 8)

6. 死锁

死锁产生的四个必要条件

  1. 互斥使用:资源不能共享

  2. 不可抢占:不能强制夺取资源

  3. 请求和保持:持有资源同时请求新资源

  4. 循环等待:形成等待环路

如何避免死锁

最实用的是破坏循环等待:按固定顺序获取锁。

复制代码
// 错误:可能死锁
线程1:lock1 -> lock2
线程2:lock2 -> lock1

// 正确:按固定顺序
线程1:lock1 -> lock2
线程2:lock1 -> lock2

"吃饺子需要酱油和醋"的生动比喻说明了死锁场景。

7. 常见面试题解析

最后列出了多个高频面试题:

  1. volatile关键字的用法:保证内存可见性,不保证原子性

  2. Java多线程数据共享:通过堆内存共享数据

  3. 线程状态转换:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED

  4. ConcurrentHashMap的优化:锁分段->锁桶、链表转红黑树、协助扩容

  5. 进程和线程的区别:资源分配 vs 调度单位、内存空间是否共享

总结

Java多线程进阶涉及的知识点既深且广。从基础的锁策略到CAS机制,从synchronized的内部原理到JUC工具类的使用,每一部分都需要深入理解。关键要点:

  1. 理解不同锁策略的适用场景,没有绝对的优劣

  2. 掌握CAS的原理和局限性,特别是ABA问题

  3. 了解synchronized的优化过程,从偏向锁到重量级锁

  4. 熟练使用JUC工具类,根据场景选择合适工具

  5. 重视线程安全问题,使用线程安全集合或手动同步

  6. 避免死锁,按顺序获取锁

多线程编程如同走钢丝,需要在性能和正确性之间找到平衡。希望这篇博客能帮助你在多线程的道路上走得更稳更远。在实践中不断尝试和思考,你会逐渐掌握这门艺术。

相关推荐
数厘2 小时前
2.5可视化工具与 MySQL 连接配置及基础操作
数据库·mysql
阿捞22 小时前
Inertia.js 持久布局实现原理
前端·javascript·html
不会写DN2 小时前
如何在纯前端中通过手势交互来控制星球的转动
前端·交互
沃尔威武2 小时前
性能调优实战:从火焰图定位到SQL优化的全流程
android·数据库·sql
apcipot_rain2 小时前
Python实战——蒙特卡洛模拟分析杀牌游戏技能收益
python·游戏·数学建模
liliangcsdn2 小时前
sentence-transformer如何离线加载和使用模型
开发语言·前端·php
老绿光2 小时前
Python 字典完全指南:从入门到实战
linux·服务器·python
是小蟹呀^2 小时前
【总结】LangChain中如何维持记忆
python·langchain·memory
蓝色的杯子2 小时前
OpenClaw一文详细了解-手搓OpenClaw-4 Tool Runtime
人工智能·python