目录
1. JUC包下你常用的类?
2. Java中有哪些常用的锁,在什么场景下使用?
3. Java 并发工具你知道哪些?
4. 什么是 JMM?为什么需要 JMM?
5 Java 内存区域和 JMM 有何区别?
6. voliatle关键字有什么作用?
7. volatile 可以保证原子性么?
8. 指令重排序的原理是什么?
9. 悲观锁和乐观锁的区别?
10. Java中想实现一个乐观锁,都有哪些方式?
11. synchronized和reentrantlock的工作原理及其应用场景?
12. 除了用synchronized,还有什么方法可以实现线程同步?
13. synchronized锁静态方法和普通方法区别?
14. sychronized和volatile比较?
15. synchronized和reentrantlock区别?
16. 怎么理解可重入锁?
17. synchronized 支持重入吗?如何实现的?
18. Synchronized锁升级的过程讲一下?
19. JVM对Synchornized的优化?
20. 什么是公平锁和非公平锁?
21. 非公平锁吞吐量为什么比公平锁大?
22. ThreadLocal 有什么用?
23. ThreadLocal 原理了解吗?
24. ThreadLocal 内存泄露问题是怎么导致的?
25. 如何跨线程传递 ThreadLocal 的值?
26. ThreadLocalMap.set()原理?
27. ThreadLocal使用场景?
28. 介绍一下AQS?
29. AQS 的原理是什么?
30. CAS 和 AQS 有什么关系?
31. 如何用 AQS 实现一个可重入的公平锁?
32. CAS 有什么缺点?
33. 为什么不能所有的锁都用CAS?
34. CAS 有什么问题,Java是怎么解决的?
35. Semaphore、CountDownLatch 、CyclicBarrier是做什么的讲一讲?
1. JUC包下你常用的类?
线程池相关:
ThreadPoolExecutor
:最核心的线程池类,用于创建和管理线程池。通过它可以灵活地配置线程池的参数,如核心线程数、最大线程数、任务队列等,以满足不同的并发处理需求。Executors
:线程池工厂类,提供了一系列静态方法来创建不同类型的线程池,如newFixedThreadPool
(创建固定线程数的线程池)、newCachedThreadPool
(创建可缓存线程池)、newSingleThreadExecutor
(创建单线程线程池)等,方便开发者快速创建线程池。
并发集合类:
ConcurrentHashMap
:线程安全的哈希映射表,用于在多线程环境下高效地存储和访问键值对。它采用了分段锁等技术,允许多个线程同时访问不同的段,提高了并发性能,在高并发场景下比传统的Hashtable
性能更好。CopyOnWriteArrayList
:线程安全的列表,在对列表进行修改操作时,会创建一个新的底层数组,将修改操作应用到新数组上,而读操作仍然可以在旧数组上进行,从而实现了读写分离,提高了并发读的性能,适用于读多写少的场景。
同步工具类:
CountDownLatch
:允许一个或多个线程等待其他一组线程完成操作后再继续执行。它通过一个计数器来实现,计数器初始化为线程的数量,每个线程完成任务后调用countDown
方法将计数器减一,当计数器为零时,等待的线程可以继续执行。常用于多个线程完成各自任务后,再进行汇总或下一步操作的场景。CyclicBarrier
:让一组线程互相等待,直到所有线程都到达某个屏障点后,再一起继续执行。与CountDownLatch
不同的是,CyclicBarrier
可以重复使用,当所有线程都通过屏障后,计数器会重置,可以再次用于下一轮的等待。适用于多个线程需要协同工作,在某个阶段完成后再一起进入下一个阶段的场景。Semaphore
:信号量,用于控制同时访问某个资源的线程数量。它维护了一个许可计数器,线程在访问资源前需要获取许可,如果有可用许可,则获取成功并将许可计数器减一,否则线程需要等待,直到有其他线程释放许可。常用于控制对有限资源的访问,如数据库连接池、线程池中的线程数量等。
原子类:
- AtomicInteger:原子整数类,提供了对整数类型的原子操作,如自增、自减、比较并交换等。通过硬件级别的原子指令来保证操作的原子性和线程安全性,避免了使用锁带来的性能开销,在多线程环境下对整数进行计数、状态标记等操作非常方便。
- AtomicReference:原子引用类,用于对对象引用进行原子操作。可以保证在多线程环境下,对对象的更新操作是原子性的,即要么全部成功,要么全部失败,不会出现数据不一致的情况。常用于实现无锁数据结构或需要对对象进行原子更新的场景。
2. Java中有哪些常用的锁,在什么场景下使用?
-
synchronized:Java中的
synchronized
关键字是内置锁机制的基础,可以用于方法或代码块。当一个线程进入synchronized
代码块或方法时,它会获取关联对象的锁;当线程离开该代码块或方法时,锁会被释放。如果其他线程尝试获取同一个对象的锁,它们将被阻塞,直到锁被释放。其中,synchronzied加锁时有无锁、偏向锁、轻量级锁和重量级锁几个级别。偏向锁用于当一个线程进入同步块时,如果没有任何其他线程竞争,就会使用偏向锁,以减少锁的开销。轻量级锁使用线程栈上的数据结构,避免了操作系统级别的锁。重量级锁则涉及操作系统级的互斥锁。 -
ReentrantLock:
java.util.concurrent.locks.ReentrantLock
是一个显式的锁类,提供了比synchronized
更高级的功能,如可中断的锁等待、定时锁等待、公平锁选项等。ReentrantLock
使用lock()
和unlock()
方法来获取和释放锁。其中,公平锁按照线程请求锁的顺序来分配锁,保证了锁分配的公平性,但可能增加锁的等待时间。非公平锁不保证锁分配的顺序,可以减少锁的竞争,提高性能,但可能造成某些线程的饥饿。 -
读写锁(ReadWriteLock):
java.util.concurrent.locks.ReadWriteLock
接口定义了一种锁,允许多个读取者同时访问共享资源,但只允许一个写入者。读写锁通常用于读取远多于写入的情况,以提高并发性。 -
乐观锁和悲观锁:悲观锁(Pessimistic Locking)通常指在访问数据前就锁定资源,假设最坏的情况,即数据很可能被其他线程修改。
synchronized
和ReentrantLock
都是悲观锁的例子。乐观锁(Optimistic Locking)通常不锁定资源,而是在更新数据时检查数据是否已被其他线程修改。乐观锁常使用版本号或时间戳来实现。 -
自旋锁:自旋锁是一种锁机制,线程在等待锁时会持续循环检查锁是否可用,而不是放弃CPU并阻塞。通常可以使用CAS来实现。这在锁等待时间很短的情况下可以提高性能,但过度自旋会浪费CPU资源。
3. Java 并发工具你知道哪些?
Java中一些常用的并发工具,它们位于java.util.concurrent包中,常见的有:
- CountDownLatch:CountDownLatch是一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。它使用一个计数器进行初始化,调用countDown()方法会使计数器减一,当计数器的值减为0时,等待的线程会被唤醒。可以把它想象成一个倒计时器,当倒计时结束(计数器为0)时,等待的事件就会发生。
- 示例代码:
import java.util.concurrent.CountDownLatch;public class CountDownLatchExample {public static void main(String[] args) throws InterruptedException {int numberOfThreads = 3;CountDownLatch latch = new CountDownLatch(numberOfThreads);// 创建并启动三个工作线程for (int i = 0; i < numberOfThreads; i++) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + "正在工作");try {Thread.sleep(1000); // 模拟工作时间} catch (InterruptedException e) {e.printStackTrace();}latch.countDown(); // 完成工作,计数器减一System.out.println(Thread.currentThread().getName() + "完成工作");}).start();}System.out.println("主线程等待工作线程完成");latch.await(); // 主线程等待,直到计数器为0System.out.println("所有工作线程已完成,主线程继续执行");}
}
- CyclicBarrier:CyclicBarrier 允许一组线程互相等待,直到到达一个公共的屏障点。当所有线程都到达这个屏障点后,它们可以继续执行后续操作,并且这个屏障可以被重置循环使用。与CountDownLatch不同,CyclicBarrier 侧重于线程间的相互等待,而不是等待某些操作完成。
- 示例代码:
import java.util.concurrent.CyclicBarrier;public class CyclicBarrierExample {public static void main(String[] args) {int numberOfThreads = 3;CyclicBarrier barrier = new CyclicBarrier(numberOfThreads, () -> {System.out.println("所有线程都到达了屏障,继续执行后续操作");});for (int i = 0; i < numberOfThreads; i++) {new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + " 正在运行");Thread.sleep(1000); // 模拟运行时间barrier.await(); // 等待其他线程System.out.println(Thread.currentThread().getName() + " 已经通过屏障");} catch (Exception e) {e.printStackTrace();}}).start();}}
}
- Semaphore: Semaphore 是一个计数信号量,用于控制同时访问某个共享资源的线程数量。通过 acquire() 方法获取许可,使用 release() 方法释放许可。如果没有许可可用,线程将被阻塞,直到有许可被释放。可以用来限制对某些资源(如数据库连接池、文件操作等)的并发访问量。
- 代码如下:
import java.util.concurrent.Semaphore;public class SemaphoreExample {public static void main(String[] args) {Semaphore semaphore = new Semaphore(2); // 允许 2 个线程同时访问for (int i = 0; i < 5; i++) {new Thread(() -> {try {semaphore.acquire(); // 获取许可System.out.println(Thread.currentThread().getName() + " 获得了许可");Thread.sleep(2000); // 模拟资源使用System.out.println(Thread.currentThread().getName() + " 释放了许可");semaphore.release(); // 释放许可} catch (InterruptedException e) {e.printStackTrace();}}).start();}} }
- Future 和 Callable:Callable 是一个类似于 Runnable 的接口,但它可以返回结果,并且可以抛出异常。Future 用于表示一个异步计算的结果,可以通过它来获取callable 任务的执行结果或取消任务。
- ConcurrentHashMap:ConcurrentHashMap 是一个线程安全的哈希表,它允许多个线程同时进行读操作,在一定程度上支持并发的修改操作,避免了 HashMap 在多线程环境下需要使用
synchronized 或 Collections.synchronizedMap() 进行同步的性能问题。
4. 什么是 JMM?为什么需要 JMM?
Java 内存模型(Java Memory Model, JMM) 是 Java 虚拟机规范中定义的一种抽象内存访问规则。它规定了多线程环境下,线程如何通过内存进行交互,尤其是如何访问共享变量(如实例字段、静态变量)。JMM 的核心目标是屏蔽不同硬件和操作系统的内存访问差异,确保 Java 程序在所有平台上的并发行为具有一致性和可预测性。
1. 解决硬件内存差异问题
现代计算机为提升性能,采用 多级缓存(CPU Cache) 和 指令重排序优化。这导致:
-
缓存不一致:不同 CPU 核心的缓存中,同一共享变量的值可能不同。
-
指令执行乱序:代码编写顺序与实际执行顺序不一致。
JMM 通过定义happens-before
规则(如锁操作、volatile
写入等),强制同步内存状态,解决上述问题。
2. 提供跨平台一致性
不同硬件(如 x86 与 ARM)的内存模型差异巨大。JMM 抽象了底层细节,使开发者只需遵循 Java 层面的并发规则,无需关心底层实现,保证程序在所有支持 Java 的平台行为一致。
3. 明确多线程交互规范
若无 JMM,编译器和 JVM 可能过度优化(如激进的重排序),导致多线程程序出现不可预测的 Bug(如脏读、死锁)。JMM 通过约束编译器和 JVM 的行为,为开发者提供清晰的并发编程语义。
5 Java 内存区域和 JMM 有何区别?
Java 内存区域是 JVM 运行时数据区的物理划分,描述的是 JVM 进程内实际的内存分配结构(如堆、栈等)。
JMM(Java Memory Model) 是 Java 内存模型,定义的是多线程环境下共享变量访问的抽象规则(如可见性、有序性),属于并发编程的规范,与物理内存无关。
6. voliatle关键字有什么作用?
volatile 作用有 2 个:
-
保证变量对所有线程的可见性:当一个变量被声明为 volatile 时,它会保证对这个变量的写操作会立即刷新到主存中,而对这个变量的读操作会直接从主存中读取,从而确保了多线程环境下对该变量访问的可见性。这意味着一个线程修改了 volatile 变量的值,其他线程能够立刻看到这个修改,不会受到各自线程工作内存的影响。
-
禁止指令重排序优化:volatile 关键字在 Java 中主要通过内存屏障来禁止特定类型的指令重排序。
- 写 - 写(Write - Write)屏障:在对 volatile 变量执行写操作之前,会插入一个写屏障。这确保了在该变量写操作之前的所有普通写操作都已完成,防止了这些写操作被移到 volatile 写操作之后。
- 读 - 写(Read - Write)屏障:在对 volatile 变量执行读操作之后,会插入一个读屏障。它确保了对 volatile 变量的读操作之后的所有普通读操作都不会被提前到 volatile 读之前执行,保证了读取到的数据是最新的。
- 写 - 读(Write - Read)屏障:这是最重要的一个屏障,它发生在 volatile 写之后和 volatile 读之前。这个屏障确保了 volatile 写操作之前的所有内存操作(包括写操作)都不会被重排序到 volatile 读之后,同时也确保了 volatile 读操作之后的所有内存操作(包括读操作)都不会被重排序到 volatile 写之前。
7. volatile 可以保证原子性么?
volatile 不能保证原子性,它只能保证可见性和有序性。
-
原子性:指一个操作是不可中断的整体,要么全部执行成功,要么完全不执行。
-
volatile 的局限:
-
仅能保证单次读/写操作的原子性(如
volatile int a = 1;
的读写是原子的)。 -
无法保证复合操作的原子性,例如
i++
(实际是read-modify-write
三步操作):
-
volatile int i = 0;
i++; // 非原子操作!包含:读取 i → 计算 i+1 → 写回新值
多线程并发执行时,可能出现值覆盖,导致最终结果小于预期。
8. 指令重排序的原理是什么?
在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序,但是重排序要满足下面2个条件才能进行:
- 在单线程环境下不能改变程序运行的结果。
- 存在数据依赖关系的不允许重排序。
所以重排序不会对单线程有影响,只会破坏多线程的执行语义。
我们看这个例子,A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面,如果C排到A和B的前面,那么程序的结果将会被改变。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。
9. 悲观锁和乐观锁的区别?
- 乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
- 悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像 synchronized,不管三七二十一,直接上了锁就操作资源了。
10. Java中想实现一个乐观锁,都有哪些方式?
- CAS(Compare and Swap)操作:CAS 是乐观锁的基础。Java 提供了
java.util.concurrent.atomic
包,包含各种原子变量类(如AtomicInteger
、AtomicLong
),这些类使用 CAS 操作实现了线程安全的原子操作,可以用来实现乐观锁。 - 版本号控制:增加一个版本号字段记录数据更新时候的版本,每次更新时递增版本号。在更新数据时,同时比较版本号,若当前版本号和更新前获取的版本号一致,则更新成功,否则失败。
- 时间戳:使用时间戳记录数据的更新时间,在更新数据时,在比较时间戳。如果当前时间戳大于数据的时间戳,则说明数据已经被其他线程更新,更新失败。
11. synchronized和reentrantlock的工作原理及其应用场景?
synchronized
是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized
原理
synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
在执行monitorenter
时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
对象锁的拥有者线程才可以执行 monitorexit
指令来释放锁。在执行 monitorexit
指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。
如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println("synchronized 代码块");}} }
synchronized
同步语句块的实现使用的是monitorenter
和monitorexit
指令,其中monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。public class SynchronizedDemo2 {public synchronized void method() {System.out.println("synchronized 方法");} }
synchronized
修饰的方法并没有monitorenter
指令和monitorexit
指令,取而代之的是ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。
reentrantlock原理
ReentrantLock 的底层实现主要依赖于 AbstractQueuedSynchronizer(AQS)这个抽象类。AQS 是一个提供了基本同步机制的框架,其中包括了队列、状态值等。
ReentrantLock 在 AQS 的基础上通过内部类 Sync 来实现具体的锁操作。不同的 Sync 子类实现了公平锁和非公平锁的不同逻辑:
- 可中断性:ReentrantLock 实现了可中断性,这意味着线程在等待锁的过程中,可以被其他线程中断而提前结束等待。在底层,ReentrantLock 使用了与 LockSupport.park() 和 LockSupport.unpark() 相关的机制来实现可中断性。
- 设置超时时间:ReentrantLock 支持在尝试获取锁时设置超时时间,即等待一定时间后如果还未获得锁,则放弃锁的获取。这是通过内部的 tryAcquireNanos 方法来实现的。
- 公平锁和非公平锁:在直接创建 ReentrantLock 对象时,默认情况下是非公平锁。公平锁是按照线程等待的顺序来获取锁,而非公平锁则允许多个线程在同一时刻竞争锁,不考虑它们申请锁的顺序。公平锁可以通过在创建 ReentrantLock 时传入 true 来设置,例如:
ReentrantLock fairLock = new ReentrantLock(true);
- 多个条件变量:ReentrantLock 支持多个条件变量,每个条件变量可以与一个 ReentrantLock 关联。这使得线程可以更灵活地进行等待和唤醒操作,而不仅仅是基于对对象监视器的 wait() 和 notify()。多个条件变量的实现依赖于 Condition 接口,例如:
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 使用下面方法进行等待和唤醒
condition.await();
condition.signal();
- 可重入性:ReentrantLock 支持可重入性,即同一个线程可以多次获得同一把锁,而不会造成死锁。这是通过内部的 holdCount 计数来实现的。当一个线程多次获取锁时,holdCount 递增,释放锁时递减,只有当 holdCount 为零时,其他线程才有机会获取锁。
synchronized应用场景:
- 简单同步需求:当你需要对代码块或方法进行简单的同步控制时,synchronized是一个很好的选择。它使用起来简单,不需要额外的资源管理,因为锁会在方法退出或代码块执行完毕后自动释放。
- 代码块同步:如果你想对特定代码段进行同步,而不是整个方法,可以使用synchronized代码块。这可以让你更精细地控制同步的范围,从而减少锁的持有时间,提高并发性能。
- 内置锁的使用:synchronized关键字使用对象的内置锁(也称为监视器锁),这在需要使用对象作为锁对象的情况下很有用,尤其是在对象状态与锁保护的代码紧密相关时。
ReentrantLock应用场景:
- 高级锁功能需求:ReentrantLock提供了synchronized所不具备的高级功能,如公平锁、响应中断、定时锁尝试、以及多个条件变量。当你需要这些功能时,ReentrantLock是更好的选择。
- 性能优化:在高度竞争的环境中,ReentrantLock可以提供比synchronized更好的性能,因为它提供了更细粒度的控制,如尝试锁定和定时锁定,可以减少线程阻塞的可能性。
- 复杂同步结构:当你需要更复杂的同步结构,如需要多个条件变量来协调线程之间的通信时,ReentrantLock及其配套的Condition对象可以提供更灵活的解决方案。
综上,synchronized适用于简单同步需求和不需要额外锁功能的场景,而ReentrantLock适用于需要更高级锁功能、性能优化或复杂同步逻辑的情况。选择哪种同步机制取决于具体的应用需求和性能考虑。
12. 除了用synchronized,还有什么方法可以实现线程同步?
- 使用
ReentrantLock
类:ReentrantLock
是一个可重入的互斥锁,相比synchronized
提供了更灵活的锁定和解锁操作。它还支持公平锁和非公平锁,以及可以响应中断的锁获取操作。 - 使用
volatile
关键字:虽然volatile
不是一种锁机制,但它可以确保变量的可见性。当一个变量被声明为volatile
后,线程将直接从主内存中读取该变量的值,这样就能保证线程间变量的可见性。但它不具备原子性。 - 使用
Atomic
类:Java提供了一系列的原子类,例如AtomicInteger
、AtomicLong
、AtomicReference
等,用于实现对单个变量的原子操作,这些类在实现细节上利用了CAS(Compare-And-Swap)算法,可以用来实现无锁的线程安全。
13. synchronized锁静态方法和普通方法区别?
锁的对象不同:
- 普通方法: 锁的是当前对象实例(this)。同一对象实例的
synchronized
普通方法,同一时间只能被一个线程访问;不同对象实例间互不影响,可被不同线程同时访问各自的同步普通方法。 - 静态方法: 锁的是当前类的
Class
对象。由于类的Class
对象全局唯一,无论多少个对象实例,该静态同步方法同一时间只能被一个线程访问。
作用范围不同:
- 普通方法: 仅对同一对象实例的同步方法调用互斥,不同对象实例的同步普通方法可并行执行。
- 静态方法:对整个类的所有实例的该静态方法调用都互斥,一个线程进入静态同步方法,其他线程无法进入同一类任何实例的该方法。
多实例场景影响不同:
- 普通方法: 多线程访问不同对象实例的同步普通方法时,可同时执行。
- 静态方法: 不管有多少对象实例,同一时间仅一个线程能执行该静态同步方法。
14. sychronized和volatile比较?
synchronized
关键字和 volatile
关键字是两个互补的存在,而不是对立的存在!
volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好 。但是volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块 。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性。
15. synchronized和reentrantlock区别?
synchronized 和 ReentrantLock 都是 Java 中提供的可重入锁:
- 用法不同:synchronized 可用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用在代码块上。
- 获取锁和释放锁方式不同:synchronized 会自动加锁和释放锁,当进入 synchronized 修饰的代码块之后会自动加锁,当离开 synchronized 的代码段之后会自动释放锁。而 ReentrantLock 需要手动加锁和释放锁
- 锁类型不同:synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。
- 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
- 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。
16. 怎么理解可重入锁?
可重入锁是指同一个线程在获取了锁之后,可以再次重复获取该锁而不会造成死锁或其他问题。当一个线程持有锁时,如果再次尝试获取该锁,就会成功获取而不会被阻塞。
ReentrantLock实现可重入锁的机制是基于线程持有锁的计数器。
·当一个线程第一次获取锁时,计数器会加1,表示该线程持有了锁。在此之后,如果同一个线程再次获取锁,计数器会再次加1。每次线程成功获取锁时,都会将计数器加1。 ·当线程释放锁时,计数器会相应地减1。只有当计数器减到0时,锁才会完全释放,其他线程才有机会获取锁。
这种计数器的设计使得同一个线程可以多次获取同一个锁,而不会造成死锁或其他问题。每次获取锁时,计数器加1;每次释放锁时,计数器减1。只有当计数器减到0时,锁才会完全释放。
ReentrantLock通过这种计数器的方式,实现了可重入锁的机制。它允许同一个线程多次获取同一个锁,并且能够正确地处理锁的获取和释放,避免了死锁和其他并发问题。
17. synchronized 支持重入吗?如何实现的?
synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
synchronized底层是利用计算机系统mutex Lock实现的。每一个可重入锁都会关联一个线程ID和一个锁状态status。
当一个线程请求方法时,会去检查锁状态。
- 如果锁状态是0,代表该锁没有被占用,使用CAS操作获取锁,将线程ID替换成自己的线程ID。
- 如果锁状态不是0,代表有线程在访问该方法。此时,如果线程ID是自己的线程ID,如果是可重入锁,会将status自增1,然后获取到该锁,进而执行相应的方法;如果是非重入锁,就会进入阻塞队列等待。
在释放锁时,
- 如果是可重入锁的,每一次退出方法,就会将status减1,直至status的值为0,最后释放该锁。
- 如果非可重入锁的,线程退出方法,直接就会释放该锁。
18. Synchronized锁升级的过程讲一下?
具体的锁升级的过程是:无锁->偏向锁->轻量级锁->重量级锁。
- 无锁:这是没有开启偏向锁的时候的状态,在JDK1.6之后偏向锁的默认开启的,但是有一个偏向延迟,需要在JVM启动之后的多少秒之后才能开启,这个可以通过JVM参数进行设置,同时是否开启偏向锁也可以通过JVM参数设置。
- 偏向锁:这个是在偏向锁开启之后的锁的状态,如果还没有一个线程拿到这个锁的话,这个状态叫做匿名偏向,当一个线程拿到偏向锁的时候,下次想要竞争锁只需要拿线程ID跟MarkWord当中存储的线程ID进行比较,如果线程ID相同则直接获取锁(相当于锁偏向于这个线程),不需要进行CAS操作和将线程挂起的操作。
- 轻量级锁:在这个状态下线程主要是通过CAS操作实现的。将对象的MarkWord存储到线程的虚拟机栈上,然后通过CAS将对象的MarkWord的内容设置为指向Displaced Mark Word的指针,如果设置成功则获取锁。在线程出临界区的时候,也需要使用CAS,如果使用CAS替换成功则同步成功,如果失败表示有其他线程在获取锁,那么就需要在释放锁之后将被挂起的线程唤醒。
- 重量级锁:当有两个以上的线程获取锁的时候轻量级锁就会升级为重量级锁,因为CAS如果没有成功的话始终都在自旋,进行while循环操作,这是非常消耗CPU的,但是在升级为重量级锁之后,线程会被操作系统调度然后挂起,这可以节约CPU资源。
线程A进入 synchronized 开始抢锁,JVM 会判断当前是否是偏向锁的状态,如果是就会根据 Mark Word中存储的线程 ID 来判断,当前线程A是否就是持有偏向锁的线程。如果是,则忽略 check,线程A直接执行临界区内的代码。
但如果 Mark Word 里的线程不是线程 A,就会通过自旋尝试获取锁,如果获取到了,就将 Mark Word 中的线程 ID 改为自己的;如果竞争失败,就会立马撤销偏向锁,膨胀为轻量级锁。
后续的竞争线程都会通过自旋来尝试获取锁,如果自旋成功那么锁的状态仍然是轻量级锁。然而如果竞争失败,锁会膨胀为重量级锁,后续等待的竞争的线程都会被阻塞。
19. JVM对Synchornized的优化?
synchronized 核心优化方案主要包含以下4个:
- 锁膨胀:synchronized从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫做锁膨胀也叫做锁升级。JDK1.6之前,synchronized是重量级锁,也就是说synchronized在释放和获取锁时都会从用户态转换成内核态,而转换的效率是比较低的。但有了锁膨胀机制之后,synchronized的状态就多了无锁、偏向锁以及轻量级锁了,这时候在进行并发操作时,大部分的场景都不需要用户态到内核态的转换了,这样就大幅的提升了synchronized的性能。
- 锁消除:指的是在某些情况下,JVM虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而底提高程序性能的目的。
- 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
- 自适应自旋锁:指通过自身循环,尝试获取锁的一种方式,优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。
20. 什么是公平锁和非公平锁?
- 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
- 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
21. 非公平锁吞吐量为什么比公平锁大?
- 公平锁执行流程:获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
- 非公平锁执行流程:当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。
22. ThreadLocal 有什么用?
ThreadLocal 提供线程私有的变量存储空间。每个线程通过 ThreadLocal 访问自身独立的变量副本,实现线程间数据隔离。
当你创建一个 ThreadLocal
变量时,每个访问该变量的线程都会拥有一个独立的副本。这也是 ThreadLocal
名称的由来。线程可以通过 get()
方法获取自己线程的本地副本,或通过 set()
方法修改该副本的值,从而避免了线程安全问题。
23. ThreadLocal 原理了解吗?
Thread
类源代码
public class Thread implements Runnable {//......//与此线程有关的ThreadLocal值。由ThreadLocal类维护ThreadLocal.ThreadLocalMap threadLocals = null;//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;//......
}
从上面Thread
类 源代码可以看出Thread
类中有一个 threadLocals
和 一个 inheritableThreadLocals
变量,它们都是 ThreadLocalMap
类型的变量,我们可以把 ThreadLocalMap
理解为ThreadLocal
类实现的定制化的 HashMap
。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal
类的 set
或get
方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap
类对应的 get()
、set()
方法。
ThreadLocal
类的set()
方法
public void set(T value) {//获取当前请求的线程Thread t = Thread.currentThread();//取出 Thread 类内部的 threadLocals 变量(哈希表结构)ThreadLocalMap map = getMap(t);if (map != null)// 将需要存储的值放入到这个哈希表中map.set(this, value);elsecreateMap(t, value);
}
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}
通过上面这些内容,我们足以通过猜测得出结论:最终的变量是放在了当前线程的 ThreadLocalMap
中,并不是存在 ThreadLocal
上,ThreadLocal
可以理解为只是ThreadLocalMap
的封装,传递了变量值。 ThrealLocal
类中可以通过Thread.currentThread()
获取到当前线程对象后,直接通过getMap(Thread t)
可以访问到该线程的ThreadLocalMap
对象。
每个Thread
中都具备一个ThreadLocalMap
,而ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//......
}
比如我们在同一个线程中声明了两个 ThreadLocal
对象的话, Thread
内部都是使用仅有的那个ThreadLocalMap
存放数据的,ThreadLocalMap
的 key 就是 ThreadLocal
对象,value 就是 ThreadLocal
对象调用set
方法设置的值。
ThreadLocal
数据结构如下图所示:
24. ThreadLocal 内存泄露问题是怎么导致的?
ThreadLocal
内存泄漏的根本原因在于其内部实现机制。
每个线程维护一个名为 ThreadLocalMap
的 map。 当你使用 ThreadLocal
存储值时,实际上是将值存储在当前线程的 ThreadLocalMap
中,其中 ThreadLocal
实例本身作为 key,而你要存储的值作为 value。
ThreadLocal
的 set()
方法源码如下:
public void set(T value) {Thread t = Thread.currentThread(); // 获取当前线程ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMapif (map != null) {map.set(this, value); // 设置值} else {createMap(t, value); // 创建新的 ThreadLocalMap}
}
ThreadLocalMap
的 set()
和 createMap()
方法中,并没有直接存储 ThreadLocal
对象本身,而是使用 ThreadLocal
的哈希值计算数组索引,最终存储于类型为static class Entry extends WeakReference<ThreadLocal<?>>
的数组中。
int i = key.threadLocalHashCode & (len-1);
ThreadLocalMap
的 Entry
定义如下:
static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
ThreadLocalMap 的 key 和 value 引用机制:
- key 是弱引用:ThreadLocalMap 中的 key 是 ThreadLocal 的弱引用(WeakReference<ThreadLocal<?>>)。这意味着,如果 ThreadLocal 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 ThreadLocalMap 中对应的 key 变为 null。
- value 是强引用:即使 key 被 GC 回收,value 仍然被 ThreadLocalMap.Entry 强引用存在,无法被 GC 回收。
当 ThreadLocal 实例失去强引用后,其对应的 value 仍然存在于 ThreadLocalMap 中,因为 Entry 对象强引用了它。如果线程持续存活(例如线程池中的线程),ThreadLocalMap 也会一直存在,导致 key 为 null 的 entry 无法被垃圾回收,即会造成内存泄漏。
也就是说,内存泄漏的发生需要同时满足两个条件:
- ThreadLocal 实例不再被强引用;
- 线程持续存活,导致 ThreadLocalMap 长期存在。
虽然 ThreadLocalMap 在 get()、set()和 remove()操作时会尝试清理 key 为 null 的 entry,但这种清理机制是被动的,并不完全可靠。
如何避免内存泄漏的发生?
- 在使用完 ThreadLocal 后,务必调用 remove()方法。这是最安全和最推荐的做法。remove()方法会从 ThreadLocalMap 中显式地移除对应的 entry,彻底解决内存泄漏的风险。即使将 ThreadLocal 定义为 static final,也强烈建议在每次使用后调用 remove()。
- 在线程池等线程复用的场景下,使用 try - finally 块可以确保即使发生异常,remove()方法也一定会被执行。
25. 如何跨线程传递 ThreadLocal 的值?
由于 ThreadLocal
的变量值存放在 Thread
里,而父子线程属于不同的 Thread
的。因此在异步场景下,父子线程的 ThreadLocal
值无法进行传递。
如果想要在异步场景下传递 ThreadLocal
值,有两种解决方案:
InheritableThreadLocal
:InheritableThreadLocal
是 JDK1.2 提供的工具,继承自ThreadLocal
。使用InheritableThreadLocal
时,会在创建子线程时,令子线程继承父线程中的ThreadLocal
值,但是无法支持线程池场景下的ThreadLocal
值传递。TransmittableThreadLocal
:TransmittableThreadLocal
(简称 TTL) 是阿里巴巴开源的工具类,继承并加强了InheritableThreadLocal
类,可以在线程池的场景下支持ThreadLocal
值传递。
InheritableThreadLocal
原理:
InheritableThreadLocal
实现了创建异步线程时,继承父线程 ThreadLocal
值的功能。该类是 JDK 团队提供的,通过改造 JDK 源码包中的 Thread
类来实现创建线程时,ThreadLocal
值的传递。
InheritableThreadLocal
的值存储在哪里?
在 Thread
类中添加了一个新的 ThreadLocalMap
,命名为 inheritableThreadLocals
,该变量用于存储需要跨线程传递的 ThreadLocal
值。如下:
class Thread implements Runnable {ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
如何完成 ThreadLocal
值的传递?
通过改造 Thread
类的构造方法来实现,在创建 Thread
线程时,拿到父线程的 inheritableThreadLocals
变量赋值给子线程即可。相关代码如下:
// Thread 的构造方法会调用 init() 方法
private void init(/* ... */) {// 1、获取父线程Thread parent = currentThread();// 2、将父线程的 inheritableThreadLocals 赋值给子线程if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
26. ThreadLocalMap.set()原理?
参考之前的博客
【JUC】并发编程重点知识——ThreadLocal-CSDN博客
27. ThreadLocal使用场景?
参考之前的博客
【JUC】并发编程重点知识——ThreadLocal-CSDN博客
28. 介绍一下AQS?
AQS (AbstractQueuedSynchronizer
,抽象队列同步器)是从 JDK1.5 开始提供的 Java 并发核心组件。
AQS 解决了开发者在实现同步器时的复杂性问题。它提供了一个通用框架,用于实现各种同步器,例如 可重入锁(ReentrantLock
)、信号量(Semaphore
)和 倒计时器(CountDownLatch
)。通过封装底层的线程同步机制,AQS 将复杂的线程管理逻辑隐藏起来,使开发者只需专注于具体的同步逻辑。
简单来说,AQS 是一个抽象类,为同步器提供了通用的 执行框架。它定义了 资源获取和释放的通用流程,而具体的资源获取逻辑则由具体同步器通过重写模板方法来实现。 因此,可以将 AQS 看作是同步器的 基础“底座”,而同步器则是基于 AQS 实现的 具体“应用”。
29. AQS 的原理是什么?
AQS最核心的就是三大部分:
- 状态:state;
- 控制线程抢锁和配合的FIFO队列(双向链表);
- 期望协作工具类去实现的获取/释放等重要方法(重写)。
参考【JUC】并发编程重点知识——AQS与常见同步工具类
30. CAS 和 AQS 有什么关系?
CAS和AQS两者的区别:
- CAS 是一种乐观锁机制,它包含三个操作数:内存位置(V)、预期值(A)和新值(B)。CAS 操作的逻辑是,如果内存位置 V 的值等于预期值 A,则将其更新为新值 B,否则不做任何操作。整个过程是原 子性的,通常由硬件指令支持,如在现代处理器上,cmpxchg 指令可以实现 CAS 操作。
- AQS 是一个用于构建锁和同步器的框架,许多同步器如 Reentrant Lock、Semaphore、CountDownLatch 等都是基于 AQS 构建的。AQS 使用一个 volatile 的整数变量 state 来表示同步状态,通过内置的 FIFO 队列来管理等待线程。它提供了一些基本的操作,如 acquire(获取资源)和 release(释放资源),这些操作会修改 state 的值,并根据 state 的值来判断线程是否可以获取或释放资源。AQS 的 acquire 操作通常会先尝试获取资源,如果失败,线程将被添加到等待队列中,并阻塞等待。release 操作会释放资源,并唤醒等待队列中的线程。
CAS和AQS两者的联系:
- CAS 为 AQS 提供原子操作支持:AQS 内部使用 CAS 操作来更新 state 变量,以实现线程安全的状态修改。在 acquire 操作中,当线程尝试获取资源时,会使用 CAS 操作尝试将 state 从一个值更新为另一个值,如果更新失败,说明资源已被占用,线程会进入等待队列。在 release 操作中,当线程释放资源时,也会使用 CAS 操作将 state 恢复到相应的值,以保证状态更新的原子性。
31. 如何用 AQS 实现一个可重入的公平锁?
AQS 实现一个可重入的公平锁的详细步骤:
- 继承 AbstractQueuedSynchronizer:创建一个内部类继承自 AbstractQueuedSynchronizer,重写 tryAcquire、tryRelease、isHeldExclusively 等方法,这些方法将用于实现锁的获取、释放和判断锁是否被当前线程持有。
- 实现可重入逻辑:在 tryAcquire 方法中,检查当前线程是否已经持有锁,如果是,则增加锁的持有次数(通过 state 变量);如果不是,尝试使用 CAS 操作来获取锁。
- 实现公平性:在 tryAcquire 方法中,按照队列顺序来获取锁,即先检查等待队列中是否有线程在等待,如果有,当前线程必须进入队列等待,而不是直接竞争锁。
- 创建锁的外部类:创建一个外部类,内部持有 AbstractQueuedSynchronizer 的子类对象,并提供 lock 和 unlock 方法,这些方法将调用 AbstractQueuedSynchronizer 子类中的方法。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;public class FairReentrantLock {private static class Sync extends AbstractQueuedSynchronizer {// 判断锁是否被当前线程持有protected boolean isHeldExclusively() {return getExclusiveOwnerThread() == Thread.currentThread();}// 尝试获取锁protected boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 公平性检查:检查队列中是否有前驱节点,如果有,则当前线程不能获取锁if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}} else if (current == getExclusiveOwnerThread()) {// 可重入逻辑:如果是当前线程持有锁,则增加持有次数int nextc = c + acquires;if (nextc < 0) {throw new Error("Maximum lock count exceeded");}setState(nextc);return true;}return false;}// 尝试释放锁protected boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread()) {throw new IllegalMonitorStateException();}boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}// 提供一个条件变量,用于实现更复杂的同步需求,这里只是简单实现ConditionObject newCondition() {return new ConditionObject();}}private final Sync sync = new Sync();// 加锁方法public void lock() {sync.acquire(1);}// 解锁方法public void unlock() {sync.release(1);}// 判断当前线程是否持有锁public boolean isLocked() {return sync.isHeldExclusively();}// 提供一个条件变量,用于实现更复杂的同步需求,这里只是简单实现public Condition newCondition() {return sync.newCondition();}
}
代码解释:
内部类 Sync:
- isHeldExclusively:使用
getExclusiveOwnerThread
方法检查当前锁是否被当前线程持有。 tryAcquire
:- 首先获取当前锁的状态
c
。 - 如果
c
为0,表示锁未被持有,此时进行公平性检查, 通过hasQueuedPredecessors
检查是否有前驱节点在等待队列中。如果没有,使用compareAndSetState
尝试将状态设置为acquires
(通常为1),并设置当前线程为锁的持有线程。 - 如果
c
不为0,说明锁已被持有,检查是否为当前线程持有。如果是,增加锁的持有次数(可重入),但要防止溢出。
- 首先获取当前锁的状态
tryRelease
:- 先将状态减
releases
(通常为1)。 - 检查当前线程是否为锁的持有线程,如果不是,抛出异常。
- 如果状态减为0,说明锁被完全释放,将持有线程设为
null
。
- 先将状态减
- newCondition:创建一个
ConditionObject
用于更复杂的同步操作,如等待 / 通知机制。
外部类 FairReentrantLock:
lock
方法:调用sync.acquire(1)
尝试获取锁。unlock
方法:调用sync.release(1)
释放锁。isLocked
方法:调用sync.isHeldExclusively
判断锁是否被当前线程持有。newCondition
方法:调用sync.newCondition
提供条件变量。
32. CAS 有什么缺点?
CAS的缺点主要有3点:
-
ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
-
循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。
-
只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。
33. 为什么不能所有的锁都用CAS?
CAS操作是基于循环重试的机制,如果CAS操作一直未能成功,线程会一直自旋重试,占用CPU资源。在高并发情况下,大量线程自旋会导致CPU资源浪费。
34. CAS 有什么问题,Java是怎么解决的?
会有 ABA 的问题,变量值在操作过程中先被其他线程从 A 修改为 B,又被改回 A,CAS 无法感知中途变化,导致操作误判为“未变更”。比如:
- 线程1读取变量为 A,准备改为 C。
- 此时线程2将变量 A → B → A。
- 线程1的CAS执行时发现仍是 A,但状态已丢失中间变化。
Java 提供的工具类会在 CAS 操作中增加版本号(Stamp)或标记,每次修改都更新版本号,使得即使值相同也能识别变更历史。比如,可以用 AtomicStampedReference 来解决 ABA 问题,通过比对值和版本号识别ABA问题。
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);// 尝试修改值并更新版本号
boolean success = ref.compareAndSet(100, 200, 0, 1);
// 前提:当前值=100 且 版本号=0,才会更新为 (200,1)
35. Semaphore、CountDownLatch 、CyclicBarrier是做什么的讲一讲?
1. Semaphore(信号量)
核心用途:控制同时访问特定资源的线程数量,常用于资源池限流或互斥场景。
核心机制:
-
维护一组 "许可证"(permits)。线程通过
acquire()
申请许可证(若无许可则阻塞),通过release()
释放许可证。 -
初始化时指定许可数量(如
new Semaphore(5)
表示最多允许 5 个线程并发访问)。 -
支持公平/非公平模式,可重入(同一线程可多次获取许可)。
典型场景:数据库连接池限流、限制文件并发写入数。
2. CountDownLatch(倒计时器)
核心用途:让一个或多个线程等待其他线程完成操作,强调“单次等待”。
核心机制:
-
初始化时设置计数器(如
new CountDownLatch(3)
)。 -
其他线程调用
countDown()
减少计数器(非阻塞);主线程调用await()
阻塞等待计数器归零。 -
计数器不可重置,一次使用后失效。
典型场景:主线程等待所有子任务初始化完成后再执行;多线程任务完成后触发汇总操作。
3. CyclicBarrier(循环屏障)
核心用途:让一组线程相互等待,达到屏障点后统一执行后续操作,支持重用。
核心机制:
-
初始化时指定参与线程数(如
new CyclicBarrier(4)
)及可选Runnable
屏障动作(所有线程到达后触发)。 -
线程调用
await()
时阻塞,直到所有线程都到达屏障点,屏障才会打开。 -
计数器可自动重置,支持重复使用(区别于 CountDownLatch)。
典型场景:多阶段计算(如并行计算分阶段同步);模拟多玩家游戏回合等待。