难道你还想靠“加个 synchronized 就完事儿”来写高性能并发吗?

举报
喵手 发表于 2026/01/15 18:04:35 2026/01/15
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

前言 😅

前言——说真的,并发这玩意儿最“阴险”的地方在于:它能跑不代表它正确;它“看起来没问题”不代表线上不会在凌晨 3 点把你薅起来。写并发代码像在黑屋子里摆多米诺骨牌:你以为摆稳了,结果风一吹(线程调度一变、CPU 核数一换、负载一上来),哗啦啦全倒。
  这篇就按你给的大纲,把 Java 并发的高级知识(锁 / 无锁 / Fork-Join / 并发容器 / 原子类 / 常见坑)串起来讲透,配两段实战代码:ForkJoin 并行归并排序StampedLock 优化读多写少。我会尽量讲“人话”,但不牺牲专业度;也会顺手吐槽两句(毕竟谁还没被死锁教育过呢🙃)。

目录

  1. 并发设计的“地图”:锁、无锁、任务并行、并发容器到底在解决什么
  2. synchronized vs Lock/Condition:差异、适用场景、常见误区
  3. 并行计算模型:ExecutorServiceForkJoinPool、并行流(Parallel Stream)
  4. 非阻塞算法与 CAS:AtomicXUnsafe/VarHandle 视角下的“原子性”
  5. 并发集合与性能考量:ConcurrentHashMap 等容器的“快”从哪来
  6. 实战 1:Fork/Join 实现并行归并排序(可直接跑)
  7. 实战 2:StampedLock 优化“读多写少”(乐观读 + 回退)
  8. 常见陷阱:ABA、锁饥饿、死锁、错误的内存可见性假设
  9. 延伸阅读与学习路线

1) 并发设计的“地图”

高性能并发设计,通常逃不开四类武器:

  • 锁(Blocking):用互斥来换正确性(synchronizedReentrantLockStampedLock)。
    优点:好理解;缺点:竞争激烈时吞吐掉得快,还可能饥饿/死锁。
  • 无锁(Non-blocking):用 CAS/原子变量减少阻塞(AtomicIntegerLongAdder、CAS 循环)。
    优点:高竞争下更能扛;缺点:更难写对,还会遇到 ABA、活锁、自旋浪费 CPU。
  • 任务并行模型:把问题切成任务调度(ExecutorServiceForkJoinPool、并行流)。
    优点:结构化并行、容易扩展;缺点:任务划分不合理会适得其反(分太细:调度开销爆炸;分太粗:吃不满 CPU)。
  • 并发容器:把“并发控制”封装进数据结构(ConcurrentHashMapCopyOnWriteArrayList)。
    优点:少踩坑;缺点:误用也会很惨(例如在高写入下用 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 绑定在 对象监视器 上,每个对象一个等待队列,表达多个条件时很乱。
  • ConditionLock 的“等待队列”,可以一个锁配多个条件:notEmptynotFull 这种场景写起来舒服很多。

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, ...) 搞得锁竞争很重,优先考虑 LongAddercomputeIfAbsent + 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)
  • 用原子类或并发工具类(CountDownLatchCyclicBarrier 等)

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 !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。