int a = 1 是原子性操作吗

典型回答 在Java中,int a = 1;这条语句可以被认为是原子性操作,因为它是一个简单的赋值语句,它在一个操作中完成,不会被中断。在单线程的情况下,这条语句将会原子性地执行,即将1赋值给变量a的操作是不可分割的,不会被其它线程中断。 但是,在多线程的情况下,多个线程可以同时访问和修改同一个变量,这就可能导致竞态条件(race condition)的问题,即多个线程争夺同一个资源,导致结果无法预测。 举个例子: 1 2 3 4 5 6 7 int a = 0; // 线程1 a = 1; // 线程2 a = 2; 尽管每个赋值语句本身可能是原子的,但是在多线程环境中,线程1和线程2的执行顺序是不确定的。可能会出现以下情况: 线程1执行完毕后,线程2再执行,此时a的值为2。 线程2执行完毕后,线程1再执行,此时a的值为1。 这种情况被称为竞态条件(race condition)。为了避免竞态条件和确保线程安全,可以使用同步机制,例如使用 synchronized 关键字或者 java.util.concurrent 包中的原子类(如 AtomicInteger)来保护共享数据的访问。这样可以确保在同一时刻只有一个线程能够访问共享数据,从而避免竞态条件。 扩展知识 User a = new User(); 是原子性操作吗? 在Java中,User a = new User();这条语句看起来是单个操作,但实际上它包含了几个步骤,这些步骤包括了: 1、为User对象分配内存:JVM首先为新的User对象分配内存。 2、调用构造函数初始化对象:执行User类的构造函数来初始化对象。 3、引用赋值:将对象的内存引用赋给变量a。 尽管在源代码层面上这看起来是一个单一操作,实际执行时它涉及到多个底层步骤。 然而,关于引用赋值部分(即将内存地址赋值给变量a这一步),Java语言规范保证了它的原子性,意味着引用变量的赋值操作是原子的。这意味着在任何时间点,线程看到的引用变量a要么是指向某个User对象的内存地址,要么是null(或者是之前指向的另一个有效对象的地址),不会出现中间状态。 总结来说,虽然整个User a = new User();操作不是原子性的,但是其中的赋值部分是原子性的。 如果在很极限的高并发场景,并且伴随着指令重排的话,比如把引用复制这步骤重排到前面去了,那么就可以被别的线程拿到一个并不完整的对象。具体可以参考: ✅有了synchronized为什么还需要volatile? 里面讲的就是这个问题。

March 22, 2026 · 1 min · santu

volatile是如何保证可见性和有序性的?

典型回答 volatile和可见性 对于volatile变量,当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。 所以,如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。 ✅什么是MESI缓存一致性协议 volatile和有序性 volatile除了可以保证数据的可见性之外,还有一个强大的功能,那就是他可以禁止指令重排优化等。 普通的变量仅仅会保证在该方法的执行过程中所依赖的赋值结果的地方都能获得正确的结果,而不能保证变量的赋值操作的顺序与程序代码中的执行顺序一致。 volatile是通过内存屏障来禁止指令重排的,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。被volatile修饰的变量的操作,会严格按照代码顺序执行,load->add->save 的执行顺序就是:load、add、save。 如经典的双重校验锁必须加volatile的问题,就是因为volatile加了内存屏障。 ✅有了synchronized为什么还需要volatile? 扩展知识 内存屏障 ✅到底啥是内存屏障?到底怎么加的?

March 22, 2026 · 1 min · santu

CAS一定有自旋吗?

典型回答 不一定,但是通常为了提高CAS的成功率,会考虑做自旋。 最简单的自旋就是while(true) 通常情况下,CAS 操作都会采用自旋的方式,当 CAS 失败时,会重新尝试执行 CAS 操作,直到操作成功或达到最大重试次数为止。 因为,CAS 操作一般都是在多线程并发访问时使用,如果直接阻塞线程,会导致性能下降,而采用自旋的方式,可以让 CPU 空转一段时间,等待锁被释放,从而避免线程切换和阻塞的开销。 但是,如果自旋时间过长或者线程数过多,就会占用过多的 CPU 资源,导致系统性能下降,因此在使用 CAS 操作时,需要根据实际情况进行适当的调整。

March 22, 2026 · 1 min · santu

什么是CAS?存在什么问题?

典型回答 CAS是一项乐观锁技术,是Compare And Swap的简称,顾名思义就是先比较再替换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。在进行并发修改的时候,会先比较A和V中取出的值是否相等,如果相等,则会把值替换成B,否则就不做任何操作。 当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 在JDK1.5 中新增java.util.concurrent(J.U.C)就是建立在CAS之上的。相对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。 CAS的主要应用就是实现乐观锁和锁自旋。 扩展知识 ABA问题 CAS会导致“ABA问题”。 CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差内会导致数据的变化。 比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且2进行了一些操作变成了B,然后2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,然后1操作成功。尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。 举个例子,线程1和线程2同时通过CAS尝试修改用户A余额,线程1和线程2同时查询当前余额为100元,然后线程2因为用户A要把钱借给用户B,先把余额从100改成50。然后又有用户C还给用户A 50元,线程2则又把50改成了100。这时线程1继续修改,把余额从100改成200。 虽然过程上金额都没问题,都改成功了,但是对于用户余额来说,丢失了两次修改的过程,在修改前用户C欠用户A 50元,但是修改后,用户C不欠钱了,而用户B欠用户A 50元了。而这个过程数据是很重要的。 上面这个例子是很多八股文中讲ABA举的例子,包括我之前也用过这个例子,但是其实这个例子有个问题就是你仔细想想,上面的流程真的有问题吗?只要每一次账户余额变更都有操作记录,都有流水,其实整个过程是没啥业务上的问题的,用户C不欠钱了,因为他把钱还了。B欠钱了,因为他确实借了。所以最终是咋样还是咋样的 那么ABA到底有什么问题呢?如何体现出这个问题呢?举个形象的例子: 小A和小B准备结婚,两个人都到民政局了,这时候小B说我要先上个厕所,你等我一下,这时候小B去找小C进行了登记结婚,然后又进行了离婚。回来之后继续和小A完成了登记。 整个过程,就是个典型的ABA问题,虽然对于小A来说,老婆还是那个老婆,登记也成功了,但是是这么回事儿吗?老婆已经是离过婚的了呀,甚至他可能直接把财产都转移了。 所以,这就是ABA问题在业务开发中最大的影响,那就是你以为你Compare的那个数据没变化就没问题了,但是这中间有可能他变过,同时导致了其他的变化,虽然他自己有改回来了,但是不代表被他影响的数据也能改回来。而这就是被我们忽略的中间操作!如果这个中间操作的结果和本次CAS操作的结果是互斥的,而你有没有做更多的检查,就通过一个字段的CAS做了比较,那么就可能会出现问题。 那估计有人说了,如果我把多个字段一起做CAS呢?首先,当你想到要这么做的时候,你就已经认为ABA问题确实会带来问题,然后想办法在解决了。但是解决问题不只有这一个方案,还有个更好的方案,那就是通过版本号。 部分乐观锁的实现是通过版本号(version)的方式来解决ABA问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。 版本号可以确保,不管这行记录是哪个字段或者哪几个字段发生了变化,他都会改变。所以有了这个一个字段,就能解决所有的ABA问题了。 在Java中,可以借助AtomicStampedReference,它是 Java 并发编程中的一个类,用于解决多线程环境下的“ABA”问题。AtomicStampedReference 通过同时维护一个引用和一个时间戳,可以解决 ABA 问题。它允许线程在执行 CAS 操作时,不仅检查引用是否发生了变化,还要检查时间戳是否发生了变化。这样,即使一个变量的值被修改后又改回原值,由于时间戳的存在,线程仍然可以检测到这中间的变化。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.util.concurrent.atomic.AtomicStampedReference; public class Example { public static void main(String[] args) { String initialRef = "hollis"; int initialStamp = 0; AtomicStampedReference<String> atomicStampedRef = new AtomicStampedReference<>(initialRef, initialStamp); String newRef = "hollis666"; int newStamp = initialStamp + 1; boolean updated = atomicStampedRef.compareAndSet(initialRef, newRef, initialStamp, newStamp); System.out.println("Updated: " + updated); } } 在这个例子中,AtomicStampedReference 初始化时不仅包含了一个初始引用 “hollis”,还包含了一个初始的时间戳 0。当我们尝试使用 compareAndSet 方法更新引用时,我们同时提供了期望的引用值和时间戳,以及新的引用值和时间戳。这样,只有当当前引用和时间戳都匹配期望值时,更新操作才会成功,从而避免了 ABA 问题。 ...

March 22, 2026 · 2 min · santu

CAS在操作系统层面是如何保证原子性的?

典型回答 CAS是一种基本的原子操作,用于解决并发问题。在操作系统层面,CAS 操作的原理是基于硬件提供的原子操作指令。在x86架构的CPU中,CAS 操作通常使用 cmpxchg 指令实现。 为啥cmpxchg指令可以保证原子性呢?主要由以下几个方面的保障: cmpxchg 指令是一条原子指令。在 CPU 执行 cmpxchg 指令时,处理器会自动锁定总线,防止其他 CPU 访问共享变量,然后执行比较和交换操作,最后释放总线。 cmpxchg 指令在执行期间,CPU 会自动禁止中断。这样可以确保 CAS 操作的原子性,避免中断或其他干扰对操作的影响。 cmpxchg 指令是硬件实现的,可以保证其原子性和正确性。CPU 中的硬件电路确保了 cmpxchg 指令的正确执行,以及对共享变量的访问是原子的。 扩展知识 CAS的可见性保障 同样是因为cmpxchg 指令,这个指令是基于 CPU 缓存一致性协议实现的。在多核 CPU 中,所有核心的缓存都是一致的。当一个 CPU 核心执行 cmpxchg 指令时,其他 CPU 核心的缓存会自动更新,以确保对共享变量的访问是一致的。 ✅什么是MESI缓存一致性协议

March 22, 2026 · 1 min · santu

synchronized和reentrantLock区别?

典型回答 ReentrantLock 和 synchronized 都是用于线程的同步控制,但它们在功能上来说差别还是很大的。对比下来 ReentrantLock 功能明显要丰富的多。 二者相同点是,都是可重入锁。二者也有很多不同,如: synchronized是Java内置特性,而ReentrantLock是通过Java代码实现的。 synchronized是可以自动获取/释放锁的,但是ReentrantLock需要手动获取/释放锁。 ReentrantLock还具有响应中断、超时等待等特性。 ReentrantLock可以实现公平锁和非公平锁,而synchronized只是非公平锁。(✅sychronized是非公平锁,那么是如何体现的? ) 另外,随着JDK21的发布,虚拟线程已经推出,在虚拟线程中,不建议使用synchronized,而是建议用ReentrantLock。 ✅为什么虚拟线程不能用synchronized? 扩展知识 ReentrantLock用法 Java语言直接提供了synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。 java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁,ReentrantLock 内部是基于 AbstractQueuedSynchronizer(简称AQS)实现的。 ReentrantLock是可重入锁,它和synchronized一样,一个线程可以多次获取同一个锁。 用法: 1 2 3 4 5 6 7 8 9 10 11 12 public class Counter { private final Lock lock = new ReentrantLock(); private int count; public void add(int n) { lock.lock(); try { count += n; } finally { lock.unlock(); } } } 怎么创建公平锁? new ReentrantLock() 默认创建的为非公平锁,如果要创建公平锁可以使用 new ReentrantLock(true)。 ...

March 22, 2026 · 1 min · santu

CountDownLatch、CyclicBarrier、Semaphore区别?

典型回答 CountDownLatch、CyclicBarrier、Semaphore都是Java并发库中的同步辅助类,它们都可以用来协调多个线程之间的执行。 但是,它们三者之间还是有一些区别的: CountDownLatch是一个计数器,它允许一个或多个线程等待其他线程完成操作。它通常用来实现一个线程等待其他多个线程完成操作之后再继续执行的操作。 CyclicBarrier是一个同步屏障,它允许多个线程相互等待,直到到达某个公共屏障点,才能继续执行。它通常用来实现多个线程在同一个屏障处等待,然后再一起继续执行的操作。 Semaphore是一个计数信号量,它允许多个线程同时访问共享资源,并通过计数器来控制访问数量。它通常用来实现一个线程需要等待获取一个许可证才能访问共享资源,或者需要释放一个许可证才能完成操作的操作。 CountDownLatch适用于一个线程等待多个线程完成操作的情况 CyclicBarrier适用于多个线程在同一个屏障处等待 Semaphore适用于一个线程需要等待获取许可证才能访问共享 使用CountDownLatch、CyclicBarrier、Semaphore实现线程协调: ✅有三个线程T1,T2,T3如何保证顺序执行?

March 22, 2026 · 1 min · santu

LongAdder和AtomicLong的区别?

典型回答 LongAdder是Java 8中推出了一个新的类,主要是为了解决AtomicLong在多线程竞争激烈的情况下性能并不高的问题。它主要是采用分段+CAS的方式来提升原子操作的性能。 相比于AtomicLong,LongAdder有更好的性能,但是LongAdder是典型的以空间换时间的实现方式,所以他所需要用到的空间更大, 而且LongAdder可能存在结果不准确的问题,而AtomicLong并不会。 扩展知识 AtomicLong实现原理 AtomicLong和AtomicInteger、AtomicDouble�等其他原子操作的类一样,都是基于Unsafe�实现的。Unsafe�是一个用来进行硬件级别的原子操作的工具类。在AtomicLong�中定义如下: 1 private static final Unsafe unsafe = Unsafe.getUnsafe(); 并且通过在类中定义一个volatile的变量用来计数: 1 private volatile long value; 以下几个常用的方法具体实现如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public final long incrementAndGet() { return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L; } public final long addAndGet(long delta) { return unsafe.getAndAddLong(this, valueOffset, delta) + delta; } public final boolean compareAndSet(long expect, long update) { return unsafe.compareAndSwapLong(this, valueOffset, expect, update); } public final long getAndSet(long newValue) { return unsafe.getAndSetLong(this, valueOffset, newValue); } 可以看到,都是基于unsafe来实现的,其实就是调用了底层的CAS操作来进行原子操作的。 ...

March 22, 2026 · 2 min · santu

如何对多线程进行编排

典型回答 在Java 8中, 新增加了一个新的类: CompletableFuture,它提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture的方法。 CompletableFuture类实现了Future接口,所以你还是可以像以前一样通过get方法阻塞或者轮询的方式获得结果,但是这种方式不推荐使用。 CompletableFuture和FutureTask同属于Future接口的实现类,都可以获取线程的执行结果。 借助CompletableFuture就能实现对多线程进行编排。 ✅CompletableFuture的底层是如何实现的? 关于CompletableFuture的具体用法,大家可以参考:https://juejin.cn/post/7140244126679138312 这篇介绍的挺全的。

March 22, 2026 · 1 min · santu

有三个线程T1,T2,T3如何保证顺序执行?

典型回答 想要让三个线程依次执行,并且严格按照T1,T2,T3的顺序的话,主要就是想办法让三个线程之间可以通信、或者可以排队。 想让多个线程之间可以通信,可以通过join方法实现,还可以通过CountDownLatch、CyclicBarrier和Semaphore来实现通信。 想要让线程之间排队的话,可以通过线程池或者CompletableFuture的方式来实现。 扩展知识 依次执行start方法 在代码中,分别依次调用三个线程的start方法,这种方法是最容易想到的,但是也是最不靠谱的。 代码实现如下,通过执行的话可以发现,数据结果是不固定的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static void main(String[] args) { Thread thread1 = new Thread(new Runnable() { @Override public void run() { System.out.println("Thread 1 running"); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { System.out.println("Thread 2 running"); } }); Thread thread3 = new Thread(new Runnable() { @Override public void run() { System.out.println("Thread 3 running"); } }); thread1.start(); thread2.start(); thread3.start(); } 以上代码的数据结果每次执行都不固定,所以,没办法满足我们的要求。 ...

March 22, 2026 · 6 min · santu

留言给博主