锁消除、锁粗化相关概念
synchronized 基础回顾
尽管 synchronized 为多线程编程提供了重要的保障,但在高并发场景下,它的性能开销逐渐成为一个不容忽视的问题。例如,当大量线程竞争同一个锁时,频繁的线程阻塞和唤醒会导致用户态与内核态之间的频繁切换,这会消耗大量的系统资源,严重影响程序的性能。为了应对这些问题,JVM 对 synchronized 进行了一系列的优化,其中锁粗化、锁消除、批量重偏向、批量撤销以及重量级锁的自适应自旋等机制尤为重要,接下来将详细介绍这些优化机制。
优化前奏:锁的状态与升级
锁状态初窥
在 Java 中,synchronized 关键字所涉及的锁一共有四种状态,从低到高依次为无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态 。这些状态与对象头中的 Mark Word 密切相关,Mark Word 存储着对象的运行时数据,如哈希码、分代年龄以及至关重要的锁状态信息等。
当对象处于无锁状态时,这意味着没有任何线程对其进行锁定操作,它可以被多个线程自由访问。此时 Mark Word 中的数据主要用于存储对象的哈希码、分代年龄等常规信息,锁标志位为 “01” ,偏向锁标志位为 “0” ,表明对象未被锁定,也不存在偏向任何线程的情况。
当第一个线程访问对象并尝试获取锁时,如果此时没有其他线程竞争,JVM 会将锁升级为偏向锁。在偏向锁状态下,Mark Word 会记录下当前获取锁的线程 ID,偏向锁标志位设置为 “1” ,锁标志位依然为 “01” 。后续只要是这个线程再次访问该对象,它只需检查 Mark Word 中记录的线程 ID 是否与自己的 ID 一致,如果一致,就可以直接获取锁,无需进行额外的同步操作,这大大减少了无竞争情况下的锁开销,提高了程序的执行效率。
随着多线程环境的变化,如果有其他线程尝试获取偏向锁对象的锁,这就意味着出现了竞争。此时偏向锁将无法满足同步需求,会升级为轻量级锁。在轻量级锁状态下,JVM 会在当前线程的栈帧中创建一个锁记录(Lock Record),并将对象头中的 Mark Word 复制到锁记录中,同时对象头中的 Mark Word 会被修改为指向锁记录的指针,此时锁标志位变为 “00” 。轻量级锁主要通过自旋(Spinning)的方式来尝试获取锁,即线程在短时间内不断尝试重新获取锁,而不是立即阻塞,这样可以避免线程切换带来的性能开销,在竞争不激烈的情况下能有效提升性能。
当轻量级锁的自旋操作无法成功获取锁,或者有较多线程竞争锁时,锁就会进一步升级为重量级锁。在重量级锁状态下,对象头中的 Mark Word 会存储指向重量级锁(Monitor)的指针,锁标志位变为 “10” 。此时,未获取到锁的线程会被阻塞,进入等待队列,等待持有锁的线程释放锁后被唤醒并重新尝试获取锁。由于涉及到线程的阻塞和唤醒,以及用户态与内核态的切换,重量级锁的开销相对较大。
值得注意的是,synchronized 锁状态只能升级,不能降级。这种策略主要是为了提高获得锁和释放锁的效率,避免在锁状态频繁切换过程中产生过多的性能开销。例如,一旦锁从偏向锁升级为轻量级锁,即使后续又回到了无竞争状态,也不会再降级为偏向锁,而是保持当前的轻量级锁状态,直到出现更适合升级的条件 。
升级过程解析
为了更清晰地理解 synchronized 锁的升级过程,下面通过一个具体的案例和流程图进行详细说明。假设有一个共享资源对象resource
,有多个线程尝试对其进行访问。
起初,resource
处于无锁状态,此时没有任何线程持有锁。当线程Thread1
首次访问resource
并进入synchronized
代码块时,JVM 会尝试将锁升级为偏向锁。它通过 CAS(Compare-And-Swap)操作,将Thread1
的线程 ID 记录在resource
对象头的 Mark Word 中,并设置偏向锁标志位。由于此时没有其他线程竞争,CAS 操作成功,resource
进入偏向锁状态,Thread1
可以直接执行synchronized
代码块中的代码 。
public class SynchronizedExample {
private static final Object resource = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource) {
// Thread1执行的代码
}
});
thread1.start();
}
}
如果在Thread1
持有偏向锁期间,线程Thread2
也尝试访问resource
并进入synchronized
代码块,JVM 会检测到对象头中的线程 ID 与Thread2
不一致,这表明出现了竞争。此时偏向锁会被撤销,resource
进入轻量级锁升级流程。JVM 会在Thread2
的栈帧中创建锁记录,并将resource
对象头的 Mark Word 复制到锁记录中,然后通过 CAS 操作尝试将对象头的 Mark Word 修改为指向Thread2
锁记录的指针。如果 CAS 操作成功,Thread2
获取到轻量级锁,可以执行同步代码;如果失败,说明竞争较为激烈,轻量级锁会进一步升级为重量级锁 。
public class SynchronizedExample {
private static final Object resource = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource) {
try {
Thread.sleep(1000); // 模拟Thread1执行较长时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource) {
// Thread2执行的代码
}
});
thread1.start();
thread2.start();
}
}
当轻量级锁升级为重量级锁时,resource
对象头的 Mark Word 会指向一个重量级锁(Monitor)对象。Thread2
在获取锁失败后会被阻塞,进入 Monitor 的等待队列。只有当Thread1
执行完synchronized
代码块,释放锁后,Monitor 会唤醒等待队列中的Thread2
,Thread2
才有机会重新尝试获取重量级锁并执行同步代码 。
深入优化策略
锁粗化:化零为整的智慧
在多线程编程中,锁的使用频率和范围对程序性能有着显著影响。锁粗化(Lock Coarsening)便是一种通过扩大锁的作用范围,减少不必要的锁获取和释放操作,从而提升性能的优化策略 。
正常情况下,为了提高并发性能,我们通常会将锁的粒度控制得尽可能小,以减少线程竞争和锁的持有时间。然而,当一系列连续的操作都对同一个对象进行频繁的加锁和解锁时,这种细粒度的锁操作反而会带来额外的开销。例如,在一个循环中多次对同一个对象进行加锁和解锁操作:
public class LockCoarseningExample {
private static final Object lock = new Object();
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
// 一些简单的操作,如打印或计算
System.out.println("执行第 " + i + " 次操作");
}
}
}
}
在上述代码中,每次循环都进行一次锁的获取和释放操作,这无疑会消耗大量的系统资源。而锁粗化机制则会检测到这种连续的、对同一对象的锁操作,并将锁的范围扩展到整个操作序列之外。经过锁粗化优化后,上述代码的实际执行效果类似于:
public class LockCoarseningExample {
private static final Object lock = new Object();
public static void main(String[] args) {
synchronized (lock) {
for (int i = 0; i < 10000; i++) {
// 一些简单的操作,如打印或计算
System.out.println("执行第 " + i + " 次操作");
}
}
}
}
这样,锁只需要在循环开始前获取一次,在循环结束后释放一次,大大减少了锁的获取和释放次数,降低了系统开销,从而提高了程序的执行效率 。
为了更直观地感受锁粗化的性能提升,我们可以通过一个性能测试来对比优化前后的执行时间。使用System.currentTimeMillis()
方法记录开始和结束时间,分别对优化前和优化后的代码进行多次执行,并统计平均执行时间。测试结果显示,优化后的代码执行时间明显缩短,这充分证明了锁粗化在减少锁开销、提升性能方面的有效性 。
不过,锁粗化并非适用于所有场景。如果在锁的作用范围内存在大量耗时操作,可能会导致其他线程长时间等待,降低系统的并发性能。因此,在实际应用中,需要根据具体的业务逻辑和性能需求,合理判断是否适合使用锁粗化优化。
锁消除:无用锁的精简
锁消除(Lock Elimination)是 JVM 在即时编译(JIT)阶段执行的一项优化技术,旨在移除那些在运行时被检测到不可能存在竞争的锁,从而提高程序的执行效率 。这项技术的实现依赖于逃逸分析(Escape Analysis),通过分析对象的作用域和生命周期,判断对象是否会被多个线程访问,如果确定对象仅在单个线程内使用,那么对该对象的同步操作便是多余的,可以被安全地消除 。
以一个简单的字符串拼接方法为例,在 Java 中,StringBuffer
类的方法是线程安全的,内部使用了synchronized
关键字来保证线程安全。但在某些情况下,当StringBuffer
对象仅在一个方法内部被使用,且不会被其他线程访问时,这些同步操作实际上是不必要的。例如:
public class LockEliminationExample {
public String concatStrings(List<String> strings) {
StringBuffer sb = new StringBuffer();
for (String s : strings) {
sb.append(s);
}
return sb.toString();
}
}
在上述代码中,StringBuffer
对象sb
是在concatStrings
方法内部创建的局部变量,它的作用域仅限于该方法内部,不会逃逸到其他线程中。因此,JVM 在执行逃逸分析后,可以确定对sb
的同步操作不会发生竞争,进而安全地消除这些同步操作,减少锁的开销,提高程序的执行效率 。
为了验证锁消除的效果,我们可以通过反编译工具查看优化前后的字节码。在未进行锁消除优化时,StringBuffer
的append
方法调用会包含获取和释放锁的字节码指令;而经过锁消除优化后,这些与锁相关的字节码指令将被移除,从而证明了锁消除的实际作用 。
锁消除技术有效地减少了不必要的同步开销,提高了程序的并发性能。但需要注意的是,锁消除的前提是 JVM 能够准确地进行逃逸分析,判断对象的线程可见性。对于一些复杂的代码结构或动态对象创建的场景,逃逸分析可能存在一定的局限性,从而影响锁消除的效果 。
批量重偏向与批量撤销:偏向锁的动态调整
批量重偏向
在多线程编程中,偏向锁(Biased Locking)为只有一个线程频繁访问同步资源的场景带来了显著的性能提升,因为它避免了无竞争情况下的锁获取开销。然而,当多个线程交替访问偏向锁对象时,频繁的偏向锁撤销和升级操作会导致性能下降。为了解决这个问题,JVM 引入了批量重偏向(Bulk Rebias)机制 。
批量重偏向的产生原因在于,当一个类的多个对象被不同线程竞争偏向锁时,如果每次都进行偏向锁的撤销和升级,会消耗大量的系统资源。例如,在一个电商系统中,有多个线程处理不同用户的订单操作,每个订单对象都可能成为偏向锁的竞争对象,如果没有批量重偏向机制,性能将会受到严重影响 。
为了更好地理解批量重偏向的过程,我们通过一个实验来进行说明。首先,创建一个包含多个对象的类,并编写测试代码,让多个线程依次对这些对象进行加锁操作:
import org.openjdk.jol.info.ClassLayout;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class BulkRebiasTest {
public static void main(String[] args) throws InterruptedException {
// 延时产生可偏向对象
TimeUnit.SECONDS.sleep(5);
List<Object> list = new ArrayList<>();
// 线程1,创建并偏向锁对象
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
Object lock = new Object();
synchronized (lock) {
list.add(lock);
}
}
try {
// 为了防止JVM线程复用,保持线程存活
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread1");
thread1.start();
// 等待线程1创建对象完成
TimeUnit.SECONDS.sleep(3);
// 线程2,尝试获取锁,引发偏向锁撤销
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 40; i++) {
Object obj = list.get(i);
synchronized (obj) {
if (i >= 15 && i <= 21 || i >= 38) {
System.out.println("thread2 - 第 " + (i + 1) + " 次加锁执行中\t" + ClassLayout.parseInstance(obj).toPrintable());
}
}
}
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2");
thread2.start();
TimeUnit.SECONDS.sleep(3);
// 线程3,再次尝试获取锁
Thread thread3 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
Object lock = list.get(i);
if (i >= 17 && i <= 21 || i >= 35 && i <= 41) {
System.out.println("thread3 - 第 " + (i + 1) + " 次准备加锁\t" + ClassLayout.parseInstance(lock).toPrintable());
}
synchronized (lock) {
if (i >= 17 && i <= 21 || i >= 35 && i <= 41) {
System.out.println("thread3 - 第 " + (i + 1) + " 次加锁执行中\t" + ClassLayout.parseInstance(lock).toPrintable());
}
}
}
}, "thread3");
thread3.start();
TimeUnit.SECONDS.sleep(3);
System.out.println("查看新创建的对象");
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
// 防止主线程退出
LockSupport.park();
}
}
在这个实验中,线程 1 首先创建 50 个对象并对其进行偏向锁操作。然后线程 2 尝试获取这些对象的锁,当线程 2 获取锁时,会引发偏向锁的撤销和升级。当偏向锁撤销次数达到一定阈值(默认 20 次)时,JVM 会触发批量重偏向操作 。
从实验结果可以看出,在批量重偏向发生后,后续线程获取锁时,不再进行偏向锁的撤销,而是直接将对象偏向于当前线程,从而减少了偏向锁撤销带来的性能开销 。
JVM 中与批量重偏向相关的参数主要有-XX:BiasedLockingBulkRebiasThreshold
,它用于设置批量重偏向的阈值,默认值为 20。通过调整这个参数,可以根据实际应用场景优化批量重偏向的触发条件,以达到更好的性能表现 。
批量撤销
批量撤销(Bulk Revoke)是偏向锁优化机制中的另一个重要部分,它主要用于应对多线程竞争非常激烈的场景 。当一个类的对象在频繁的偏向锁撤销操作后,JVM 会认为该类的对象不适合使用偏向锁,进而采取批量撤销的措施 。
批量撤销的背景在于,当偏向锁的撤销次数持续增加,达到一定阈值后,说明该类的对象在多线程环境下竞争激烈,偏向锁的优势无法体现,反而会因为频繁的撤销和升级操作消耗性能。例如,在一个高并发的分布式系统中,多个节点的线程同时访问共享资源,此时偏向锁可能会频繁地被撤销和升级,导致系统性能下降 。
同样通过实验来深入理解批量撤销机制。在上述批量重偏向的实验基础上,继续观察线程的操作。当线程 2 和线程 3 继续对对象进行加锁操作,使得偏向锁撤销次数达到另一个阈值(默认 40 次)时,JVM 会触发批量撤销操作 。
在批量撤销发生后,该类的所有对象将不再支持偏向锁,新建的对象也将处于无锁状态。当线程尝试获取锁时,会直接升级为轻量级锁或重量级锁 。这一过程有效地避免了偏向锁在高竞争场景下的性能损耗,保障了系统在复杂多线程环境下的稳定运行 。
JVM 中与批量撤销相关的参数是-XX:BiasedLockingBulkRevokeThreshold
,默认值为 40。通过合理调整这个参数,可以根据应用程序的实际多线程竞争情况,优化批量撤销的触发时机,从而提升系统的整体性能 。
重量级锁的自适应自旋:等待中的权衡
在多线程编程中,当线程竞争锁时,如果锁已经被其他线程持有,传统的做法是将未获取到锁的线程直接阻塞,等待锁的释放。然而,这种方式存在一定的性能开销,因为线程的阻塞和唤醒涉及到用户态与内核态的切换,会消耗较多的系统资源。为了减少这种开销,自旋锁(Spin Lock)应运而生 。
自旋锁的基本思想是,当线程尝试获取锁时,如果发现锁已被占用,它不会立即阻塞,而是在原地进行一段时间的自旋(即循环等待),不断尝试获取锁。如果在自旋的过程中,锁被释放,那么线程就可以直接获取到锁,避免了线程阻塞和唤醒带来的开销。自旋锁适用于锁被持有时间较短、线程竞争不激烈的场景。例如,在一个多线程的计算任务中,每个线程需要频繁地访问一个共享的计数器进行累加操作,由于每次操作时间较短,使用自旋锁可以有效地减少线程切换的开销,提高程序的执行效率 。
然而,自旋锁并非万能的解决方案,它也存在一定的局限性。如果锁被持有时间较长,线程长时间自旋会浪费 CPU 资源,降低系统的整体性能。在高并发场景下,大量线程同时自旋可能会导致 CPU 使用率飙升,影响其他任务的执行 。
为了克服自旋锁的局限性,JVM 引入了自适应自旋锁(Adaptive Spinning)机制 。自适应自旋锁的原理是,JVM 会根据前一次自旋的情况,动态调整自旋的次数。如果前一次自旋成功获取到了锁,那么下一次自旋的时间会适当延长,因为 JVM 认为在这段时间内锁很可能会再次被释放;反之,如果前一次自旋未能获取到锁,那么下一次自旋的时间会缩短,甚至直接放弃自旋,将线程阻塞,以避免过多地浪费 CPU 资源 。
具体来说,当线程尝试获取重量级锁时,如果发现锁已被占用,它会先进行自旋操作。在自旋过程中,JVM 会监控自旋的结果和锁的状态。如果在自旋的时间内成功获取到了锁,那么下次自旋的时间会增加,例如从原来的 10 次循环增加到 20 次循环;如果自旋超时仍未获取到锁,JVM 会根据当前的竞争情况和系统负载,决定是否继续自旋或直接将线程阻塞 。这种动态调整自旋次数的方式,使得自适应自旋锁能够更好地适应不同的多线程场景,在提高性能的同时,避免了 CPU 资源的过度浪费 。
实际应用
优化策略抉择
在实际的 Java 应用开发中,选择合适的 synchronized 锁优化策略至关重要,这直接关系到系统的性能和稳定性。不同的优化策略适用于不同的多线程场景,需要根据具体情况进行细致的分析和抉择。
对于高并发且锁竞争激烈的场景,自适应自旋锁是一个较为合适的选择。在这种场景下,线程竞争锁的情况频繁发生,如果采用传统的线程阻塞方式,线程的频繁阻塞和唤醒会带来巨大的性能开销。而自适应自旋锁能够根据前一次自旋的结果动态调整自旋次数,在锁持有时间较短的情况下,通过自旋等待锁的释放,避免了线程上下文切换的开销,从而显著提高系统的并发性能。例如,在一个电商秒杀系统中,大量用户同时抢购有限的商品资源,此时对商品库存的操作需要加锁来保证数据的一致性。由于每个用户的抢购操作时间较短,使用自适应自旋锁可以有效地减少线程切换带来的性能损耗,提高系统的响应速度,确保更多的用户能够在短时间内完成抢购操作 。
当应用场景中无竞争或竞争较少时,偏向锁则能发挥出其独特的优势。偏向锁的设计初衷就是为了优化在无竞争情况下的锁获取操作,它通过将锁偏向于第一个获取它的线程,使得该线程在后续的访问中无需进行额外的同步操作,大大提高了程序的执行效率。以一个单线程操作线程安全集合的场景为例,如在一个后台任务中,单线程负责定期从数据库中读取数据并插入到一个ConcurrentHashMap
中。由于只有一个线程进行操作,不存在锁竞争,使用偏向锁可以避免不必要的锁获取和释放操作,从而提升数据处理的速度,减少任务执行的时间 。
在一些特定的代码结构中,如连续的、对同一对象的多次加锁解锁操作,锁粗化可以有效地减少锁的获取和释放次数,提升性能。比如在一个字符串拼接的方法中,如果多次对StringBuffer
对象进行加锁解锁操作来进行字符拼接,通过锁粗化将这些操作合并为一次大的加锁操作,可以显著减少锁操作带来的开销,提高字符串拼接的效率 。
而锁消除则主要应用于那些经过逃逸分析确定不存在共享数据竞争的代码块。在这些场景下,消除不必要的锁操作可以避免额外的性能开销,提高程序的执行效率。例如,在一个方法内部创建并使用的局部StringBuffer
对象,由于其作用域仅限于该方法内部,不会被其他线程访问,JVM 可以通过锁消除技术移除对该对象的同步操作,从而加快方法的执行速度 。
代码示例与性能验证
为了更直观地展示 synchronized 锁优化策略的效果,我们通过一个综合示例来进行说明,并使用性能测试工具对比优化前后的性能。假设我们有一个多线程的计数器类Counter
,其中的increment
方法用于对计数器进行加一操作。
首先,我们来看未进行优化的代码:
package com.vvhz.concurrent;
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
在这个示例中,increment
方法使用了synchronized
关键字来保证线程安全,但这种方式在高并发场景下可能会存在性能问题。
接下来,我们对代码进行优化,应用锁粗化和自适应自旋锁等优化策略:
package com.vvhz.concurrent;
public class OptimizedCounter {
private int count = 0;
private final Object lock = new Object();
public void increment(Integer ITERATIONS) {
synchronized (lock) {
for (int i = 0; i < ITERATIONS; i++) {
count++;
}
}
}
public int getCount() {
return count;
}
}
在优化后的代码中,我们将多次对count
的操作放在同一个synchronized
块中,实现了锁粗化。同时,由于 JVM 默认开启了自适应自旋锁,在多线程竞争锁时会根据实际情况动态调整自旋策略。
为了验证优化效果,我们使用JMH
(Java Microbenchmark Harness)性能测试工具进行测试。JMH
是一个专门用于 Java 代码微基准测试的工具,它能够准确地测量代码的执行性能。
以下是使用JMH
进行测试的代码示例:
测试前先添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.maisuitx</groupId>
<artifactId>concurrent</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<jmh.version>1.36</jmh.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- JMH core dependencies -->
<!-- JMH 的核心库,提供了进行基准测试所需的类和方法。 -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<!-- JMH 的注解处理器,用于在编译时生成基准测试所需的代码。 -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
package com.vvhz.concurrent;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
public class SynchronizedBenchmark {
private static final int THREADS = 10;
private static final int ITERATIONS = 100000;
@Benchmark
public static int testUnoptimizedCounter() throws InterruptedException {
Counter counter = new Counter();
Thread[] threads = new Thread[THREADS];
for (int i = 0; i < THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < ITERATIONS; j++) {
counter.increment();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
return counter.getCount();
}
@Benchmark
public static int testOptimizedCounter() throws InterruptedException {
OptimizedCounter counter = new OptimizedCounter();
Thread[] threads = new Thread[THREADS];
for (int i = 0; i < THREADS; i++) {
threads[i] = new Thread(() -> {
counter.increment(ITERATIONS);
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
return counter.getCount();
}
public static void main(String[] args) throws RunnerException {
try {
System.out.println(testOptimizedCounter());
System.out.println(testUnoptimizedCounter());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Options options = new OptionsBuilder()
.include(SynchronizedBenchmark.class.getSimpleName())
.build();
new Runner(options).run();
}
}
在上述代码中,我们定义了两个基准测试方法testUnoptimizedCounter
和testOptimizedCounter
,分别用于测试未优化和优化后的Counter
类的性能。通过JMH
的配置,我们设置了预热迭代次数、测量迭代次数、线程数以及每次迭代的时间等参数。
运行上述测试代码后,JMH
会输出详细的性能测试结果,包括平均执行时间等指标。通过对比这些指标,可以清晰地看到优化后的代码在性能上的显著提升。
平均操作耗时(Average):
325733.359 ns/op
是所有测量迭代的平均操作耗时。误差范围(±(99.9%)):
17201.060 ns/op
表示在 99.9% 的置信水平下的误差范围。最小值、平均值、最大值(min, avg, max):分别展示了测量迭代中的最小、平均和最大操作耗时。
标准差(stdev):
4467.061
反映了测量数据的离散程度。置信区间(CI):
[308532.299, 342934.419]
表示在 99.9% 的置信水平下,真实的平均操作耗时可能所在的区间。
通过以上代码示例和性能测试,我们可以得出结论:在实际应用中,合理应用 synchronized 锁的优化策略能够显著提升多线程程序的性能。但同时也需要注意,不同的优化策略适用于不同的场景,开发者需要根据具体的业务需求和性能瓶颈,仔细分析并选择合适的优化策略,以实现系统性能的最大化 。
总结
synchronized 锁的优化机制在 Java 并发编程中扮演着至关重要的角色,它们显著提升了多线程程序的性能和效率,为开发高并发、高性能的 Java 应用提供了坚实的基础 。锁粗化通过扩大锁的作用范围,减少锁的获取和释放次数,降低了系统开销;锁消除则移除了不必要的锁,避免了无意义的同步操作;批量重偏向和批量撤销机制根据多线程竞争情况动态调整偏向锁的使用,提高了偏向锁在复杂场景下的适用性;自适应自旋锁在减少线程上下文切换开销的同时,通过动态调整自旋策略,更好地适应了不同的锁竞争场景 。