news 2026/9/30 5:51:54

Java多线程同步全解析:从synchronized到Lock与并发工具实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java多线程同步全解析:从synchronized到Lock与并发工具实战

1. 线程同步到底在解决什么问题

先抛一个所有人都会遇到的场景:你写了一个秒杀系统,库存只剩10件,结果同一时刻有30个用户同时下单。如果程序处理得不够严谨,最终成交的订单数可能远超10件,甚至出现库存变成负数的笑话。这不是段子,而是真实生产环境里我再熟悉不过的血泪教训。我们常说的线程并发冲突、数据错乱,本质上就是线程同步没有做好。

很多人学Java多线程,第一步就是记住synchronized、Lock、ThreadLocal这些名词。但我觉得,理解“为什么要同步”比会写同步代码重要得多。Java多线程程序跑得好不好,几乎全部取决于你对并发底层问题的理解深度。这就像开车没必要会造发动机,但你至少得知道油门踩大了会窜出去、刹车踩急了会打滑,否则出事只是早晚的问题。

线程同步要解决的核心,是对共享资源的访问控制。Java内存模型(JMM)规定了线程和主内存之间的交互,它揭示了并发出现问题的根源,归纳起来就是三个经典特性:原子性、可见性、有序性。

原子性指一个操作是不可中断的,要么全做,要么全不做。典型反例是 i++,看起来是一行代码,实际在字节码层面经历读取、修改、写回三步,多个线程同时执行就可能在同一个旧值上叠加,导致最终结果远小于预期。

可见性指一个线程修改了共享变量,另一个线程能不能立刻看到。在JMM里,每个线程都有自己独立的工作内存,变量读写的都是副本。如果A线程改了变量还没来得及刷回主内存,B线程读到的还是旧值,这就产生了可见性问题。

有序性指指令执行的顺序,JVM和CPU为了优化性能会进行指令重排序。单线程下重排序不影响结果,但多线程下可能出现类似“先打印后赋值却被另一个线程先看到赋值”的怪现象。一个经典的例子是双重检查锁单例(DCL),如果实例字段不加volatile,另一个线程可能拿到一个“半初始化”的对象,程序看起来没报错,却莫名空指针。

正是因为这三个特性的存在,我们才需要线程同步机制来兜底。注意一个关键认知:同步的目的不是让线程排队执行,而是让共享数据在并发环境下保持一致和正确。这点如果理解偏了,很容易写出“为了同步而同步”的烂代码,比如把一个高性能的并发系统硬生生压成单线程,那还不如不用多线程。

2. 锁的核心:synchronized的深度用法

2.1 三种用法与锁对象辨析

synchronized是Java语言层面最基础的同步手段,理解它的关键在于“锁住的是对象,不是代码”。

第一种是同步实例方法,锁住的是当前实例对象this。同一个实例的多个线程访问同步方法会互斥,但不同实例之间互不影响。很多初学者写了一个带synchronized方法的类,然后创建多个实例分别交给不同线程,发现同步完全失效,就是因为锁对象是this而不是这个类。

第二种是同步静态方法,锁住的是当前类的Class对象。Class对象在JVM里全局唯一,所以静态同步方法天然具备类级别的互斥效果。这里有个容易踩坑的点:实例同步方法和静态同步方法用的不是同一把锁,两者并发执行互不干扰,但如果你期望它们彼此互斥,结果会让你怀疑人生。

第三种是同步代码块,这是实际项目里用得最多的一种。它可以锁定任意对象,写法是synchronized(lock) { ... },比较典型的是锁“常量字符串的intern对象”或者用单独的Object实例充当锁。为什么不建议直接用字符串常量做锁?这涉及到锁对象的可见范围问题,在.class文件里,内容相同的字符串可能合并到同一个String实例,导致本不相干的线程抢同一把锁,万一是用"lock"这种常量,JVM内部常量池会直接复用,其他业务代码也用到同样字符串常量的时候就会无端阻塞,排查起来非常痛苦。

锁对象的选择直接决定同步范围。我习惯用一个原则:永远锁住操作的数据所属的对象,而不是随便new一个Object去套。比如操作一个ArrayList,就synchronized这个list对象本身;操作某个Account对象,锁账号实例比锁一个外部静态变量更内聚,更符合业务直觉。代码评审时看到synchronized(new Object())这种写法,基本可以直接打回,因为每次new出来的锁对象不同,多线程之间根本不存在互斥关系。

2.2 可重入性与锁升级机制

synchronized是可重入锁,即同一个线程已经持有一把锁之后,可以再次进入由这把锁保护的代码区域。比如方法A加了synchronized,A内部又调用同一个类的同步方法B,线程持有A的锁时进入B不需要重新竞争。底层记录着锁的持有线程和重入次数,重入一次计数加一,退出一次计数减一,归零才真正释放。可重入性极大简化了编程模型,否则我们在A里调用B之前还得手动“解开”外层锁,或者更糟糕——直接死锁。

JDK 1.6之后,synchronized不再是一个单纯的重量级锁,而是引入了锁升级机制。锁的初始形态是无锁,随着并发竞争逐步升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。

偏向锁的意思是,同一线程第一次获取锁时,会在对象头Mark Word里记录线程ID。之后这个线程再次进入同步块时,不需要做任何同步操作,直接执行,性能开销接近零。但如果另一个线程来竞争,偏向锁会撤销,进入轻量级锁状态。轻量级锁通过CAS自旋尝试获取锁,适合线程“等一下就能拿到锁”的场景。如果自旋次数超过阈值(默认是10次,自适应的版本会动态调整),锁就膨胀成重量级锁,此时未拿到锁的线程会被挂起阻塞,涉及用户态和内核态的切换,开销最大。

面试时遇到“synchronized的性能”问题,很多人还停留在老版本印象里说它慢。其实锁升级机制让synchronized在低竞争下性能很好,只有高竞争场景才需要对比ReentrantLock。这几年我用synchronized写业务同步代码,绝大多数场景都够用,真正需要上ReentrantLock的只有少数对等待中断、超时、公平性有要求的场景。

注意:synchronized既保证原子性,也保证可见性和有序性。它在进入同步块时清空工作内存,退出时把修改强制刷新到主内存,这就是所谓的“synchronized happens-before规则”。理解这一点非常重要——不仅争用锁的线程被阻塞了,数据的一致性也得到保障。

3. volatile、原子类与Lock家族,谁才是你的选择

3.1 volatile的边界在哪里

valatile是Java里最轻量的同步机制,它保证变量的可见性和禁止指令重排序,但不保证原子性。很多人拿它和synchronized对比,其实两者的定位完全不同。我用一句话总结:volatile是状态标志位和个人使用的“轻量级同步”,synchronized是复合操作的“重型保障”。

volatile适合的经典场景是boolean stop标志位。一个线程运行while循环,另一个线程修改stop状态,如果不加volatile,循环线程可能永远看不到新值,程序死循环跑满CPU。加了volatile后,修改立刻对其他线程可见,简洁又高效。但如果你试图用volatile修饰int count来实现多线程累加,结果一定是错的,因为count++不是原子操作,volatile解决不了多个线程同时读改写带来数据丢失的问题。

如何判断一个场景能不能用volatile?我的经验是看操作是否满足“单次读写”约束。如果一个线程只写、其他线程只读,或者写操作不依赖当前值,那么volatile足够;如果涉及读改写、复合操作、依赖上个状态的校验,老老实实用锁或者原子类。

3.2 原子类与CAS的无锁思路

JDK的java.util.concurrent.atomic包提供了AtomicInteger、AtomicLong、AtomicBoolean等原子类,它们的核心是CAS(Compare And Swap)操作。CAS的流程是:拿内存中的值V和期望值A比较,相等就用新值B替换,不相等就重试或放弃。整个过程是CPU级别的一条原子指令,不需要加锁。

用AtomicInteger替代synchronized做计数是个典型的无锁优化:

import java.util.concurrent.atomic.AtomicInteger; public class Counter { private final AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } public int getCount() { return count.get(); } }

多个线程同时调用increment方法,底层通过CAS不断重试,在竞争不高时性能比synchronized好得多。但要注意CAS存在ABA问题:某个线程把变量从A改成B又改回A,另一个线程CAS时看到的值还是A,认为没有变化,实际上中间发生了修改。大多数业务场景ABA不构成问题,但如果你写的是无锁栈或链表,就要使用带版本号的AtomicStampedReference。

高并发场景下还有一个更进阶的选手:LongAdder。它把热点数据分散到多个Cell里,每个线程只更新自己那一个Cell的分值,从AtomicInteger的“单点CAS竞争”变成了“分段累加”的设计,大大降低冲突概率,最终sum时再汇总。LongAdder适合读少写多的统计场景,但它的读取不是强一致性的,精确度要求极高的场景需要自己权衡。

3.3 ReentrantLock与Condition的进阶手法

Lock接口是JDK 1.5引入的,最常用实现是ReentrantLock。和synchronized相比,它有几个独门武器:支持公平锁(构造参数传true,线程按请求顺序获取锁)、支持响应中断(lockInterruptibly)、支持超时获取(tryLock(timeout, unit)),还支持更灵活的条件变量Condition。

import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockCounter { private int count = 0; private final Lock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } }

注意ReentrantLock必须在finally块里手动解锁,一旦忘记解锁,锁永远不会被释放,其他线程会卡死到天荒地老。这是和synchronized最大的区别:synchronized异常时JVM自动释放锁,而Lock不行。我见过不止一次线上事故,罪魁祸首就是某个同事try/finally写漏了,导致接口请求大面积超时。

Condition的作用是替代传统的wait/notify机制。一个Lock可以创建多个Condition,每个条件队列独立管理,实现更细粒度的等待通知。经典的生产者消费者里,可以用两个Condition分别表示缓冲区非空和非满:消费者等待notEmpty,生产者放完触发notEmpty.signal;生产者等待notFull,消费者取完触发notFull.signal。而用synchronized的wait/notifyAll,每次唤醒的是所有线程,再让它们自己抢资源,效率低不少。

做个直接对比:在一个容器容量为10的生产者消费者模型里,single Condition 和多个Condition带来的差异,在高吞吐场景下非常明显,这也是面试里考察Lock用的经典题目。

3.4 synchronized与Lock怎么选

我给出的不是谁更好,而是各自的最适区间。表格整理一下:

维度synchronizedReentrantLock
底层实现Monitor(对象头标记)AQS(AbstractQueuedSynchronizer)
锁获取方式隐式,无感知显式lock/unlock
公平性非公平默认非公平,可配置公平
可中断不支持支持lockInterruptibly
超时机制不支持支持tryLock(timeout)
条件队列一个锁一个等待集可以创建多个Condition
异常释放自动释放必须finally手动释放
性能低竞争时有锁升级优化高竞争时可避免一些锁膨胀问题

大部分业务场景,我的建议是优先用synchronized,因为它写起来简单、不容易出错、可读性好。只有出现“需要可中断获取锁”“需要超时避免死等”“需要多个条件队列”“需要公平锁”这四类需求时,才切换去用ReentrantLock。

4. 线程间的协作:等待通知机制与并发工具类

4.1 wait/notify的正确打开方式

线程同步不只是互相排斥,很多场景线程之间还需要协调协作。Java提供的经典协作方式是基于Object.wait()和Object.notify()/notifyAll()的等待通知机制。

这里有一个黄金法则:wait、notify、notifyAll必须放在synchronized块或方法中调用,否则抛出IllegalMonitorStateException。原因是Java设计上要求调用wait前必须持有对象锁,这样可以避免丢失通知的竞态条件。

同时,wait之后线程会释放持有的锁,进入等待队列。这一点经常被忽略:如果wait不释放锁,持有锁的线程永远无法被人唤醒,程序直接死锁。

被唤醒后wait方法返回,线程需要重新去竞争锁,拿到锁才继续执行。这里有个极大的坑:不能假设被唤醒后条件一定满足了。wait期间可能有多个线程同时被唤醒,其中只有一个能抢到锁处理资源,另一个线程重新抢到锁时资源已经被别人消费光了。所以规范做法是用while循环检查条件,而不是用if:

synchronized (queue) { while (queue.size() == 0) { queue.wait(); // 条件不满足就等 } // 条件满足,消费一个元素 queue.remove(); }

这种写法同时也能规避虚假唤醒问题。所谓虚假唤醒,是指线程在没有notify的情况下被唤醒,这是操作系统层允许的行为,实际比较少见,但正规并发代码必须防御它。

4.2 CountDownLatch:等所有线程都跑完

热搜词里专门有个“java线程等待都完成”,这对应的正是CountDownLatch。它的场景非常典型:主线程启动N个子线程分别执行任务,然后主线程需要等待所有子线程都完成之后,再做汇总或收尾工作。

使用方式很直接:

import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class WaitAllExample { public static void main(String[] args) throws InterruptedException { int threadCount = 5; CountDownLatch latch = new CountDownLatch(threadCount); ExecutorService pool = Executors.newFixedThreadPool(threadCount); for (int i = 1; i <= threadCount; i++) { int taskId = i; pool.execute(() -> { try { Thread.sleep(500); System.out.println("任务" + taskId + "执行完毕"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); // 计数减一,必须在finally里保证执行 } }); } latch.await(); // 主线程阻塞等待计数归零 System.out.println("所有任务已完成,开始汇总结果"); pool.shutdown(); } }

CountDownLatch是一次性的,计数器减到0之后不可重置。如果需要反复等待多批次任务,就用CyclicBarrier,它支持循环利用。两者一字之差,语义完全不同,千万别用混。CountDownLatch是“一扇门,所有人都到了才开门”,CyclicBarrier是“所有线程互相等待,到齐后一起出发”,合作关系更强。

4.3 ThreadLocal:不共享就没有同步问题

线程同步还有一条逆向思路:既然同步是为了解决共享变量冲突,那能不能不共享?ThreadLocal就能实现每个线程独立持有自己的变量副本,线程之间天然隔离,不需要加锁。

常用的场景是SimpleDateFormat。这是个线程不安全的类,并发调用parse会抛出各种诡异异常。低版本下有人用synchronized包一层,结果性能暴跌;更优的做法是每个线程一个SimpleDateFormat实例:

private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); public Date parse(String time) throws ParseException { return DATE_FORMAT.get().parse(time); }

ThreadLocal的使用也要当心内存泄漏。ThreadLocalMap里的Entry是弱引用,但value是强引用。如果线程池里的线程长期存活,而ThreadLocal变量被置为null,value依然通过线程的Entry被引用,无法回收,可能触发OOM。规范习惯是使用完调用remove()清理,这一点是很多人不重视却实打实会踩的坑。

5. 实战场景拆解:从数据错乱到DCL单例

5.1 案例一:模拟售票超卖问题

为了讲清楚同步的威力,我用一个模拟场景来说明。定义一个TicketSystem类:

public class TicketSystem { private int tickets = 10; public void sell(String window) { if (tickets > 0) { tickets--; System.out.println(window + "卖出一张票,剩余:" + tickets); } else { System.out.println(window + "票已售罄"); } } }

三个窗口线程同时卖票,代码会出现两种情况:一是负库存,多个线程同时执行到判断语句,都认为tickets大于0就进入卖票逻辑;二是超卖,tickets--被拆成读取-修改-写回,多个线程申请的是同一个旧值。解决方式很简单,给sell方法加synchronized修饰,锁住当前TicketSystem实例,判断和扣减就形成原子操作了:

public synchronized void sell(String window) { // 剩余逻辑不变 }

为什么判断和扣减必须同步?因为“判断库存是否充足”和“扣减库存”在并发下必须看作一个整体操作。只锁扣减不锁判断,两个线程还是能同时进入判断,“判断+扣减”被拆成两步执行,照样出问题。

5.2 案例二:双重检查锁(DCL)单例

单例模式是Java面试里的常客,懒汉式单例在并发场景下必须考虑线程安全。那么经典的“双重检查锁”写法如下:

public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查:避免每次都加锁 synchronized (Singleton.class) { // 加锁保证线程安全 if (instance == null) { // 第二次检查:防止重复创建 instance = new Singleton(); } } } return instance; } }

这里instance为什么必须加volatile?因为instance = new Singleton()不是原子操作,它经历三个步骤:分配内存、执行构造方法初始化对象、把变量指向该内存地址。JVM和CPU的指令重排序可能把第2步和第3步调转,如果线程A执行完第3步后还没执行第2步,线程B进入第一次检查发现instance不为null,返回了一个尚未完成初始化的对象,随后调用其方法就可能触发空指针或莫名其妙的异常。

加volatile之后,禁止了重排序,确保线程B永远只能看到一个完整构造的对象。这个例子把volatile的有序性语义体现得淋漓尽致,也是我强烈建议所有Java开发者亲手写一遍并画一下内存时序图的知识点。

5.3 案例三:生产者消费者模型

我们用一个基于ReentrantLock+Condition的生产者消费者模型来收束实战部分,这是一个高频的面试手写题,也是日常业务中消息队列消费场景的简化版:

import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Processor<K> { private final int capacity; private final Queue<K> queue = new LinkedList<>(); private final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); public Processor(int capacity) { this.capacity = capacity; } public void put(K element) throws InterruptedException { lock.lock(); try { while (queue.size() == capacity) { notFull.await(); // 队列满了就等 } queue.offer(element); notEmpty.signal(); // 通知等待的消费者 } finally { lock.unlock(); } } public K take() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); // 队列空了就等 } K element = queue.poll(); notFull.signal(); // 通知等待的生产者 return element; } finally { lock.unlock(); } } }

这套代码的好处在于:notEmpty给到消费者的通知不会误伤生产者,反之亦然,两个等待队列互不干扰。如果是用synchronized的wait/notifyAll,每次唤醒都得把所有线程从等待队列拉出来重新竞争,并发高时上下文切换成本会明显上升。

5.4 并发工具类的选型速查

学习并发工具类,最怕背了概念却不知道什么时候用。结合实际场景,我总结一个选型逻辑:

  • 多个线程更新同一个计数:AtomicInteger,写入竞争大时升级为LongAdder
  • 共享状态存在复杂条件判断:synchronized代码块或ReentrantLock
  • 等待一批任务全部完成:CountDownLatch
  • 一群线程互相等待、齐步走:CyclicBarrier
  • 控制同时只能有N个线程访问资源:Semaphore
  • 只希望线程A写、线程B读,且状态简单:volatile足矣
  • 每个线程独立持有对象副本:ThreadLocal

这个经验不像官方文档那样面面俱到,但足够应对绝大多数生产问题。核心思想永远是:先明确并发问题是什么,再选择最小代价的解决方式,而不是上来就搬一个重量级工具。

6. 常见问题与排查技巧实录

6.1 死锁:最隐蔽的线程杀手

死锁是并发编程里最经典也最难排查的问题。线程A持有锁1等待锁2,线程B持有锁2等待锁1,两个线程就永远互相等待。

写个极简演示:

Object lockA = new Object(); Object lockB = new Object(); // 线程1 synchronized (lockA) { Thread.sleep(100); synchronized (lockB) { System.out.println("线程1获取到两把锁"); } } // 线程2 synchronized (lockB) { Thread.sleep(100); synchronized (lockA) { System.out.println("线程2获取到两把锁"); } }

排查死锁最有力的工具是线程转储。用jps找到进程PID,再执行jstack <pid>,输出里有明确的“Found one Java-level deadlock”字样,并列出两个线程互相等待的锁名称和代码行号。面试时如果被问到排查过程,完整回答出这一步是明显的加分项。

预防死锁的核心手段有三个:按固定顺序加锁,让所有线程以相同的顺序获取锁;尝试获取锁的超时机制,比如ReentrantLock的tryLock,拿不到锁就释放已持有的锁重试;缩小锁范围,尽量减少一次持有多个锁的机会。个人经验,业务代码里最管用的是按固定顺序加锁,从源头上消灭循环依赖。

6.2 数据错乱但难复现,怎么办

多线程Bug最讨厌的地方在于“偶发”。反复运行十次可能只失败一次,线上出问题,本地怎么都复现不了。

遇到这种情况,我一般从这几个方向排查:

第一,检查是否所有对共享变量的访问都被同一把锁保护。很多人只给写操作加了锁,读操作裸奔,或者用了不同的锁对象保护同一个变量,这两个都是常见的“看起来有同步,实际等于没有”。

第二,检查共享变量是否加了volatile。不加volatile,某线程修改的变量对另一个线程不可见,即使没有并发写,也可能读到旧值。此类Bug的表现是:单线程跑永远正常,多线程跑结果随机。

第三,用jstack看线程状态,是否大量Thread.State是BLOCKED、WAITING、TIMED_WAITING。如果大部分线程长时间卡在某个锁的monitor上,说明这里有严重的锁竞争,大概率存在锁范围过大或死循环持锁的问题。

第四,在JDK 9+的环境下可以使用jcmd或jfr录制一段运行数据,查看线程争用情况。jfr是做性能分析的利器,能记录锁竞争和阻塞分布的细节,值得花时间学。

6.3 锁粒度太大,性能被拖垮

很多团队把同步手段用对了,但性能依然很难看,问题大多出在锁粒度上。比如一个批量导入方法里,本来只是操作某个共享计数器需要同步,结果整个方法都加了synchronized。虽然正确性没问题,但所有调用者全部串行,吞吐量几乎被腰斩。

优化思路是把同步块缩小到最小必要范围,尽量只锁住真正的临界区代码。还是说计数器,你完全可以在方法内部只对count++加synchronized块,而不锁整个方法。如果方法里还有网络调用、数据库IO,这种优化带来的收益会非常恐怖。我见过一个业务方法,去掉大范围同步、换成细粒度锁之后,接口RT从800ms降到200ms,靠的就是“缩小同步范围”这一招。

6.4 常见问题速查表

现象可能原因排查方向
值总是小于预期i++复合操作缺同步检查是否加了锁或使用原子类
一加锁就StackOverflow可重入锁被误用为不可重入确认synchronized递归调用场景
某线程永远不退出共享标志位缺volatile给标志位加volatile
程序卡死无响应死锁或永久等待jstack查看线程状态
wait/notify抛异常未持有锁调用wait将调用移入synchronized块
明明成功了却还阻塞CountDownLatch计数不等于任务数检查是否有分支漏调countDown
内存持续增长ThreadLocal未清理使用完调用remove()
线程池任务全卡住锁未在finally中释放检查lock/unlock是否配对

这份速查表不是万能药,但覆盖了我这几年在生产环境里遇到的多线程问题80%以上的表象。遇到问题先对号入座,能省下大量排查时间。

6.5 几个我反复踩过的坑

坑一:把this当作锁对象导致锁扩散。如果一个类里有多个不相关的同步方法,都用了synchronized修饰(等价于锁this),那么不同业务场景的同步方法之间也会互相阻塞。比如一个UserService既有login同步方法,又有updatePassword同步方法,两个方法锁同一把this锁,登录请求和改密请求互相影响。改进做法是拆成多个锁对象,或改用显式Lock分别加锁。

坑二:并发量很大时还去用公平锁。公平锁虽然让线程按顺序获取锁,但它需要维护一个先进先出的等待队列,大量线程排队时开销非常高。业务上如果对“插入顺序”没有强需求,老老实实用默认的非公平锁。我见过有团队为了“看起来公平”把性能拖垮的案例,实在得不偿失。

坑三:教科书说用synchronized,项目里却不知道锁该放哪。我的习惯是先明确共享变量是谁,其次考虑有没有复合操作,最后决定用同步还是用原子类或ThreadLocal。不要为了用并发工具而用,先有并发问题,才有并发方案。

坑四:以为加了锁就不会有可见性问题。锁确实能解决可见性,但前提是所有访问都走同一把锁。如果有人绕过锁直接读共享变量,就是所谓的“数据竞争”,此时锁的happens-before规则完全不生效,数据依然不可见。代码审查时一定要检查读操作是否也被同步。

7. 多线程同步的后续扩展

线程同步这个话题,到这里其实只讲了并发编程的入门三板斧。真正生产环境里,我们很少直接new Thread去裸写,而是通过线程池管理线程;也很少手写Lock队列,更多用并发容器如ConcurrentHashMap、BlockingQueue。这些组件内部已经把同步细节封装好了,使用门槛降低不少。

后续你还可以沿着几条线深入:一条是JUC包的核心原理,AQS框架、ReentrantReadWriteLock、StampedLock;一条是Java内存模型的进阶细节,happens-before规则的完整七条、final字段的内存语义;还有一条是更高层的并发框架,比如CompletableFuture的异步编排、ForkJoinPool的分治计算。如果要做线上调优,那么锁竞争分析、JFR录制、JIT编译优化也值得研究。

就我个人的体会,理解线程同步最简单的方式,是把它当成“房屋钥匙管理规则”:共享资源就是公共活动室,不同线程就是不同的人,synchronized是所有人都必须遵守的物理锁。单项原子操作用自动门,不共享的资源就一把钥匙一个人用,而CountDownLatch就像小区集体活动的集合通知——所有人才可以出门。模型想通了,代码怎么写都自然。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 5:51:29

补码等于反码加一?从模运算与位权看补码的完整证明与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 5:50:33

WorkBuddy AI工作台实战:从安装配置到Skill开发与缓存迁移避坑指南

1. 为什么我要认真聊聊 WorkBuddy 这个 AI 工作台第一次接触 WorkBuddy 是在一个做企业数字化的朋友那里。他当时丢给我一句话&#xff1a;“你把它当成一个能自己动手干活的 AI 同事&#xff0c;而不是一个只会聊天的机器人。”这句话基本概括了 WorkBuddy 的定位——腾讯推出…

作者头像 李华
网站建设 2026/9/30 5:50:33

政务信创改造避坑指南!4款主流云桌面优缺点全方位拆解

政务数字化转型迈入深度落地阶段&#xff0c;政务信创办公已经成为各级党政机关信息化升级的核心工作。现阶段多数政务单位在信创改造中&#xff0c;普遍面临软硬件适配不兼容、分散终端难以统一管控、内部数据安全防护薄弱、老旧办公设备无法复用等诸多实操难题。如何在严格契…

作者头像 李华
网站建设 2026/9/30 5:49:58

实验二:标准外设库方式流水灯与Keil仿真

一、实验目的 掌握STM32标准外设库&#xff08;SPL&#xff09;的工程搭建与GPIO库函数用法&#xff1b;用标准外设库实现LED轮流闪烁&#xff1b;使用Keil软件仿真逻辑分析仪观察GPIO输出波形&#xff0c;分析闪烁周期。二、实验硬件元件引脚说明红色LEDPA0高电平亮蓝色LEDPA2…

作者头像 李华
网站建设 2026/9/30 5:49:12

Windows环境变量配置指南:Path、JAVA_HOME与多版本切换

上个月帮同事看一台新装的开发机&#xff0c;命令行敲java -version直接甩回一句"不是内部或外部命令"&#xff0c;他盯着屏幕上明明已经装好的 JDK 一脸茫然。这种场景我在 Windows 上见过太多次了——软件装完了&#xff0c;就是跑不起来&#xff0c;八成是环境变量…

作者头像 李华