Java并发编程面试总结(上)
Java里的线程和操作系统的线程是一样的吗?
Java里的线程和操作系统的线程并不完全一样,主流的 JVM 采用的是1:1 线程模型,也就是说,每个 Java 线程都会映射到一个底层的操作系统线程。不过,Java 线程并不等同于操作系统线程,它其实是对操作系统线程进行了一层抽象和封装。
- 抽象层面不同:
- 属于 Java 语言层面的概念,由 JVM 负责管理,通过
Thread
类或者Runnable
接口就能创建。 - 是操作系统内核层面的概念,其创建、调度以及销毁都由操作系统负责。
- 属于 Java 语言层面的概念,由 JVM 负责管理,通过
- 调度机制不同:
- 调度是由 JVM 按照 Java 语言规范来进行的,不过最终还是要依赖操作系统的调度器来执行线程。
- 调度直接由操作系统内核完成,会受到硬件资源以及操作系统策略的影响。
- 资源消耗不同:
- 建和销毁的成本相对较低,因为 JVM 可以对线程进行复用,比如使用线程池。
- 创建和切换的开销较大,这是因为需要进行内核态与用户态的切换。
- 平台独立性:
- 具备 “一次编写,到处运行” 的特点,能够在不同操作系统上保持一致的编程接口。
- 不同的操作系统提供的线程 API 各不相同,例如 Windows 使用
CreateThread
,Linux 使用pthread_create
。
使用多线程要注意哪些问题?
-
原子性:
当多个线程同时访问或者修改共享资源时,因执行顺序的不确定导致数据不一致,此时需要通过互斥锁
synchronized
或原子类AtomicInteger
(通过CAS+volatile)来确保同一时刻只有一个线程操作资源。 -
内存可见性:
一个线程对共享变量的修改,可能无法及时被其他线程看到,这就是内存可见性问题。通过使用
volatile
关键字保证变量的可见性。借助synchronized
或Lock
来保证内存刷新。借助
synchronized
或Lock
来保证内存刷新?两者都会触发内存屏障
synchronized:进入同步块,刷新本地缓存,从主内存中读取最新值,退出同步块,将更新后的值刷新到主内存
ReentrantLock:获取锁,清空工作内存,从主内存读取最新值。释放锁,将工作内存的修改刷新到主内存。
-
指令有序性:
编译器或者处理器为了优化性能,可能会对指令进行重排序,从而导致程序行为异常。同样使用
volatile
关键字禁止指令重排序。利用synchronized
确保代码块作为原子操作执行。
为什么CAS能保证线程安全?
如何保持数据一致性?
-
事务管理:使用数据库事务来确保一组数据库操作要么全部成功提交,要么全部失败回滚。通过ACID(原子性、一致性、隔离性、持久性)属性,数据库事务可以保证数据的一致性。
-
锁机制:使用锁来实现对共享资源的互斥访问。在 Java 中,可以使用 synchronized 关键字、ReentrantLock 或其他锁机制来控制并发访问,从而避免并发操作导致数据不一致。
-
版本控制:通过乐观锁的方式,在更新数据时记录数据的版本信息,从而避免同时对同一数据进行修改,进而保证数据的一致性。
线程的创建方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口与FutureTask
- 使用线程池(Executor框架)
如何让线程停止?
Thread.stop()
强制终止线程,可能导致资源无法释放、数据不一致(如锁未释放)。- 当线程在执行
sleep()
、wait()
、join()
等阻塞操作时,调用interrupt()
会触发InterruptedException
来停止线程。 - 使用
isInterrupted()
自行检查标志是否需要停止,当线程调用interrupt()
方法时会使线程的中断标志变为true。每个线程的中断标志位是独立的,当阻塞方法被打断时,需要抛出异常才会清除标志位。 - 通过由volatile关键字修饰的共享标志位主动退出,定义一个可见的状态变量,有主线程控制其值,工作线程循环检测该变量判断是否需要退出。
- 通过Future取消任务,使用线程池提交任务,并通过
Future.cancel
停止线程。依赖中断机制。???
Java线程的状态有哪些?
-
NEW:尚未启动的线程状态,即线程创建,还未调用start方法
-
RUNNABLE:就绪状态(已经调用start,进入就绪队列等待,但未被分配CPU时间片)+正在运行
-
BLOCKED:仅指等待内置锁(synchronized)的状态,等待显示锁(如ReentrantLock)会进入WAITING/TIMED_WAITING 状态
-
WAITING:等待状态的线程正在等待另一线程执行特定的操作
-
TIMED_WAITING:具有指定等待时间的等待状态
-
TERMINATED:线程完成执行或者出现异常,终止状态
调用interrupt是如何让线程抛出异常的?
-
当线程处于以下阻塞状态时,中断标志会被忽略,转而抛出
InterruptedException
并清除标志:Thread.sleep()Object.wait() Thread.join()LockSupport.park()BlockingQueue.take()
-
中断标志位:
thread.interrupt()
:将标志设为true
。Thread.interrupted()
:返回当前标志并清除(设为false
)。thread.isInterrupted()
:返回当前标志不清除。
当线程阻塞时,阻塞线程调用interrupt()方法会将中断标志设为true,此时的阻塞线程依然会抛出打断异常,原因同第一点,而标志位的作用仅仅是阻塞线程在catch里依据标志位来决定是否执行后续逻辑。
注意点
当线程处于阻塞状态(如sleep()
、wait()
)时,调用interrupt()
会使中断标志位短暂变为true
,但 JVM 会立即检测到线程阻塞并清除标志位(设为false
),同时唤醒线程并抛出InterruptedException
。若需后续逻辑继续响应中断,必须手动恢复标志位。
sleep和wait的区别?
- 所属类不同:sleep是
Thread
类的静态方法。可以在任何地方通过Thread.sleep()
调用;wait属于Object
类的实例方法,必须通过对象实例来调用。 - 释放锁的情况不同:
Thread.sleep()
被调用时,线程暂停执行指定的时间,但不会释放持有的对象锁,其他线程无法获得该线程持有的锁。Object.wait()
被调用时,线程会释放对象锁,其他线程能够获取对象锁并执行同步代码块。 - 唤醒机制不同:sleep在休眠指定时间后会自动唤醒,wait线程需要通过其他线程调用同一锁对象的notify或者notifyall方法来唤醒。
- 使用条件不同:sleep可在任意位置调用,无需事先获取锁。wait必须在同步块或同步方法内调用(即线程需持有该对象的锁),否则抛出IllegalMonitorStateException。
sleep会释放CPU吗?
会释放CPU,但不会释放持有对象锁,线程会进入TIMED_WAITING
状态,此时操作系统会触发调度,将CPU分配给其他已就绪状态的线程。
注意点
若t1和t2线程竞争同一把锁,若t1持有锁并未释放,t2就永远无法处于就绪态。
blocked和waiting有啥区别
触发条件
BLOCKED状态是线程尝试获取一个对象的锁,但是该锁已被其他线程持有而被动进入的状态,此时该线程将被阻塞到锁被释放,该线程会重新尝试获取锁,一旦获取到锁,就会进入RUNNABLE状态。
WAITING状态是线程主动调用wait()
、join()
或LockSupport.park()
等方法,等待其他线程的通知或者执行某些操作的一种状态。需要其他线程调用调用同一对象锁的notify或notifyall方法唤醒。
不同的线程之间如何进行通信?
-
共享变量:利用
volatile
关键字修饰变量,保证变量的可见性,一个线程对变量的修改,可以对所线程可见。 -
wait,notify,notifyall方法结合synchronized同步块:保证代码在同一时刻只能被一个线程访问,并刷新内存可见性。生产者线程,队列满用wait方法使线程等待消费,notify,notifyall通知消费线程消费。消费者线程队列空陷入等待生产,notify,notify通知生产线程生产。
-
Reentrantlock,await,signal方法:synchronized同步代码块替换成lock()和unlock(),原理大致同上。
-
线程安全队列(BlockingQueue):队列满时插入元素自动阻塞,队列空时取出元素自动阻塞。
-
CountDownLatch:让一个或多个线程等待其他线程完成操作。CountDownLatch(int count):构造函数,指定需要等待的线程数量。
countDown():减少计数器的值。await():使当前线程等待,直到计数器的值为0。不可重置,一次性使用 -
CyclicBarrier:让多个线程相互等待,直到所有线程都到达某个屏障点。可重置,循环使用。
屏障点就是CyclicBarrier内部有一个计数器,预设的线程数就是计数器的起始数据,每当调用await方法,计数器会减1,直到为0就会执行到达屏障点后的操作。
-
Semaphore:控制同时访问特定资源的线程数量。允许多个线程并发运行,每个线程运行之前需先获得许可。
JUC包下常用的类
- 线程池相关类
ThreadPoolExecutor
:最核心的线程池实现类,用于创建和管理线程池,可以自定义配置线程池的参数,如核心线程数,最大线程数,拒绝策略,任务队列等。Executors
:线程池工厂类,提供了一系列静态方法来创建不同类型的线程池,如newFixedThreadPool(创建固定线程数的线程池)、newCachedThreadPool(创建可缓存线程池)、newSingleThreadExecutor(创建单线程线程池)等,方便开发者快速创建线程池。ScheduledThreadPoolExecutor
:用来实现定时任务和周期性任务的线程池,继承自ThreadPoolExecutor
并实现了ScheduledExecutorService
接口。
- 并发集合类
- ConcurrentHashMap:线程安全的哈希映射表,用于在多线程环境下高效地存储和访问键值对。JDK7及以前它采用了分段锁等技术,允许多个线程同时访问不同的段,提高了并发性能,在高并发场景下比传统的Hashtable性能更好。JDK8及以后通过 “数组 + 链表 + 红黑树” 结构和 CAS + synchronized 机制实现高效并发
- CopyOnWriteArrayList:
CopyOnWriteArrayList
是 Java 并发包中基于写时复制机制的线程安全列表,适用于读多写少场景。其核心原理是写操作(如add
、remove
)时创建原数组副本,操作完成后替换原数组,保证线程安全;读操作(如get
、迭代器)无需加锁,直接访问原数组,性能高效。它的迭代器具有弱一致性,因为迭代器在遍历时会基于此刻创建一个数组快照,持有原数组的引用,但是CopyOnWriteArrayList写操作是在原数组副本上进行的,因此不会影响原数组,创建后不受后续写操作影响,不会抛出ConcurrentModificationException
。但写操作因涉及数组复制,内存开销大、性能较低,因此不适合频繁写操作或大规模数据场景。
- 同步工具类
- CountDownLatch:允许一个或多个线程等待其他一组线程完成操作后再继续执行。它通过一个计数器来实现,计数器初始化为线程的数量,每个线程完成任务后调用countDown方法将计数器减一,当计数器为零时,等待的线程可以继续执行。常用于多个线程完成各自任务后,再进行汇总或下一步操作的场景。
- CyclicBarrier:让一组线程互相等待,直到所有线程都到达某个屏障点后,再一起继续执行。与CountDownLatch不同的是,CyclicBarrier可以重复使用,当所有线程都通过屏障后,计数器会重置,可以再次用于下一轮的等待。适用于多个线程需要协同工作,在某个阶段完成后再一起进入下一个阶段的场景。
- Semaphore:信号量,用于控制同时访问某个资源的线程数量。它维护了一个许可计数器,线程在访问资源前需要获取许可,如果有可用许可,则获取成功并将许可计数器减一,否则线程需要等待,直到有其他线程释放许可。常用于控制对有限资源的访问,如数据库连接池、线程池中的线程数量等。
- 原子类
- AtomicInteger:原子整数类,提供了对整数类型的原子操作,如自增、自减、比较并交换等。通过硬件级别的原子指令来保证操作的原子性和线程安全性,避免了使用锁带来的性能开销,在多线程环境下对整数进行计数、状态标记等操作非常方便。
- AtomicReference:原子整数类,提供了对整数类型的原子操作,如自增、自减、比较并交换等。通过硬件级别的原子指令来保证操作的原子性和线程安全性,避免了使用锁带来的性能开销,在多线程环境下对整数进行计数、状态标记等操作非常方便。
怎么保证多线程安全?
- synchronized关键字:锁定同步代码块,确保同一时刻只有一个线程访问这些代码。
- volatile关键字:修饰变量,确保变量的可见性,保证所有线程访问的是此变量的最新值。
- Lock接口和ReentrantLock类:java.util.concurrent.locks.Lock接口提供了比synchronized更强大的锁定机制,ReentrantLock是一个实现该接口的例子,提供了更灵活的锁管理和更高的性能。
- 原子类:这些类提供了硬件级别的原子指令操作,可以用于更新基本类型的变量而无需额外的同步、
- 线程局部变量:ThreadLocal类可以为每个线程提供独立的变量副本,这样每个线程都拥有自己的变量,消除了竞争条件。
- 并发集合:使用java.util.concurrent包中的线程安全集合,如ConcurrentHashMap、ConcurrentLinkedQueue等,这些集合内部已经实现了线程安全的逻辑。
- JUC工具类:使用java.util.concurrent包中的一些工具类可以用于控制线程间的同步和协作。例如:Semaphore和CyclicBarrier等
Java中有哪些常用的锁,在什么场景下使用?
-
内置锁:通过
synchronized
关键字实现,自动获取和释放。syncronized加锁时有无锁、偏向锁、轻量级锁和重量级锁几个级别。锁对象的对象头的Mark Word通过组合标志位来判断锁的状态。
- 无锁:
biased_lock
标志位0,lock标志位01表示无锁状态,当线程访问synchronized
代码块时,若锁对象处于无锁状态,JVM 会根据竞争情况决定是否升级为偏向锁或轻量级锁。 - 偏向锁:
biased_lock
标志位1,lock标志位01表示偏向锁状态。单线程重复获取同一把锁,并非是有多个锁,某个线程更偏向其中的一把锁,首次获取锁时,JVM 会在对象头的Mark Word中记录当前线程 ID,该锁即 “偏向” 此线程。后续该线程再次获取同一把锁时,只需检查 Mark Word 中的线程 ID 是否与当前线程一致:- 一致:直接获取锁,无需 CAS 操作(开销极低)。
- 不一致:说明有其他线程竞争,需撤销偏向锁,升级为轻量级锁(通过 CAS 尝试加锁)。
- 轻量级锁:lock标志位01,轻量级锁是 JVM 在 “偏向锁失效” 到 “重量级锁” 之间的过渡方案,通过用户态 CAS 自旋避免内核态线程阻塞,适用于短时间、轻度竞争的锁场景。其核心优势在于用CPU 时间换取线程上下文切换开销,但需注意自旋次数的合理设置,避免无效 CPU 消耗。当锁竞争加剧(如自旋超时),轻量级锁会升级为重量级锁,依赖操作系统的 Mutex 机制实现线程同步。
- 重量级锁:重量级锁是 Java 中处理激烈锁竞争的最终解决方案,它通过依赖操作系统内核的互斥量(Mutex)实现线程同步。当轻量级锁的自旋优化无法解决竞争时(如锁被长时间持有或多线程激烈争夺),JVM 会将锁升级为重量级锁。此时,对象头的 Mark Word 会指向堆中的 Monitor 对象(标志位变为
10
),竞争失败的线程会被放入 Monitor 的 EntryList 队列,并通过系统调用进入内核态阻塞(状态变为BLOCKED
),不再消耗 CPU 资源。锁释放时,操作系统会从队列中唤醒一个线程,该线程需重新竞争锁。重量级锁的核心优势是通过线程休眠避免无效自旋,但代价是每次加锁 / 解锁需进行两次用户态与内核态的切换(约 5000-10000 纳秒开销),性能较低。因此,它适用于锁竞争激烈、同步代码块执行时间长的场景
- 无锁:
-
ReentrantLock:ReentrantLock 是 Java 并发包中基于 AQS 框架实现的可重入互斥锁,支持公平与非公平模式,具备可重入、可中断获取、超时获取、多条件变量等特性,通过显式调用
lock()
和unlock()
(需配合finally
避免锁泄漏)实现线程同步。- 可重入:通过状态变量记录锁的持有次数,同一线程再次获取同一把锁时,状态变量次数就会++;若调用unlock(),则会–;
- 公平锁:线程按申请顺序获取锁(通过 FIFO 等待队列),避免 “饥饿”,但性能较低(频繁上下文切换)。
- 饥饿指的是线程在等待队列中长时间无法获取锁的现象。
- 非公平锁:允许新线程 “插队” 获取锁(可能刚释放就被新线程抢占),吞吐量更高(减少队列维护开销),但可能导致线程长时间等待。
-
读写锁(ReadWriteLock):java.util.concurrent.locks.ReadWriteLock接口定义了一种锁,允许多个读取者同时访问共享资源,但只允许一个写入者。读写锁通常用于读取远多于写入的情况,以提高并发性。
-
自旋锁:自旋锁是一种锁机制,线程在等待锁时会持续循环检查锁是否可用,而不是放弃CPU并阻塞。通常可以使用CAS来实现。这在锁等待时间很短的情况下可以提高性能,但过度自旋会浪费CPU资源。
-
乐观锁和悲观锁:悲观锁(Pessimistic Locking).通常指在访问数据前就锁定资源,假设最坏的情况,即数据很可能被其他线程修改。synchronized和ReentrantLock都是悲观锁的例子。乐观锁(Optimistic Locking)通常不锁定资源,而是在更新数据时检查数据是否已被其他线程修改。乐观锁常使用版本号或时间戳来实现。
CountDownLatch 是做什么的讲一讲?
CountDownLatch是Java并发包(java.util.concurrent)中的一个同步工具类,用于让一个或多个线程等待其他线程完成操作后再继续执行。其核心是通过一个计数器(Counter)实现线程间的协调,常用于多线程任务的分阶段控制或主线程等待多个子线程就绪的场景,核心原理:
初始化计数器:创建CountDownLatch时指定一个初始计数值(如N)。
等待线程阻塞:调用await()的线程会被阻塞,直到计数器变为O。
任务完成通知:其他线程完成任务后调用countDown(),使计数器减1。
唤醒等待线程:当计数器减到0时,所有等待的线程会被唤醒。唤醒的线程会因为CPU调度或者锁竞争而争抢。
synchronized和reentrantlock原理及其应用场景?
synchronized原理:
synchronized
是 Java 中实现线程同步的关键字,其核心原理基于对象的 monitor(监视器) 机制。在字节码层面,同步代码块通过 monitorenter 和 monitorexit 指令实现,线程执行 monitorenter
时尝试获取对象的 monitor(计数器加 1),执行 monitorexit
时释放 monitor(计数器减 1);同步方法则通过 ACC_SYNCHRONIZED 方法标志让 JVM 在调用时自动处理 monitor 的获取与释放。该机制支持 可重入锁,即同一线程可多次获取同一 monitor(计数器递增)。从 JDK 6 起,JVM 引入 偏向锁、轻量级锁 和 锁粗化 等优化,通过对象头的 Mark Word 记录锁状态,在运行时动态调整锁的类型,显著提升了 synchronized
的性能。
同步方法锁原理:
-
实例方法锁this对象
当实例方法使用了synchronized关键字时,锁的是调用该方法的对象,当多个线程使用同一个实例对象调用同一个加了synchronized的实例方法时,多个线程会竞争该锁对象的monitor,只有一个线程获得锁对象,其他线程阻塞等待。
-
静态方法锁class对象
静态同步方法的锁是类的 Class 对象,同一类的所有实例共享该锁。无论通过多少个实例调用静态同步方法,本质都是竞争同一个 monitor,因此会产生线程阻塞等待。
reentrantlock原理:
ReentrantLock
是 Java 中基于 AQS(AbstractQueuedSynchronizer) 实现的可重入锁,通过维护一个 volatile 状态变量 state
和一个 FIFO 等待队列 实现锁机制。它支持 可重入性(通过 state
计数器递增 / 递减)、公平 / 非公平锁(通过构造函数选择)、可中断锁(lockInterruptibly()
)和 多条件变量(Condition
对象)。与 synchronized
相比,ReentrantLock
提供了更灵活的锁控制,但需要手动调用 lock()
和 unlock()
。其核心原理是通过 CAS 操作修改 state
来竞争锁,失败的线程会被封装成节点加入队列并阻塞,锁释放时唤醒队列头节点。
- 公平锁原理::线程尝试获取锁时,必须先检查等待队列是否有前驱节点(即是否有其他线程先申请锁)。若队列不为空且当前线程不是队首节点,则必须入队等待,确保先到先得。
- 非公平锁原理:线程尝试获取锁时,先直接通过 CAS 抢占锁(无视队列是否有等待线程),只有在抢占失败时才会入队。
应用场景
- 优先用
synchronized
:若需求简单(如方法同步、自动释放锁),或需要与wait/notify
结合使用。 - 优先用
ReentrantLock
:若需要公平锁、可中断锁、尝试获取锁、多条件变量,或基于 AQS 扩展自定义同步机制。
synchronized锁静态方法和普通方法区别?
- 锁的对象不同:
普通方法:锁的是当前对象实例(this)。同一对象实例的 synchronized 普通方法,同一时间只能被一个线程访问;不同对象实例间互不影响,可被不同线程同时访问各自的同步普通方法。
静态方法:锁的是当前类的 Class 对象。由于类的 Class 对象全局唯一,无论多少个对象实例,该静态同步方法同一时间只能被一个线程访问。
- 作用范围不同:
普通方法:仅对同一对象实例的同步方法调用互斥,不同对象实例的同步普通方法可并行执行。
静态方法:对整个类的所有实例的该静态方法调用都互斥,一个线程进入静态同步方法,其他线程无法进入同一类任何实例的该方法。
-
多实例场景影响不同:
普通方法:多线程访问不同对象实例的同步普通方法时,可同时执行。
静态方法:不管有多少对象实例,同一时间仅一个线程能执行该静态同步方法。
synchronized和reentrantlock区别?
特性 | synchronized | ReentrantLock |
---|---|---|
实现机制 | JVM 内置关键字,通过 monitor 实现 | Java 类,基于 AQS(AbstractQueuedSynchronizer)框架 |
锁获取方式 | 隐式获取 / 释放(自动) | 显式调用 lock() 和 unlock() (必须在 finally 中释放) |
可重入性 | 支持(同一线程可重复获取同一把锁) | 支持(通过 state 计数器实现) |
公平性 | 非公平(默认) | 支持公平 / 非公平(构造函数指定) |
可中断性 | 不可中断(线程无法响应中断) | 支持(lockInterruptibly() 方法) |
锁超时 | 不支持(只能无限等待) | 支持(tryLock(timeout, unit) ) |
条件变量(Condition) | 单一 wait() /notify() 机制 | 可创建多个 Condition 对象,精准唤醒特定线程 |
锁状态判断 | 无法判断锁是否被持有 | 可通过 isLocked() 查询锁状态 |
synchronized 支持重入吗?如何实现的?
synchronized底层是利用计算机系统mutex Lock实现的。每一个可重入锁都会关联一个线程ID和一个锁
态status。
当一个线程请求方法时,会去检查锁状态。
1.如果锁状态是0,代表该锁没有被占用,使用CAS操作获取锁,将线程ID替换成自己的线程ID。
2.如果锁状态不是0,代表有线程在访问该方法。此时,如果线程D是自己的线程D,如果是可重入锁,
会将status自增1,然后获取到该锁,进而执行相应的方法;如果是非重入锁,就会进入阻塞队列等
待。
在释放锁时,
1.如果是可重入锁的,每一次退出方法,就会将status减1,直至statusl的值为0,最后释放该锁。
2.如果非可重入锁的,线程退出方法,直接就会释放该锁。
syncronized锁升级的过程讲一下
-
无锁–>偏向锁
触发条件:第一个线程访问同步块(无竞争场景)。
升级过程:- JVM 检测到同步块可使用偏向锁(默认开启
-XX:+UseBiasedLocking
)。 - 线程通过 CAS 操作将 Mark Word 中的线程 ID 设为自己的 ID。
- 若成功,获取偏向锁,Mark Word 状态变为「偏向锁」。
- JVM 检测到同步块可使用偏向锁(默认开启
-
偏向锁–>轻量级锁
触发条件:其他线程尝试获取已被偏向的锁(轻度竞争)。
升级过程:-
偏向锁拥有者线程(线程 A)正在执行同步块,线程 B 尝试获取锁。JVM 发现对象已被偏向(线程 A 持有)。
-
检查线程 A 是否仍在执行同步块(即是否存活)。
-
若线程 A 存活,无法直接撤销偏向锁,需通过[偏向锁撤销机制]升级为轻量级锁:
- 在线程 B 的栈帧中创建锁记录(Lock Record)。
- 尝试用 CAS 将对象头的 Mark Word 从偏向锁状态(线程 A ID)更新为指向线程 B 锁记录的指针(轻量级锁状态)。
- 若 CAS 成功,线程 B 获取轻量级锁;若失败(线程 A 仍持有锁),进一步升级为重量级锁。
检查发现线程 A 已死亡(不再执行同步块)。
-
JVM 直接将对象头的 Mark Word 重置为无锁状态(哈希码 + 分代年龄 + 锁标志
01
)。 -
线程 B 可重新以偏向锁方式获取锁(通过 CAS 将自己的 ID 写入 Mark Word)。
-
-
轻量级锁–>重量级锁
触发条件:轻量级锁竞争中 CAS 失败(如线程 A 未释放锁,线程 B CAS 失败)。
升级过程:- 线程 B 尝试通过「自旋(Spin)」等待线程 A 释放锁(默认自旋 10 次或自适应)。
- 若自旋后仍未获取锁,则膨胀为重量级锁:
- 在对象头中设置指向 Monitor 的指针,Mark Word 变为「10」。
- 线程 B 进入 Monitor 的 EntryList 阻塞,等待操作系统唤醒。
JVM对Synchornized的优化?
-
锁膨胀:synchronized从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫做锁膨胀
也叫做锁升级。JDK1.6之前,synchronized是重量级锁,也就是说synchronized在释放和获取锁时都
会从用户态转换成内核态,而转换的效率是比较低的。但有了锁膨胀机制之后,synchronized的状态
就多了无锁、偏向锁以及轻量级锁了,这时候在进行并发操作时,大部分的场景都不需要用户态到内核
态的转换了,这样就大幅的提升了synchronized的性能。 -
锁粗化:锁粗化是指 JVM 将多个连续的加锁、解锁操作合并为一个范围更大的锁,减少频繁加锁解锁的性能损耗
-
锁消除:锁消除是 JVM 的一项关键优化技术,通过逃逸分析判断对象是否仅在单线程环境中使用且未逃逸出方法作用域。若满足条件,即使代码中存在同步操作(如使用
StringBuffer
的同步方法或显式的synchronized
块),JVM 也会在运行时自动消除这些锁,将其优化为无锁状态。逃逸分析
- 全局逃逸:对象被外部方法引用(如作为返回值)。
- 参数逃逸:对象作为参数传递给其他方法。
- 无逃逸:对象仅在方法内部使用,未被外部引用。
-
自旋锁:自旋锁是 JVM 为优化
synchronized
锁性能而设计的一种机制,当线程尝试获取已被占用的锁时,不会立即进入阻塞状态,而是通过执行一段无意义的循环(自旋)来等待锁释放。若在自旋期间锁被释放,线程能立即获取锁并继续执行,避免了线程阻塞和唤醒带来的上下文切换开销,尤其适用于锁持有时间短、多核 CPU 的场景。 -
自适应自旋:自适应自旋是 JVM 在自旋锁基础上的进阶优化机制,其核心在于让 JVM 根据历史锁竞争的统计信息动态调整自旋策略,而非采用固定的自旋次数。具体来说,当线程竞争锁失败进入自旋状态时,JVM 会记录该锁过去自旋的成功率:若此前自旋成功获取锁的频率较高,会认为当前自旋有较大概率成功,从而延长允许的自旋时间(如增加循环次数);若多次自旋失败,则判定锁竞争激烈,会缩短自旋时间甚至直接让线程进入阻塞状态,避免 CPU 资源因无效自旋被浪费。