难道你还想靠“加个 synchronized 就完事儿”来写高性能并发吗?
开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
前言 😅
前言——说真的,并发这玩意儿最“阴险”的地方在于:它能跑不代表它正确;它“看起来没问题”不代表线上不会在凌晨 3 点把你薅起来。写并发代码像在黑屋子里摆多米诺骨牌:你以为摆稳了,结果风一吹(线程调度一变、CPU 核数一换、负载一上来),哗啦啦全倒。
这篇就按你给的大纲,把 Java 并发的高级知识(锁 / 无锁 / Fork-Join / 并发容器 / 原子类 / 常见坑)串起来讲透,配两段实战代码:ForkJoin 并行归并排序、StampedLock 优化读多写少。我会尽量讲“人话”,但不牺牲专业度;也会顺手吐槽两句(毕竟谁还没被死锁教育过呢🙃)。
目录
- 并发设计的“地图”:锁、无锁、任务并行、并发容器到底在解决什么
synchronizedvsLock/Condition:差异、适用场景、常见误区- 并行计算模型:
ExecutorService、ForkJoinPool、并行流(Parallel Stream) - 非阻塞算法与 CAS:
AtomicX、Unsafe/VarHandle视角下的“原子性” - 并发集合与性能考量:
ConcurrentHashMap等容器的“快”从哪来 - 实战 1:Fork/Join 实现并行归并排序(可直接跑)
- 实战 2:StampedLock 优化“读多写少”(乐观读 + 回退)
- 常见陷阱:ABA、锁饥饿、死锁、错误的内存可见性假设
- 延伸阅读与学习路线
1) 并发设计的“地图”
高性能并发设计,通常逃不开四类武器:
- 锁(Blocking):用互斥来换正确性(
synchronized、ReentrantLock、StampedLock)。
优点:好理解;缺点:竞争激烈时吞吐掉得快,还可能饥饿/死锁。 - 无锁(Non-blocking):用 CAS/原子变量减少阻塞(
AtomicInteger、LongAdder、CAS 循环)。
优点:高竞争下更能扛;缺点:更难写对,还会遇到 ABA、活锁、自旋浪费 CPU。 - 任务并行模型:把问题切成任务调度(
ExecutorService、ForkJoinPool、并行流)。
优点:结构化并行、容易扩展;缺点:任务划分不合理会适得其反(分太细:调度开销爆炸;分太粗:吃不满 CPU)。 - 并发容器:把“并发控制”封装进数据结构(
ConcurrentHashMap、CopyOnWriteArrayList)。
优点:少踩坑;缺点:误用也会很惨(例如在高写入下用 COW)。
一个很实用的判断法:
先问瓶颈是 CPU 还是锁竞争/等待。
CPU 密集:更关心 Fork/Join、任务切分、避免共享状态。
IO 密集:更关心线程池策略、队列、背压、超时、可取消。
2) synchronized vs Lock / Condition
2.1 synchronized 的特点(别小看它)
- 语法级支持,JVM 能做很多优化(偏向锁、轻量级锁、锁消除、锁粗化等)。
- 自动释放锁:异常也能安全退出(这点对“手滑忘记 unlock”的人类很友好😮💨)。
- 配合
wait/notify/notifyAll做条件等待(但可读性一般、易误用)。
适合:临界区小、竞争不激烈、逻辑简单的场景。
2.2 ReentrantLock 的优势:更“可控”
Lock 体系给你更多控制权:
tryLock():拿不到锁就算了(避免死等)tryLock(timeout):超时返回(更符合工程化)lockInterruptibly():支持中断响应(线程能“被救回来”)- 公平锁可选(
new ReentrantLock(true)),减少长期饥饿,但吞吐可能下降 Condition:一个锁可以有多个条件队列(比wait/notify清晰)
直觉理解:
synchronized像“自动挡”,ReentrantLock像“手动挡”。手动挡更强,但也更容易把离合踩坏🙂。
2.3 Condition vs Object.wait/notify
wait/notify绑定在 对象监视器 上,每个对象一个等待队列,表达多个条件时很乱。Condition是Lock的“等待队列”,可以一个锁配多个条件:notEmpty、notFull这种场景写起来舒服很多。
3) 并行计算:Executor / ForkJoin / 并行流
3.1 ExecutorService:工程里最常见的“线程管理入口”
你通常会用:
newFixedThreadPool(n):固定线程数,适合稳定负载newCachedThreadPool():线程可扩张(小心失控)newSingleThreadExecutor():单线程串行化- 更推荐:
ThreadPoolExecutor自定义队列、拒绝策略、线程命名等
关键点:线程池不是“越大越好”。
- CPU 密集:线程数 ≈ CPU 核数(或略多一点点)
- IO 密集:线程数可以更大,但要有超时和背压,否则就是“把机器拖死的艺术”。
3.2 Fork/Join:递归拆分 + 工作窃取(Work-Stealing)
ForkJoinPool 适合:
- 可递归拆分的 CPU 密集任务:排序、分治、图像处理、Map-Reduce 风格计算
- 工作窃取:线程忙完自己的 deque,会去“偷”别人的任务,提高利用率
核心抽象:
RecursiveTask<V>:有返回值RecursiveAction:无返回值
3.3 并行流(Parallel Stream):甜,也可能齁
并行流底层默认用 ForkJoinPool.commonPool()。优点是写法爽,但坑也不少:
- 共享 commonPool:你项目里其他地方也可能在用,互相影响
- 不适合 IO 密集(阻塞会拖慢整个 commonPool)
- 任务粒度不好控制,调试困难
经验建议:性能敏感或线上服务,优先显式 Executor/ForkJoinPool;并行流更适合离线批处理、一次性任务。
4) 非阻塞算法与 CAS:AtomicX 到底“原子”在哪
4.1 CAS 是什么(非常“直男”的比较方式)
CAS:Compare-And-Set
我以为变量还是旧值 old?如果是,就改成 new;如果不是,说明别人动过,我重试。
伪代码:
do {
old = value;
new = f(old);
} while (!CAS(value, old, new));
4.2 AtomicInteger 示例:自增为什么比 i++ 安全
i++ 是读-改-写三步,可能被打断;AtomicInteger.incrementAndGet() 内部是 CAS 循环,保证原子更新。
4.3 注意:CAS 不等于“没有问题”
- 高竞争下 CAS 会自旋重试,CPU 可能飙高
- 可能出现 ABA 问题(后面专门讲)
- 对复合操作仍需设计:比如“检查再更新”的逻辑,可能需要更高级的原子结构或锁
5) 并发集合与性能考量
5.1 ConcurrentHashMap:别再用 Hashtable 了(求你了😅)
现代 ConcurrentHashMap 的思路大体是:
- 降低锁粒度(不是整张表一把大锁)
- 在合适场景使用 CAS、链表/红黑树转换等
使用建议:
- 高并发计数:不要用
map.compute(k, ...)搞得锁竞争很重,优先考虑LongAdder或computeIfAbsent+LongAdder组合 - 迭代是弱一致性的:遍历过程中可能看到更新(但不会抛
ConcurrentModificationException)
5.2 其它容器的“脾气”
CopyOnWriteArrayList:读极快、写极慢(写时复制整个数组)。适合读多写少且元素不大的场景。BlockingQueue(如ArrayBlockingQueue/LinkedBlockingQueue):生产者-消费者模型利器,能天然做背压。
6) 实战 1:ForkJoin 并行归并排序(可运行)
下面是一个并行归并排序示例:数组足够大时可以并行拆分;小数组走串行排序减少调度开销。
import java.util.Arrays;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
public class ParallelMergeSort {
// 经验阈值:太小并行反而慢(任务拆分/合并有开销)
private static final int THRESHOLD = 1 << 13; // 8192,可按机器调
public static void sort(int[] arr) {
ForkJoinPool pool = ForkJoinPool.commonPool();
int[] tmp = new int[arr.length];
pool.invoke(new MergeSortTask(arr, tmp, 0, arr.length));
}
static class MergeSortTask extends RecursiveAction {
private final int[] arr;
private final int[] tmp;
private final int left; // inclusive
private final int right; // exclusive
MergeSortTask(int[] arr, int[] tmp, int left, int right) {
this.arr = arr;
this.tmp = tmp;
this.left = left;
this.right = right;
}
@Override
protected void compute() {
int size = right - left;
if (size <= 1) return;
// 小任务走串行,避免Fork/Join开销
if (size <= THRESHOLD) {
Arrays.sort(arr, left, right);
return;
}
int mid = left + (size / 2);
MergeSortTask t1 = new MergeSortTask(arr, tmp, left, mid);
MergeSortTask t2 = new MergeSortTask(arr, tmp, mid, right);
invokeAll(t1, t2);
// 合并:注意 tmp 的复用与区间拷贝
merge(arr, tmp, left, mid, right);
}
private static void merge(int[] arr, int[] tmp, int left, int mid, int right) {
int i = left, j = mid, k = left;
while (i < mid && j < right) {
tmp[k++] = (arr[i] <= arr[j]) ? arr[i++] : arr[j++];
}
while (i < mid) tmp[k++] = arr[i++];
while (j < right) tmp[k++] = arr[j++];
// 拷回原数组
System.arraycopy(tmp, left, arr, left, right - left);
}
}
// quick demo
public static void main(String[] args) {
int[] arr = new java.util.Random().ints(2_000_000, 0, 10_000_000).toArray();
long t1 = System.currentTimeMillis();
sort(arr);
long t2 = System.currentTimeMillis();
System.out.println("sorted: " + isSorted(arr) + ", ms=" + (t2 - t1));
}
private static boolean isSorted(int[] arr) {
for (int i = 1; i < arr.length; i++) if (arr[i - 1] > arr[i]) return false;
return true;
}
}
性能要点(非常关键):
- 阈值
THRESHOLD决定任务粒度:过小 -> 任务太多调度过重;过大 -> 并行度不够。 tmp复用避免每次 merge 分配新数组(否则 GC 会给你脸色看)。- Fork/Join 适合 CPU 密集,不适合在任务里做长时间阻塞 IO。
7) 实战 2:StampedLock 优化读多写少(乐观读)
StampedLock 的思路很“现实”:
读很多、写很少?那我先“乐观读”一下,读完再检查期间有没有写入;如果被写打断,就退回到悲观读锁。
适合:读远多于写、且读操作比较快的场景(比如读缓存状态、读坐标、读配置快照)。
import java.util.concurrent.locks.StampedLock;
public class PointWithStampedLock {
private double x, y;
private final StampedLock lock = new StampedLock();
public void move(double dx, double dy) {
long stamp = lock.writeLock();
try {
x += dx;
y += dy;
} finally {
lock.unlockWrite(stamp);
}
}
// 读多写少场景的“明星方法”
public double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead();
double cx = x, cy = y;
// 验证期间是否有写入发生
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
cx = x;
cy = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.sqrt(cx * cx + cy * cy);
}
// 如果需要“读 -> 决策 -> 可能升级为写”
public void moveIfAtOrigin(double newX, double newY) {
long stamp = lock.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = lock.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
return;
} else {
lock.unlockRead(stamp);
stamp = lock.writeLock();
}
}
} finally {
lock.unlock(stamp);
}
}
}
StampedLock 的注意事项(很重要,别踩):
- 它不是可重入锁(Reentrant),同一线程重复获取可能把自己绕进去。
- 乐观读适合短读;读逻辑太长会频繁 validate 失败,反而不划算。
- 锁升级要用
tryConvertToWriteLock这种“转换”思路,否则容易死锁/活锁。
8) 常见陷阱:这些坑真的很“经典”
8.1 ABA 问题(“看起来没变,其实变过了”)
CAS 只比较“当前值是否等于旧值”。如果值从 A -> B -> A,CAS 会误以为没变。
解决思路:
AtomicStampedReference(带版本号 stamp)AtomicMarkableReference(带标记位)- 或者用更高层的不可变对象/锁
8.2 锁饥饿(Starvation)
非公平锁在高竞争下可能让某些线程长期拿不到锁。
解决:
- 关键路径用公平锁(注意吞吐会降)
- 调整任务优先级/减少临界区/拆锁
8.3 死锁(Deadlock)
两把锁交叉获取最常见:线程 1 拿 A 等 B;线程 2 拿 B 等 A。
预防:
- 统一加锁顺序(强约束)
tryLock + 超时 + 回退- 减少嵌套锁,或用更高层并发结构
8.4 错误的内存可见性假设(“我明明改了啊,它怎么没看到?”)
典型错误:
- 以为多线程读写普通变量一定能看到最新值
- 双重检查锁(DCL)没配
volatile - 用
boolean stop停线程却发现停不下来
正确姿势:
- 用
volatile保证可见性(但不保证复合操作原子性) - 用锁(进入/退出锁具备 happens-before)
- 用原子类或并发工具类(
CountDownLatch、CyclicBarrier等)
9) 延伸阅读与学习路线
-
《Java Concurrency in Practice》:并发领域的“基本法”,尤其是 happens-before、线程安全发布、取消与中断等内容。
-
Doug Lea 的并发包相关资料(
java.util.concurrent的设计思想) -
关键字路线建议:
- JMM(Java Memory Model)/ happens-before
- AQS(AbstractQueuedSynchronizer)理解
ReentrantLock/Semaphore的底层 - 并发容器内部机制(CHM、LongAdder、Striped64)
- 无锁队列(Michael-Scott queue)、Hazard pointers、RCU 等(更偏算法)
小结(给你一个工程化的“选择策略”)
- 临界区小、逻辑简单:
synchronized足够稳 - 需要中断/超时/多条件队列:
ReentrantLock + Condition - 读多写少:优先考虑
StampedLock(前提:能接受非可重入 + 读逻辑够短) - CPU 密集分治:
ForkJoinPool+ 合理阈值 - 计数热点:
LongAdder往往比AtomicLong更抗压 - 并发容器:能用就用,别自己造轮子(除非你真的打算写论文🙂)
… …
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
… …
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!
- 点赞
- 收藏
- 关注作者
评论(0)