当前位置: 首页 > news >正文

Java 同步锁性能的最佳实践:从理论到实践的完整指南

目录

一、同步锁性能分析

(一)性能验证说明

1. 使用同步锁的代码示例

2. 不使用同步锁的代码示例

3. 结果与讨论

(二)案例初步优化分析说明

1. 使用AtomicInteger原子类尝试优化分析

2. 对AtomicInteger原子类进一步优化

3. 结论说明(LongAdder原理理解体会)

二、回顾Java锁优化

(一)synchronized 关键字

1. monitor 锁的实现原理

2.分级锁

偏向锁(Biased Locking)

轻量级锁

重量级锁

3. 锁升级一览

(二)concurrent 包里面的 Lock

1. 锁机制基于线程而不是基于调用(可重入锁)

2. Lock 主要方法

lock()

unlock()

tryLock()

tryLock(long timeout, TimeUnit unit)

lockInterruptibly()

3. 读写锁ReentrantReadWriteLock

基本内容说明

性能验证说明

4.乐观读取、悲观读取和写入的机制:StampedLock

基本内容说明

性能验证说明

5. 公平锁与非公平锁

synchronized关键字 vs Lock接口

功能验证

(三)Java 中两种加锁方式对比和建议

三、锁的优化手段

(一)减少锁的粒度

(二)减少锁持有时间

(三)锁分级

(四)锁分离

(五)锁消除

(六)乐观锁

(七)无锁

参考文章


干货分享,感谢您的阅读!

在多线程编程中,锁是保证线程安全的重要手段之一,但如何选择合适的锁并进行优化,一直是我们面临的挑战。本博客探讨Java中同步锁的性能分析与优化之路,从使用同步锁和不使用同步锁的性能对比入手,逐步展开对锁的优化手段和技术原理的解析,帮助读者更好地理解和应用Java中的锁机制。

一、同步锁性能分析

同步锁在多线程编程中是保证线程安全的重要工具,其性能开销一直是不可忽视的存在。

(一)性能验证说明

为了直观说明我们可以直接先准备两个Java代码用例,我们通过高并发环境下的计数器递增操作来对比使用同步锁和不使用同步锁的性能差异。

1. 使用同步锁的代码示例

使用ReentrantLock来保护对共享资源(counter)的访问,确保同一时间只有一个线程可以对计数器进行操作。具体代码如下:

package org.zyf.javabasic.thread.lock.opti;import java.util.concurrent.locks.ReentrantLock;/*** @program: zyfboot-javabasic* @description: 使用了ReentrantLock来保护对共享资源(counter)的访问,确保同一时间只有一个线程可以对计数器进行操作。* @author: zhangyanfeng* @create: 2024-06-05 22:54**/
public class SyncLockExample {private static int counter = 0;private static final ReentrantLock lock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {long startTime = System.currentTimeMillis();Thread[] threads = new Thread[100];for (int i = 0; i < 100; i++) {threads[i] = new Thread(new IncrementWithLock());threads[i].start();}for (Thread thread : threads) {thread.join();}long endTime = System.currentTimeMillis();System.out.println("Time with lock: " + (endTime - startTime) + " ms");}static class IncrementWithLock implements Runnable {@Overridepublic void run() {for (int i = 0; i < 1000000; i++) {lock.lock();try {counter++;} finally {lock.unlock();}}}}
}

2. 不使用同步锁的代码示例

不使用任何同步机制,直接操作共享资源。具体代码如下:

package org.zyf.javabasic.thread.lock.opti;/*** @program: zyfboot-javabasic* @description: 不使用任何同步机制,直接操作共享资源。* @author: zhangyanfeng* @create: 2024-06-05 22:55**/
public class NoSyncLockExample {private static int counter = 0;public static void main(String[] args) throws InterruptedException {long startTime = System.currentTimeMillis();Thread[] threads = new Thread[100];for (int i = 0; i < 100; i++) {threads[i] = new Thread(new IncrementWithoutLock());threads[i].start();}for (Thread thread : threads) {thread.join();}long endTime = System.currentTimeMillis();System.out.println("Time without lock: " + (endTime - startTime) + " ms");}static class IncrementWithoutLock implements Runnable {@Overridepublic void run() {for (int i = 0; i < 1000000; i++) {counter++;}}}
}

3. 结果与讨论

运行以上代码,我当前的机器上可以直观的看到

  • 使用同步锁的时间: 1314 ms
  • 不使用同步锁的时间: 20 ms

从结果中可以明显看出,同步锁会带来显著的性能开销。同步锁的存在增加了线程间的等待时间和上下文切换的开销,从而降低了程序的整体运行效率。所以在使用锁时,对锁的优化使用是必不可少的。

(二)案例初步优化分析说明

在开始讲解一些常用的优化手段的时候,我们先就现在这个用例来谈谈可能我们一般可以想到的直观优化手段。

1. 使用AtomicInteger原子类尝试优化分析

Java的java.util.concurrent.atomic包提供了一些原子类,可以在并发编程中避免显式加锁。最简单的我们可以使用AtomicInteger来替代显式的锁。

package org.zyf.javabasic.thread.lock.opti;import java.util.concurrent.atomic.AtomicInteger;/*** @program: zyfboot-javabasic* @description: 使用AtomicInteger来替代显式的锁* @author: zhangyanfeng* @create: 2024-06-05 23:07**/
public class AtomicExample {private static AtomicInteger counter = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {long startTime = System.currentTimeMillis();Thread[] threads = new Thread[100];for (int i = 0; i < 100; i++) {threads[i] = new Thread(new IncrementAtomic());threads[i].start();}for (Thread thread : threads) {thread.join();}long endTime = System.currentTimeMillis();System.out.println("Time with AtomicInteger: " + (endTime - startTime) + " ms");}static class IncrementAtomic implements Runnable {@Overridepublic void run() {for (int i = 0; i < 1000000; i++) {counter.incrementAndGet();}}}
}

理论上这样优化后性能上必会上升,但实际上运行后其耗时为 6714 ms,性能反而变差了。这里其实我们之前的博客中也有讲过,其主要的原因有两个:

  • 原子类的开销AtomicIntegerincrementAndGet方法虽然是无锁的,但它依赖于底层的CAS(Compare-And-Swap)操作。CAS操作虽然是无锁的,但在高并发情况下,多个线程同时尝试更新同一个变量时,CAS操作可能会频繁地失败并重试,从而导致性能下降。相比之下,ReentrantLock在某些情况下可能反而表现更好,尤其是在锁争用不是特别激烈的时候。
  • 高并发下的内存争用在高并发情况下,多个线程同时访问和修改共享变量会导致内存争用。这种争用在使用AtomicInteger时表现得更为明显,因为每次操作都需要与主内存同步,可能会导致缓存一致性协议的开销。

2. 对AtomicInteger原子类进一步优化

我们可以尝试以下方法来进一步优化:

  • 减少线程数量:在本示例中,我们使用了100个线程同时访问共享变量,这可能导致过多的上下文切换和争用。可以尝试减少线程数量,看看性能是否有所改善。
  • 使用更高效的同步机制:可以尝试使用其他同步机制,如LongAdder或ConcurrentLinkedQueue等,这些工具在高并发场景下通常表现更好。

这里我们直接用LongAdder验证,具体代码如下:

package org.zyf.javabasic.thread.lock.opti;import java.util.concurrent.atomic.LongAdder;/*** @program: zyfboot-javabasic* @description: LongAdder在高并发情况下比AtomicInteger有更好的性能* @author: zhangyanfeng* @create: 2024-06-05 23:26**/
public class LongAdderExample {private static LongAdder counter = new LongAdder();public static void main(String[] args) throws InterruptedException {long startTime = System.currentTimeMillis();Thread[] threads = new Thread[100];for (int i = 0; i < 100; i++) {threads[i] = new Thread(new IncrementLongAdder());threads[i].start();}for (Thread thread : threads) {thread.join();}long endTime = System.currentTimeMillis();System.out.println("Time with LongAdder: " + (endTime - startTime) + " ms");}static class IncrementLongAdder implements Runnable {@Overridepublic void run() {for (int i = 0; i < 1000000; i++) {counter.increment();}}}
}

运行后发现这个时候的耗时基本在204 ms,优化还是很明显的。

3. 结论说明(LongAdder原理理解体会)

在实际应用中,选择合适的优化方法需要根据具体的业务逻辑和并发需求进行权衡和调整。这里我们针对LongAdder的优化进行说明一下,它是基于了 CAS 分段锁的思想实现的。线程去读写一个 LongAdder 类型的变量时,流程如下:

基于 Unsafe 提供的 CAS 操作 +valitale 去实现的。在 LongAdder 的父类 Striped64 中维护着一个 base 变量和一个 cell 数组,当多个线程操作一个变量的时候,先会在这个 base 变量上进行 cas 操作,当它发现线程增多的时候,就会使用 cell 数组。比如当 base 将要更新的时候发现线程增多(也就是调用 casBase 方法更新 base 值失败),那么它会自动使用 cell 数组,每一个线程对应于一个 cell ,在每一个线程中对该 cell 进行 cas 操作,这样就可以将单一 value 的更新压力分担到多个 value 中去,降低单个 value 的 “热度”,同时也减少了大量线程的空转,提高并发效率,分散并发压力。这种分段锁需要额外维护一个内存空间 cells ,不过在高并发场景下,这点成本几乎可以忽略。

我觉得可以把 LongAdder 想象成一个超市收银台系统:

  • base 变量:一个主收银台,所有顾客最开始都会排队在这里付款。
  • cell 数组:多个备用收银台,当主收银台忙不过来时,顾客会被分配到不同的备用收银台去付款。

这样做的好处是,当有大量顾客(高并发)时,大家不会都挤在一个收银台前,避免了长时间的等待,提高了结账效率。

二、回顾Java锁优化

Java 中的 synchronized 关键字和 java.util.concurrent.locks 包中的 Lock 接口(如 ReentrantLock)是两种常见的加锁方式,它们各有优缺点,针对这两种锁,JDK 自身做了很多的优化,它们的实现方式也是不同的。我们不妨进行简单的回顾一下。

(一)synchronized 关键字

synchronized 关键字给代码或者方法上锁时,都有显示或者隐藏的上锁对象。当一个线程试图访问同步代码块时,它首先必须得到锁,而退出或抛出异常时必须释放锁(注意是 synchronized 关键字的内置机制,由 Java 语言和 JVM 自动管理。我们在使用 synchronized 关键字时,无需手动编写获取和释放锁的代码,synchronized 会自动处理这些细节)。

  • 给普通方法加锁时:锁定对象是实例对象 (this)。相同实例的 synchronized 方法之间是串行的,不同实例之间则不会互相影响。

  • 给静态方法加锁时:锁定对象是类的 Class 对象。所有实例共享同一个类锁,因此静态 synchronized 方法在所有实例上都是串行的。

  • 给代码块加锁时:可以指定任意对象作为锁。锁的粒度更细,可以针对不同的对象分别加锁,灵活控制并发。

synchronized 的实现依赖于 JVM 和操作系统的原语,如监视器锁和信号量。JVM 在执行同步块时,会插入相应的加锁和解锁指令。

1. monitor 锁的实现原理

synchronized 关键字在 Java 字节码中通过监视器(Monitor)指令来实现。具体来说,当 Java 编译器编译包含 synchronized 关键字的代码时,会在相应的位置插入 monitorentermonitorexit 指令。JVM 在运行这些字节码时,会负责管理锁的获取和释放。

  • 对于同步实例方法,编译器在字节码中并不会显式插入 monitorentermonitorexit 指令。相反,它会在方法的访问标志(access flag)中添加 ACC_SYNCHRONIZED 标志。JVM 在调用该方法时会自动获取实例的监视器锁,并在方法返回或抛出异常时自动释放锁。
  • 对于同步静态方法,编译器同样会在方法的访问标志中添加 ACC_SYNCHRONIZED 标志。JVM 在调用该方法时会自动获取类的监视器锁,并在方法返回或抛出异常时自动释放锁。
  • 对于同步代码块,编译器会在进入同步块时插入 monitorenter 指令,在退出同步块时插入 monitorexit 指令。这些指令用于显式地获取和释放指定的锁对象。

现在我们直观的验证一下:

package org.zyf.javabasic.thread.lock.opti;/*** @program: zyfboot-javabasic* @description: 同步方法和同步代码块的示例类,以及它们在字节码中的表现。* @author: zhangyanfeng* @create: 2024-06-06 07:56**/
public class SynchronizedExample {private int count = 0;private final Object lock = new Object();// 同步实例方法public synchronized void increment() {count++;}// 同步静态方法public static synchronized void staticIncrement() {// 静态方法体}// 同步代码块public void blockIncrement() {synchronized (lock) {count++;}}
}

可以使用 javap -c SynchronizedExample 命令查看字节码如下:

public synchronized void increment();flags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=2, locals=1, args_size=10: aload_01: getfield      #2                  // Field count:I4: iconst_15: iadd6: aload_07: swap8: putfield      #2                  // Field count:I11: returnpublic static synchronized void staticIncrement();flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZEDCode:stack=0, locals=0, args_size=00: returnpublic void blockIncrement();flags: ACC_PUBLICCode:stack=2, locals=3, args_size=10: aload_01: getfield      #3                  // Field lock:Ljava/lang/Object;4: dup5: astore_16: monitorenter7: aload_08: dup9: getfield      #2                  // Field count:I12: iconst_113: iadd14: putfield      #2                 // Field count:I17: aload_118: monitorexit19: goto          2722: astore_223: aload_124: monitorexit25: aload_226: athrow27: returnException table:from    to  target type7    19    22   any22    25    22   any

正如我们上面总结的一样,整体来说:

  • 使用 ACC_SYNCHRONIZED 标志,JVM 自动处理锁的获取和释放。
  • 使用 monitorentermonitorexit 指令显式处理锁的获取和释放,确保在正常退出和异常退出时都能正确释放锁。

这两者虽然显示效果不同,但他们都是通过 monitor 来实现同步的。当一个线程进入一个同步块或同步方法时,它会尝试获取与该对象关联的 Monitor。如果 Monitor 已经被其他线程持有,当前线程将会阻塞,直到 Monitor 被释放。

虽然具体实现可能会根据不同的 JVM 实现有所差异,但通常可以用以下示意来表示 Monitor 的结构:

Monitor {Thread owner;          // 当前持有锁的线程int recursionCount;    // 递归计数器List<Thread> entryList; // 进入列表,等待获取锁的线程队列List<Thread> waitSet;   // 等待列表,调用 wait() 方法等待的线程队列List<Thread> exitList;  // 退出列表,等待退出的线程队列(可能的实现)
}

锁相关主要的流程如下:

初始状态:多个线程尝试进入同步代码块或方法,所有线程首先进入 EntryList 队列。这些线程处于“Waiting for monitor entry”状态( jstack 命令可查看)。

获取锁的过程:某个线程成功获取 Monitor 后,_owner 变量设置为当前线程,表示该线程持有锁。Monitor_count 计数器自增 1,表示锁被持有的次数。

等待和释放锁:持有 Monitor 的线程调用 wait() 方法时,会释放锁,并进入 WaitSet 队列。释放锁后,_owner 变量恢复为 null_count 计数器自减 1。线程状态变为“in Object.wait()”,等待被其他线程唤醒。

执行完成释放锁:持有 Monitor 的线程执行完同步代码块或方法时,会释放锁。释放锁后,_owner 变量恢复为 null_count 计数器自减 1。

唤醒等待的线程:线程调用 notify()notifyAll() 方法时,会唤醒 WaitSet 中的一个或多个线程。被唤醒的线程重新进入 EntryList 队列,等待再次获取 Monitor 锁。

2.分级锁

在 JDK 1.8 中,synchronized 关键字的性能得到了显著提升,这主要得益于 JVM 对锁机制进行了一系列优化:锁的分级及其优化路径(大体可以按照下面的路径进行升级:偏向锁 — 轻量级锁 — 重量级锁,锁只能升级,不能降级,所以一旦升级为重量级锁,就只能依靠操作系统进行调度)。

要想了解锁升级的过程,需要先看一下对象在内存里的结构。

在 Java 中,对象的内存布局中包含了 MarkWordClass PointerInstance DataPadding 等部分。而锁的升级过程主要与对象头的 MarkWord 有关。

要知道MarkWord 是对象头中用于存储对象的运行时信息的。在 64 位 JVM 中,MarkWord 的长度为 64 位。在 32 位 JVM 中,MarkWord 的长度为 32 位(如上图)。

偏向锁(Biased Locking)
  • 单线程高效使用锁:在只有一个线程使用锁的情况下,偏向锁效率最高。
  • 获取锁:第一个线程访问同步块时,会检查对象头 Mark Word 的标志位 Tag 是否为 01。如果是,线程将自己的线程 ID 写入 Mark Word,锁进入偏向锁状态。
  • 撤销偏向锁:当其他线程尝试获取锁时,如果 Mark Word 中的线程 ID 不匹配,偏向锁会被撤销,升级为轻量级锁。
  • 适合单线程场景,高效。
轻量级锁
  • 自旋锁获取:参与竞争的线程会在自己的线程栈中生成一个 LockRecord (LR),通过 CAS(自旋)的方式,将锁对象头中的 Mark Word 设置为指向自己的 LR 的指针。设置成功的线程获得锁。
  • 自旋失败:如果自旋多次失败,锁会升级为重量级锁。
  • 适合短期竞争,自旋获取锁。
重量级锁
  • 系统调度:重量级锁会导致线程挂起,进入操作系统内核态,等待操作系统调度,然后再映射回用户态。系统调用的开销很高,锁的膨胀到重量级锁就意味着性能下降。
  • 激烈竞争:如果共享变量竞争激烈,锁会迅速膨胀为重量级锁。如果并发竞争严重,可以使用 -XX:-UseBiasedLocking 禁用偏向锁,可能会有一些性能提升。
  • 适合长期竞争,但性能开销大。

3. 锁升级一览

(二)concurrent 包里面的 Lock

synchronized 是 Java 提供的最基本的同步机制,通过简单易用的语法确保线程安全。然而,随着并发需求的复杂化,Java 的并发包 (java.util.concurrent) 提供了更多高级和高效的并发工具,如 ReentrantLockReadWriteLockAtomic 类等,来应对更复杂的并发场景。在实际开发中,应根据具体情况选择合适的同步机制。现在我们聚焦在Lock进行分析。

1. 锁机制基于线程而不是基于调用(可重入锁)

这种锁机制基于线程而不是基于调用” 的意思是说,当一个线程持有锁时,它可以在同一个线程的不同调用链中多次获取同一把锁,而不会被阻塞或引发死锁。这是因为锁是跟线程关联的,而不是跟调用栈关联的。

假设有一个对象 example,它有三个同步方法 abc,每个方法都被 synchronized 关键字修饰:

package org.zyf.javabasic.thread.lock.opti;/*** @program: zyfboot-javabasic* @description: 锁机制基于线程而不是基于调用* @author: zhangyanfeng* @create: 2024-06-07 19:04**/
public class LockExample {public synchronized void a() {System.out.println("In method a");b();  // 调用 b 方法}public synchronized void b() {System.out.println("In method b");c();  // 调用 c 方法}public synchronized void c() {System.out.println("In method c");}public static void main(String[] args) {LockExample example = new LockExample();example.a();}
}

main 方法中,我们调用了 example.a()。在 a 方法内部,又调用了 b 方法,而 b 方法内部又调用了 c 方法。这种调用关系如下:

example.a()-> example.b()-> example.c()

当线程调用 example.a() 时,由于 a 方法是同步方法,线程必须先获得对象 example 的锁。在 a 方法内部,线程调用 example.b()。虽然 b 方法也是同步方法,但是由于当前线程已经持有了 example 对象的锁,所以它可以继续执行,不需要再次获取锁。类似地,在 b 方法内部,线程调用 example.c() 时,也不需要再次获取锁。

这个过程中锁是跟线程关联的,而不是跟每次调用关联的。一个线程持有锁之后,可以在它的调用栈中多次获取同一把锁,而不需要重新获取锁,也不会被阻塞。

如果 Java 的锁机制不是基于线程的,而是基于每次调用的,那么在上面的示例中,线程在调用 b 方法时会尝试再次获取 example 对象的锁,但是由于它已经持有这个锁,这将导致死锁。因此,基于线程的锁机制(即可重入锁)避免了这种情况,使得一个线程在持有锁时,可以多次获取同一把锁。Java 的 ReentrantLock 类也支持可重入性,将以上synchronized替换成其效果也是一样的。

像上面这样,在并发编程中,可重入锁(Reentrant Lock)指的是一个线程可以多次获得同一把锁。可重入锁的作用在于避免线程死锁,当一个线程已经持有了一个锁,再次请求该锁时可以直接获取,而无需再次等待。这种锁机制基于线程而不是基于调用。

2. Lock 主要方法

在 Java 的并发编程中,Lock 接口提供了比 synchronized 更加灵活和强大的锁机制。Lock 与 synchronized 的使用方法不同,它需要手动加锁,然后在 finally 中解锁。

Lock 接口是基于 AQS(AbstractQueuedSynchronizer)实现的,而 AQS 又依赖于 volatile 和 CAS(Compare-And-Swap)操作来实现线程的同步控制。其中AQS基本原理可见文章从ReentrantLock理解AQS的原理及应用总结,我们这里暂时增加一张原文图片进行体会理解:

​现在我们来看一下几个关键方法:

lock()

获取锁。如果锁已经被其他线程持有,则当前线程将被阻塞,直到获取到锁。和 synchronized 没什么区别,如果获取不到锁,都会被阻塞;

lock.lock();
try {// critical section
} finally {lock.unlock();
}
unlock()

释放锁。通常在 finally 块中调用,以确保锁在使用之后总是被释放。重申需要手动加锁,然后在 finally 中解锁,在 finally 中解锁,在 finally 中解锁。

tryLock()

尝试获取锁。如果锁可用,则获取锁并返回 true,否则返回 false。该方法不会阻塞线程。

if (lock.tryLock()) {try {// critical section} finally {lock.unlock();}
} else {// handle lock not acquired
}
tryLock(long timeout, TimeUnit unit)

尝试在给定的时间范围内获取锁。如果在指定时间内获取到锁,则返回 true,否则返回 false

if (lock.tryLock(1, TimeUnit.SECONDS)) {try {// critical section} finally {lock.unlock();}
} else {// handle lock not acquired
}
lockInterruptibly()

获取锁,但与 lock() 不同的是,这个方法允许响应中断。如果线程在等待锁的过程中被中断,则抛出 InterruptedException

try {lock.lockInterruptibly();try {// critical section} finally {lock.unlock();}
} catch (InterruptedException e) {// handle interrupt
}

平时开发中建议在需要及时响应的业务场景下使用带超时时间的 tryLock 方法。基本建议如下:

  • 普通场景:使用 lock() 方法即可,确保线程安全。
  • 高并发、及时响应场景:使用带超时时间的 tryLock 方法,保证服务的高可用性和快速响应能力。

3. 读写锁ReentrantReadWriteLock

在高并发场景下,对于一些业务来说,使用 Lock 这种粗粒度的锁可能会导致性能瓶颈。例如,对于一个 HashMap,如果业务场景是读多写少,给读操作加上和写操作一样的锁会大大降低效率。因为在这种情况下,读操作会频繁发生,而每次读操作都被迫等待写锁的释放,这样就大大降低了系统的吞吐量。

为了解决这类问题,我们可以使用 ReentrantReadWriteLock(一种读写分离的锁机制)。ReentrantReadWriteLock 提供了两种锁:读锁(ReadLock)和写锁(WriteLock),其核心思想是将读操作和写操作分离开来,从而提高系统的并发性能。

基本内容说明
  • 读锁:允许多个线程同时获取读锁,进行并发读操作。读锁之间是共享的,多个读线程可以同时读取而不会相互阻塞。
  • 写锁:只有一个线程可以获取写锁,进行写操作。写锁之间是互斥的,写线程会阻塞其他写线程。
  • 读写互斥:读操作和写操作之间是互斥的,当一个线程持有写锁时,其他线程不能获取读锁。这样保证了数据的一致性和线程安全。
性能验证说明

为了更直观地对比 ReentrantReadWriteLock 在读多写少场景中的性能优势,我们可以编写一个性能测试用例,比较使用 ReentrantReadWriteLockReentrantLock 的性能差异。

private static final int NUM_OPERATIONS = 10000;
private static final int NUM_THREADS = 10;public static void main(String[] args) throws InterruptedException {// Test with ReentrantLocklong startTime = System.currentTimeMillis();Thread[] threads = new Thread[NUM_THREADS];for (int i = 0; i < NUM_THREADS; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < NUM_OPERATIONS / NUM_THREADS; j++) {reentrantLock.lock();try {map.put("key" + j, "value" + j);} finally {reentrantLock.unlock();}}});}for (Thread thread : threads) {thread.start();}for (Thread thread : threads) {thread.join();}long endTime = System.currentTimeMillis();System.out.println("ReentrantLock Write Time: " + (endTime - startTime) + " ms");// Test with ReentrantReadWriteLockstartTime = System.currentTimeMillis();for (int i = 0; i < NUM_THREADS; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < NUM_OPERATIONS / NUM_THREADS; j++) {writeLock.lock();try {map.put("key" + j, "value" + j);} finally {writeLock.unlock();}}});}for (Thread thread : threads) {thread.start();}for (Thread thread : threads) {thread.join();}endTime = System.currentTimeMillis();System.out.println("ReentrantReadWriteLock Write Time: " + (endTime - startTime) + " ms");
}

运行结果如下:

  • ReentrantLock 的写入时间为 63 ms;
  • ReentrantReadWriteLock 的写入时间为 32 ms

这个结果看起来ReentrantReadWriteLock 的写入时间比 ReentrantLock 要快,这表明在这种情况下,读写分离的锁确实能够提高性能。

4.乐观读取、悲观读取和写入的机制:StampedLock

StampedLock 是在 Java 8 中引入的,它提供了一种乐观读取、悲观读取和写入的机制,可以比 ReentrantReadWriteLock 更高效地支持读写分离。StampedLock 不支持重入,因此在使用时需要特别注意避免死锁的情况。

基本内容说明

StampedLock 主要有三种锁模式:

  • 写锁(writeLock):与普通的互斥锁类似,一次只能被一个线程持有。当一个线程持有写锁时,任何其他线程试图获取写锁或者悲观读锁都会被阻塞。
  • 悲观读锁(readLock):与 ReentrantReadWriteLock 中的读锁类似,用于读取共享数据。当一个线程持有悲观读锁时,其他线程试图获取写锁的请求会被阻塞,但不会阻塞其他悲观读锁的获取请求。
  • 乐观读锁(tryOptimisticRead):是一种乐观的读取模式,允许多个线程同时访问共享数据,不会阻塞其他线程的写入操作。但在使用乐观读锁时,需要通过 validate 方法来验证读取操作是否有效。

通过合理地选择锁模式,可以使 StampedLock 在某些情况下比传统的读写锁更高效。

性能验证说明

使用 StampedLockReentrantReadWriteLock 来实现读写分离,并比较它们的性能:

package org.zyf.javabasic.thread.lock.opti;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.StampedLock;/*** @program: zyfboot-javabasic* @description: 使用 StampedLock 和 ReentrantReadWriteLock 来实现读写分离,并比较它们的性能。* @author: zhangyanfeng* @create: 2024-06-07 22:35**/
public class StampedLockVsReentrantReadWriteLock {private static final int NUM_THREADS = 200;private static final int NUM_OPERATIONS = 5000000;private static volatile int sharedVariable = 0;public static void main(String[] args) throws InterruptedException {long start, end;// Test with StampedLockStampedLock stampedLock = new StampedLock();start = System.currentTimeMillis();Thread[] stampedLockWriteThreads = new Thread[NUM_THREADS];Thread[] stampedLockReadThreads = new Thread[NUM_THREADS];for (int i = 0; i < NUM_THREADS; i++) {stampedLockWriteThreads[i] = new Thread(() -> {for (int j = 0; j < NUM_OPERATIONS; j++) {long stamp = stampedLock.writeLock();try {sharedVariable++;} finally {stampedLock.unlockWrite(stamp);}}});stampedLockWriteThreads[i].start();stampedLockReadThreads[i] = new Thread(() -> {for (int j = 0; j < NUM_OPERATIONS; j++) {long stamp = stampedLock.readLock();try {int value = sharedVariable;} finally {stampedLock.unlockRead(stamp);}}});stampedLockReadThreads[i].start();}for (Thread thread : stampedLockWriteThreads) {thread.join();}for (Thread thread : stampedLockReadThreads) {thread.join();}end = System.currentTimeMillis();System.out.println("StampedLock Write and Read Time: " + (end - start) + " ms");// Test with ReentrantReadWriteLockReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();Lock writeLock = rwLock.writeLock();Lock readLock = rwLock.readLock();start = System.currentTimeMillis();Thread[] rwLockWriteThreads = new Thread[NUM_THREADS];Thread[] rwLockReadThreads = new Thread[NUM_THREADS];for (int i = 0; i < NUM_THREADS; i++) {rwLockWriteThreads[i] = new Thread(() -> {for (int j = 0; j < NUM_OPERATIONS; j++) {writeLock.lock();try {sharedVariable++;} finally {writeLock.unlock();}}});rwLockWriteThreads[i].start();rwLockReadThreads[i] = new Thread(() -> {for (int j = 0; j < NUM_OPERATIONS; j++) {readLock.lock();try {int value = sharedVariable;} finally {readLock.unlock();}}});rwLockReadThreads[i].start();}for (Thread thread : rwLockWriteThreads) {thread.join();}for (Thread thread : rwLockReadThreads) {thread.join();}end = System.currentTimeMillis();System.out.println("ReentrantReadWriteLock Write and Read Time: " + (end - start) + " ms");}
}

运行结果如下:

  • StampedLock 的写入和读取时间为 21200 ms;
  • ReentrantReadWriteLock 的写入和读取时间为 26779 ms

结果来看StampedLock 的性能优于 ReentrantReadWriteLock。StampedLock 在这种情况下的表现更好,这与其内部实现机制有关。StampedLock 使用乐观读锁来提高并发性,而 ReentrantReadWriteLock 使用悲观读锁。在适当的情况下,StampedLock 能够更高效地支持读写分离,这通常在读操作远远多于写操作的情况下更为明显。

5. 公平锁与非公平锁

非公平锁允许在释放锁时不考虑等待队列中的其他线程的情况,新的线程可以立即争抢锁。这种情况下,可能存在某些线程总是无法获取锁的情况,造成线程饥饿。

相反,公平锁确保等待时间最长的线程会被优先选择来获取锁,这样每个线程都有机会获取到锁,避免了饥饿现象。在公平锁中,线程按照请求锁的顺序进入队列,并按顺序获取锁。

synchronized关键字 vs Lock接口

在Java中,synchronized关键字使用的是非公平锁,而在Lock接口的实现类中,可以通过构造函数来选择使用公平锁或非公平锁。

public ReentrantReadWriteLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();readerLock = new ReadLock(this);writerLock = new WriteLock(this);
}

公平锁的实现需要维护一个有序的等待队列,因此在多核场景下,可能会降低吞吐量。这个在一开始讲Lock的时候我们就已经提过了,这里图在放一下:

功能验证
package org.zyf.javabasic.thread.lock.opti;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @program: zyfboot-javabasic* @description: 比较非公平锁和公平锁的性能* @author: zhangyanfeng* @create: 2024-06-08 00:07**/
public class LockPerformanceComparison {private static final int NUM_THREADS = 10;private static final int NUM_ITERATIONS = 1000000;private static final Lock unfairLock = new ReentrantLock();private static final Lock fairLock = new ReentrantLock(true); // 公平锁public static void main(String[] args) {System.out.println("Unfair Lock Performance Test");testLock(unfairLock);System.out.println("Fair Lock Performance Test");testLock(fairLock);}private static void testLock(Lock lock) {long startTime = System.currentTimeMillis();Thread[] threads = new Thread[NUM_THREADS];for (int i = 0; i < NUM_THREADS; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < NUM_ITERATIONS; j++) {lock.lock();try {// 模拟一些计算或任务Math.pow(Math.random(), Math.random());} finally {lock.unlock();}}});}for (Thread thread : threads) {thread.start();}for (Thread thread : threads) {try {thread.join();} catch (InterruptedException e) {e.printStackTrace();}}long endTime = System.currentTimeMillis();System.out.println("Execution Time: " + (endTime - startTime) + " ms");}
}

运行结果如下:

  • Unfair Lock Performance Test:Execution Time: 3338 ms
  • Fair Lock Performance Test:Execution Time: 26218 ms

可以看到非公平锁(Unfair Lock)的性能明显优于公平锁(Fair Lock)。这是因为公平锁需要维护一个有序的等待队列,以确保线程按照它们的请求顺序获得锁。相比之下,非公平锁允许线程在锁可用时立即获取锁,而不考虑它们的请求顺序,因此效率更高。

在实际应用中,如果不需要严格的线程调度顺序,通常会选择使用非公平锁来获得更好的性能。

(三)Java 中两种加锁方式对比和建议

类别SynchronizedLock
实现方式monitorAQS
底层细节JVM优化Java API
分级锁
功能特性单一丰富
锁分离读写锁
锁超时带超时时间的 tryLock
可中断lockInterruptibly

Lock 的功能是比 Synchronized 多的,能够对线程行为进行更细粒度的控制。但如果只是用最简单的锁互斥功能,建议直接使用 Synchronized,有两个原因:

  • Synchronized 的编程模型更加简单,更易于使用
  • Synchronized 引入了偏向锁,轻量级锁等功能,能够从 JVM 层进行优化,同时JIT 编译器也会对它执行一些锁消除动作。

三、锁的优化手段

Java 中有两种加锁的方式:一种就是常见的synchronized 关键字,另外一种,就是使用 concurrent 包里面的 Lock。针对这两种锁,JDK 自身做了很多的优化,它们的实现方式也是不同的。这个在上文中已经讲了很多,我们体会的已经比较深了,现在站在巨人的肩膀上总结一下优化的一般思路:减少锁的粒度、减少锁持有的时间、锁分级、锁分离 、锁消除、乐观锁、无锁等。

(一)减少锁的粒度

锁粒度(Lock Granularity)指的是锁定的资源范围大小。锁的粒度越大,意味着锁定的资源范围越广,可能会导致更多的线程阻塞等待锁的释放;锁的粒度越小,意味着锁定的资源范围越窄,可以减少线程的阻塞和等待时间。

减少锁粒度是指通过细化锁的范围和控制的资源,来减少线程之间的冲突和竞争,从而提高并发性和系统性能。

假设我们有一个大型整数数组,多个线程需要同时对该数组进行读写操作。我们可以将数组分成多个段,每个段使用一个独立的锁,从而允许不同线程并行访问不同段的数据。

先看使用单个锁的 SingleLockArray:使用一个全局锁来保护整个数组。所有线程在访问数组时都需要获取这把锁,因此会导致更多的锁竞争和阻塞。

package org.zyf.javabasic.thread.lock.opti;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @program: zyfboot-javabasic* @description: 使用一个全局锁来保护整个数组。所有线程在访问数组时都需要获取这把锁,因此会导致更多的锁竞争和阻塞。* @author: zhangyanfeng* @create: 2024-06-08 01:02**/
public class SingleLockArray {private static final int ARRAY_SIZE = 10000;private final int[] array = new int[ARRAY_SIZE];private final Lock lock = new ReentrantLock();public void increment(int index) {lock.lock();try {array[index]++;} finally {lock.unlock();}}public int get(int index) {lock.lock();try {return array[index];} finally {lock.unlock();}}
}

在看使用分段锁的 SegmentLockArray:将数组分成多个段,每个段使用一个独立的锁。这样,当多个线程访问不同段的数据时,可以并行执行,而不会相互阻塞。

package org.zyf.javabasic.thread.lock.opti;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @program: zyfboot-javabasic* @description: 将数组分成多个段,每个段使用一个独立的锁。这样,当多个线程访问不同段的数据时,可以并行执行,而不会相互阻塞* @author: zhangyanfeng* @create: 2024-06-08 01:06**/
public class SegmentLockArray {private static final int NUM_SEGMENTS = 10;private static final int SEGMENT_SIZE = 1000;private static final int ARRAY_SIZE = NUM_SEGMENTS * SEGMENT_SIZE;private final int[] array = new int[ARRAY_SIZE];private final Lock[] locks = new ReentrantLock[NUM_SEGMENTS];public SegmentLockArray() {for (int i = 0; i < NUM_SEGMENTS; i++) {locks[i] = new ReentrantLock();}}public void increment(int index) {int segment = index / SEGMENT_SIZE;locks[segment].lock();try {array[index]++;} finally {locks[segment].unlock();}}public int get(int index) {int segment = index / SEGMENT_SIZE;locks[segment].lock();try {return array[index];} finally {locks[segment].unlock();}}}

进行验证说明

package org.zyf.javabasic.thread.lock.opti;import java.util.concurrent.ThreadLocalRandom;/*** @program: zyfboot-javabasic* @description: 对比* @author: zhangyanfeng* @create: 2024-06-08 01:07**/
public class LockArrayPerComparison {private static final int NUM_SEGMENTS = 10;private static final int SEGMENT_SIZE = 1000;private static final int ARRAY_SIZE = NUM_SEGMENTS * SEGMENT_SIZE;public static void main(String[] args) throws InterruptedException {SegmentLockArray array = new SegmentLockArray();int numThreads = 100;Thread[] threads = new Thread[numThreads];// Writing threadsfor (int i = 0; i < numThreads / 2; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < 10000; j++) {int index = ThreadLocalRandom.current().nextInt(ARRAY_SIZE);array.increment(index);}});}// Reading threadsfor (int i = numThreads / 2; i < numThreads; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < 10000; j++) {int index = ThreadLocalRandom.current().nextInt(ARRAY_SIZE);array.get(index);}});}long startTime = System.currentTimeMillis();for (Thread thread : threads) {thread.start();}for (Thread thread : threads) {thread.join();}long endTime = System.currentTimeMillis();System.out.println("Execution Time with Segment Locks: " + (endTime - startTime) + " ms");// Compare with single lockSingleLockArray singleLockArray = new SingleLockArray();Thread[] singleLockThreads = new Thread[numThreads];// Writing threadsfor (int i = 0; i < numThreads / 2; i++) {singleLockThreads[i] = new Thread(() -> {for (int j = 0; j < 10000; j++) {int index = ThreadLocalRandom.current().nextInt(ARRAY_SIZE);singleLockArray.increment(index);}});}// Reading threadsfor (int i = numThreads / 2; i < numThreads; i++) {singleLockThreads[i] = new Thread(() -> {for (int j = 0; j < 10000; j++) {int index = ThreadLocalRandom.current().nextInt(ARRAY_SIZE);singleLockArray.get(index);}});}startTime = System.currentTimeMillis();for (Thread thread : singleLockThreads) {thread.start();}for (Thread thread : singleLockThreads) {thread.join();}endTime = System.currentTimeMillis();System.out.println("Execution Time with Single Lock: " + (endTime - startTime) + " ms");}
}

运行结果如下:

  • Execution Time with Segment Locks: 94 ms
  • Execution Time with Single Lock: 40 ms

可以看到使用分段锁的 SegmentLockArray 的执行时间明显优于使用单个锁的 SingleLockArray,特别是在高并发的场景下。这样,通过减少锁粒度,我们可以显著提高系统的并发性能。

(二)减少锁持有时间

通过让锁资源尽快地释放,减少锁持有的时间,其他线程可更迅速地获取锁资源,进行其他业务的处理。

我们使用两个计数器类,一个是未优化的版本,另一个是优化后的版本。

未优化的版本(UnoptimizedCounter):具体代码如下展示,在锁定的代码块中进行了模拟的工作(Thread.sleep(1)),导致锁的持有时间较长,从而增加了锁竞争和线程阻塞的时间。

package org.zyf.javabasic.thread.lock.opti;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @program: zyfboot-javabasic* @description: 未优化的版本* @author: zhangyanfeng* @create: 2024-06-08 01:22**/
public class UnoptimizedCounter {private int count = 0;private final Lock lock = new ReentrantLock();public void increment() {lock.lock();try {// Simulate some work that doesn't need to be lockedThread.sleep(1);count++;} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}public int getCount() {lock.lock();try {return count;} finally {lock.unlock();}}
}

优化后的版本(OptimizedCounter):将模拟的工作(`Thread.sleep(1))移到了锁定代码块之外。锁的持有时间大幅减少,锁定代码块仅包含了必要的操作(count++),减少了锁竞争和线程阻塞的时间,从而提高了系统的并发性能。

package org.zyf.javabasic.thread.lock.opti;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @program: zyfboot-javabasic* @description: 优化后的版本* @author: zhangyanfeng* @create: 2024-06-08 01:23**/
public class OptimizedCounter {private int count = 0;private final Lock lock = new ReentrantLock();public void increment() {try {// Simulate some work that doesn't need to be lockedThread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}lock.lock();try {count++;} finally {lock.unlock();}}public int getCount() {lock.lock();try {return count;} finally {lock.unlock();}}
}

我们将通过创建多个线程对这两个计数器进行并发操作,并测量它们的执行时间:

package org.zyf.javabasic.thread.lock.opti;/*** @program: zyfboot-javabasic* @description: LockPerformanceTest* @author: zhangyanfeng* @create: 2024-06-08 01:24**/
public class LockTimePerformanceTest {private static final int NUM_THREADS = 10;private static final int NUM_ITERATIONS = 1000;public static void main(String[] args) throws InterruptedException {System.out.println("UnoptimizedCounter Performance Test");UnoptimizedCounter unoptimizedCounter = new UnoptimizedCounter();testCounterPerformance(unoptimizedCounter);System.out.println("OptimizedCounter Performance Test");OptimizedCounter optimizedCounter = new OptimizedCounter();testCounterPerformance(optimizedCounter);}private static void testCounterPerformance(Object counter) throws InterruptedException {Thread[] threads = new Thread[NUM_THREADS];long startTime = System.currentTimeMillis();for (int i = 0; i < NUM_THREADS; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < NUM_ITERATIONS; j++) {if (counter instanceof UnoptimizedCounter) {((UnoptimizedCounter) counter).increment();} else if (counter instanceof OptimizedCounter) {((OptimizedCounter) counter).increment();}}});threads[i].start();}for (Thread thread : threads) {thread.join();}long endTime = System.currentTimeMillis();System.out.println("Execution Time: " + (endTime - startTime) + " ms");if (counter instanceof UnoptimizedCounter) {System.out.println("Final count (Unoptimized): " + ((UnoptimizedCounter) counter).getCount());} else if (counter instanceof OptimizedCounter) {System.out.println("Final count (Optimized): " + ((OptimizedCounter) counter).getCount());}}
}

运行结果如下:

  • UnoptimizedCounter Performance Test:Execution Time: 12656 ms
  • OptimizedCounter Performance Test:Execution Time: 1254 ms

优化后的版本(OptimizedCounter)应该会显示更短的执行时间,从而验证了减少锁持有时间对性能的优化效果。

(三)锁分级

JVM中的锁分级机制主要包括偏向锁、轻量级锁和重量级锁。这些锁的分级和升级是为了在不同的线程竞争情况下,尽可能地减少锁的开销和提升性能。

锁的升级过程是不可逆的,即从偏向锁到轻量级锁,再到重量级锁。如果一个锁已经升级为重量级锁,即使之后的竞争减少,也不会降级回轻量级锁或偏向锁。这样做是为了简化JVM的实现,避免复杂的锁降级逻辑带来的额外开销。

这个在上文第二部分的开头就已经讲过了。

(四)锁分离

锁分离是一种针对不同类型操作进行区分的优化技术,通过使用不同的锁来分别处理读操作和写操作,来提高并发性能。读写锁(ReentrantReadWriteLock)就是一种典型的锁分离技术的实现。锁分离的核心思想在于,读操作和写操作对资源的影响不同,可以采用不同的锁策略来优化性能。

其优化版本StampedLock 的性能优于 ReentrantReadWriteLock,StampedLock 使用乐观读锁来提高并发性,而 ReentrantReadWriteLock 使用悲观读锁。在适当的情况下,StampedLock 能够更高效地支持读写分离,这通常在读操作远远多于写操作的情况下更为明显。

具体验证代码在上方第二部分的Lock中已经给出了样例,请使用中自行选择。

(五)锁消除

锁消除(Lock Elimination)是指 JVM 通过分析代码的运行范围,判断出某些锁在多线程环境下没有竞争,因此可以去掉这些锁操作。这个过程是由 JVM 在运行时通过即时编译器(JIT 编译器)和逃逸分析来决定的。

逃逸分析(Escape Analysis)是锁消除的关键技术。它分析对象的作用范围,判断对象是否会逃逸出方法或线程。如果某个对象只在方法内部使用,并且不会逃逸出这个方法,则认为这个对象是线程私有的,锁操作就没有实际意义,可以消除。

考虑以下两个字符串拼接的示例:

package org.zyf.javabasic.thread.lock.opti;/*** @program: zyfboot-javabasic* @description: 两个字符串拼接的示例* @author: zhangyanfeng* @create: 2024-06-08 01:49**/
public class LockEliminationExample {public static void main(String[] args) {long startTime;long endTime;// Test with StringBufferstartTime = System.currentTimeMillis();for (int i = 0; i < 1000000; i++) {concatenateStringBuffer("Hello", "World");}endTime = System.currentTimeMillis();System.out.println("StringBuffer Execution Time: " + (endTime - startTime) + " ms");// Test with StringBuilderstartTime = System.currentTimeMillis();for (int i = 0; i < 1000000; i++) {concatenateStringBuilder("Hello", "World");}endTime = System.currentTimeMillis();System.out.println("StringBuilder Execution Time: " + (endTime - startTime) + " ms");}public static String concatenateStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString();}public static String concatenateStringBuilder(String s1, String s2) {StringBuilder sb = new StringBuilder();sb.append(s1);sb.append(s2);return sb.toString();}
}

StringBufferStringBuilder 都用于字符串拼接,但 StringBuffer 是线程安全的,通过内部使用的同步机制(锁)来保证线程安全,而 StringBuilder 则是非线程安全的,没有同步机制。

当 JVM 通过逃逸分析发现 StringBuffer 对象没有逃逸出方法时,就会将其锁操作消除,从而使得它的性能和 StringBuilder 接近。

(六)乐观锁

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)

​在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。具体的理论和使用验证可见:Java中常用的锁总结与理解

当然最直接的可见:超越并发瓶颈:CAS与乐观锁的智慧应用

(七)无锁

无锁队列是一种在多线程环境下访问共享资源的方式,它不会阻塞其他线程的执行。在 Java 中,最典型的无锁队列实现是 ConcurrentLinkedQueue,它使用CAS(Compare and Swap)指令来处理对数据的并发访问。CAS指令是一种非阻塞的原子操作,不会引起上下文切换和线程调度,因此是一种非常轻量级的多线程同步机制。

ConcurrentLinkedQueue的实现基于CAS机制,它将队列的入队和出队等操作拆分为更细粒度的步骤,进一步减小了CAS控制的范围,提高了并发性能。与之相对应的是阻塞队列LinkedBlockingQueue,它内部使用锁机制来实现同步,因此性能没有无锁队列高。

除了ConcurrentLinkedQueue外,还有一种无锁队列框架叫做Disruptor,它是一个无锁、有界的队列框架,具有极高的性能。Disruptor使用RingBuffer、无锁和缓存行填充等技术来实现高效的并发访问,适用于极高并发的场景,比如日志、消息等中间件。尽管Disruptor的编程模型相对复杂,但在需要极致性能的场景下,可以取代传统的阻塞队列。

四、总结

在《Java 同步锁性能的最佳实践:从理论到实践的完整指南》中,我们深入探讨了Java中同步锁的性能及其优化手段,从基础理论到实际应用案例,为开发者提供了全面的指导。

首先,我们分析了同步锁的性能,使用代码示例验证了使用与不使用同步锁对程序性能的影响,明确了同步锁在多线程环境中的必要性和影响因素。通过案例初步优化分析,运用AtomicInteger进行性能优化,进一步引入LongAdder的原理,使读者在实际开发中能够更好地应对高并发场景下的性能问题。

接着,我们回顾了Java锁的基本概念与实现机制,包括synchronized关键字的监视器锁实现原理、锁的分级以及锁升级的过程。对比了Java concurrent包中的Lock接口,包括可重入锁、读写锁和StampedLock的机制,强调了不同锁机制在性能上的差异及其适用场景。

最后,我们总结了锁的优化手段,包括减少锁的粒度、持有时间、锁分级、锁分离、锁消除、乐观锁以及无锁技术等。这些优化策略不仅提升了程序的性能,也为并发编程提供了更为灵活的解决方案。

通过本指南,开发者不仅能够掌握Java中同步锁的基本使用,还能深入理解各种锁的优缺点及其适用场景,为在多线程环境下构建高效、稳定的应用程序奠定了坚实的基础。在未来的开发中,灵活运用这些优化手段,将为提高系统性能提供更多可能性。

参考文章

Java synchronized 关键字(3)-JVM 重量级锁 Monitor 的实现_jvm中monitor的实现原理-CSDN博客

JAVA多线程学习-------monitor所实现原理_java objectmonitor 原理-CSDN博客

Monitor工作原理&synchronized锁膨胀过程及其优化_rom monitor软件的工作原理-CSDN博客

synchronized底层monitor原理_synchronized monitor原理-CSDN博客

https://zhuanlan.zhihu.com/p/581418806

https://juejin.cn/post/7131739806156980232

https://www.51cto.com/article/743092.html

浅谈一下Java中的几种JVM级别的锁_java_脚本之家

聊聊 Java 的几把 JVM 级锁_jvm层次的锁都有哪些-CSDN博客

https://www.cnblogs.com/wzj4858/p/8215369.html

Java基础-2、并发_分段锁的原理,锁力度减小的思考-CSDN博客

JAVA锁机制-可重入锁,可中断锁,公平锁,读写锁,自旋锁_可中断锁实现原理-CSDN博客

Java中的线程和锁机制_第三 提供的接 审核threadlocal,synchronized等锁机制以及线程池等技术的底-CSDN博客

ReentrantReadWriteLock读写锁-CSDN博客

读写锁ReentrantReadWriteLock&StampLock详解_reentrantreadwritelock和stampedlock-CSDN博客

https://www.cnblogs.com/xiaoxi/p/9140541.html

ReentrantReadWriteLock.ReadLock (Java 2 Platform SE 5.0)

深入理解读写锁ReentrantReadWriteLock_读写锁会产生什么问题-CSDN博客

Java并发编程学习篇3_读写锁ReadWriteLock、阻塞队列BlockingQueue、同步队列SynchronousQueue、线程池(三大方法、七大参数、四种拒绝策略、原生方式创建线程池)_reentrantreadwrihtlock有队列吗-CSDN博客

StampedLock (Java Platform SE 8 )

StampedLock Class (Java.Util.Concurrent.Locks) | Microsoft Learn

https://www.cnblogs.com/myworld7/p/12332911.html

StampedLock (Java SE 19 & JDK 19 [build 1])

https://www.cnblogs.com/zhujiqian/p/16898222.html

开放平台

一文搞懂ReentrantLock的公平锁和非公平锁_ReentrantLock_Ayue、_InfoQ写作社区

Java多线程 -- 公平锁和非公平锁的一些思考-阿里云开发者社区

码农会锁,ReentrantLock之公平锁讲解和实现 | HeapDump性能社区

滑动验证页面

深入剖析 ReentrantLock 公平锁与非公平锁源码实现 - AIQ

13 案例分析:多线程锁的优化

https://blog.51cto.com/panyujie/5478573

Synchronize锁优化手段有哪些_牛客博客

相关文章:

Java 同步锁性能的最佳实践:从理论到实践的完整指南

目录 一、同步锁性能分析 &#xff08;一&#xff09;性能验证说明 1. 使用同步锁的代码示例 2. 不使用同步锁的代码示例 3. 结果与讨论 &#xff08;二&#xff09;案例初步优化分析说明 1. 使用AtomicInteger原子类尝试优化分析 2. 对AtomicInteger原子类进一步优化 …...

思科、华为、H3C常用命令对照表

取消/关闭 思科no华为undo华三undo 查看 思科show华为display华三display 退出 思科exit华为quit华三quit 设备命名 思科hostname华为sysname华三sysname 进入全局模式 思科enable、config terminal华为system-view华三system-view 删除文件 思科delete华为delete华…...

[qt5学习笔记]Application Example示例程序源码解析

开发环境问题 vs2022下直接打开ui、ts文件失败 解决办法如下图&#xff0c; 设置designer独立运行。估计是嵌入运行存在些许bug。 同理&#xff0c;ts编辑工具linguist也存在这个问题。 qrc rc的编辑嵌入编辑都正常&#xff0c;但分离式更稳定可靠。 qt creator编译失败 原…...

华为交换机堆叠技术简介配置

目录 一、华为堆叠技术简介&#xff08;一&#xff09;提高可靠性&#xff08;二&#xff09;扩展端口数量&#xff08;三&#xff09;增大带宽&#xff08;四&#xff09;简化组网&#xff08;五&#xff09;长距离堆叠 二、华为交换机堆叠技术的案例及命令配置&#xff08;一…...

腿足机器人之四- 卡尔曼滤波

腿足机器人之四卡尔曼滤波 概率学基础贝叶斯准则熵 卡尔曼滤波扩展卡尔曼滤波信息滤波器 在机器人&#xff08;四足、人形&#xff09;领域&#xff0c;感知和行动的不确定性可能由多种因素引起&#xff0c;如传感器噪声、外部环境的变化、非精确控制以及实时性算力限制等。 和…...

nginx 部署前端vue项目

&#x1f468;‍⚕ 主页&#xff1a; gis分享者 &#x1f468;‍⚕ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕ 收录于专栏&#xff1a;前端工程师 文章目录 一、&#x1f353;什么是nginx&#xff1f;二、&#x1f353;nginx 部署…...

【第1章:深度学习概览——1.6 深度学习框架简介与选择建议】

嘿,各位老铁们,今天咱们来一场深度学习框架的深度探索之旅。在这个充满无限可能的深度学习时代,深度学习框架就像是连接理论与实践的桥梁,帮助我们从算法设计走向实际应用。随着技术的飞速发展,深度学习框架的选择变得越来越多样化,每一种框架都有其独特的优势和适用场景…...

NLLB 与 ChatGPT 双向优化:探索翻译模型与语言模型在小语种应用的融合策略

作者&#xff1a;来自 vivo 互联网算法团队- Huang Minghui 本文探讨了 NLLB 翻译模型与 ChatGPT 在小语种应用中的双向优化策略。首先介绍了 NLLB-200 的背景、数据、分词器和模型&#xff0c;以及其与 LLM&#xff08;Large Language Model&#xff09;的异同和协同关系。接着…...

C#的委托delegate与事件event

在C#中&#xff0c;delegate&#xff08;委托&#xff09;和 event&#xff08;事件&#xff09;是两个非常重要的概念&#xff0c;它们主要用于实现回调机制和事件驱动编程。下面详细介绍它们的原理和使用场景。 1. Delegate&#xff08;委托&#xff09; 1.1 委托的原理 委托…...

Spring Boot 集成MyBatis-Plus

文章目录 一、背景说明二、集成过程 2.1 引入 maven 依赖2.2 增加属性配置2.3 自动配置类 三、验证集成 3.1 控制器3.2 服务类3.3 Mapper接口类3.4 实体类3.4 不要忘记XML文件3.5 发起请求 四、技巧拓展 4.1 如何打印sql语句&#xff1f;4.2 如何对参数增加非空验证&#xff1f…...

javacv将视频切分为m3u8视频并播放

学习链接 ffmpeg-demo 当前对应的 gitee代码 Spring boot视频播放(解决MP4大文件无法播放)&#xff0c;整合ffmpeg,用m3u8切片播放。 springboot 通过javaCV 实现mp4转m3u8 上传oss 如何保护会员或付费视频&#xff1f;优酷是怎么做的&#xff1f; - HLS 流媒体加密 ffmpe…...

Docker 入门与实战:从安装到容器管理的完整指南

&#x1f680; Docker 入门与实战&#xff1a;从安装到容器管理的完整指南 &#x1f31f; &#x1f4d6; 简介 在现代软件开发中&#xff0c;容器化技术已经成为不可或缺的一部分。而 Docker 作为容器化领域的领头羊&#xff0c;以其轻量级、高效和跨平台的特性&#xff0c;深…...

计算机视觉:卷积神经网络(CNN)基本概念(二)

第一章&#xff1a;计算机视觉中图像的基础认知 第二章&#xff1a;计算机视觉&#xff1a;卷积神经网络(CNN)基本概念(一) 第三章&#xff1a;计算机视觉&#xff1a;卷积神经网络(CNN)基本概念(二) 第四章&#xff1a;搭建一个经典的LeNet5神经网络 接上一篇《计算机视觉&am…...

SQL SERVER的PARTITION BY应用场景

SQL SERVER的PARTITION BY关键字说明介绍 PARTITION BY关键字介绍具体使用场景排名计算累计求和分组求最值分组内百分比计算分组内移动平均计算分组内数据分布统计分组内数据偏移计算 总结 PARTITION BY关键字介绍 在SQL SERVER中&#xff0c;关键字PARTITION BY主要用于窗口函…...

【ISO 14229-1:2023 UDS诊断全量测试用例清单系列:第十二节】

ISO 14229-1:2023 UDS诊断服务测试用例全解析&#xff08;TesterPresent_0x3E服务&#xff09; 作者&#xff1a;车端域控测试工程师 更新日期&#xff1a;2025年02月14日 关键词&#xff1a;UDS协议、0x3E服务、会话保持、ISO 14229-1:2023、ECU测试 一、服务功能概述 0x3E服…...

gsoap实现webservice服务

gsoap实现webservice服务 在实现Web服务时&#xff0c;使用gSOAP是一个很好的选择&#xff0c;因为它提供了强大的工具和库来创建SOAP和RESTful服务。gSOAP是一个C和C语言开发的库&#xff0c;它支持SOAP协议的各种版本&#xff0c;包括SOAP 1.1和SOAP 1.2。下面是如何使用gSO…...

达梦:dmserver占用io高排查

目录标题 1. 使用达梦数据库的性能视图查询当前活动会话查询执行时间较长的 SQL 2. 使用 DM 性能监视工具3. 使用操作系统工具监控 I/Oiostat 工具dstat 工具 4. 优化查询和索引审查 SQL 执行计划优化索引 5. 调整数据库参数6. 分析数据库日志7. 硬件和存储检查总结 针对达梦数…...

MoE架构中的专家选择门控机制:稀疏激活如何实现百倍效率突破?

技术原理&#xff08;数学公式与核心逻辑&#xff09; 核心公式 门控网络输出&#xff1a; G ( x ) Softmax ( W g ⋅ x b g ) G(x) \text{Softmax}(W_g \cdot x b_g) G(x)Softmax(Wg​⋅xbg​) 最终输出&#xff1a; y ∑ i 1 n G i ( x ) ⋅ E i ( x ) (仅保留Top-…...

用python写一个聊天室程序

下面是一个简单的基于Socket的Python聊天室程序示例&#xff0c;包括服务器端和客户端&#xff1a; 服务器端代码&#xff1a; import socket import threadingdef handle_client(client, address):print(f"New connection from {address}")while True:msg client…...

七星棋牌全开源修复版源码解析:6端兼容,200种玩法全面支持

本篇文章将详细讲解 七星棋牌修复版源码 的 技术架构、功能实现、二次开发思路、搭建教程 等内容&#xff0c;助您快速掌握该棋牌系统的开发技巧。 1. 七星棋牌源码概述 七星棋牌修复版源码是一款高度自由的 开源棋牌项目&#xff0c;该版本修复了原版中的多个 系统漏洞&#…...

Vulhub靶机 ActiveMQ任意 文件写入(CVE-2016-3088)(渗透测试详解)

一、开启vulhub环境 docker-compose up -d 启动 docker ps 查看开放的端口 漏洞版本&#xff1a;ActiveMQ在5.14.0之前的版本&#xff08;不包括5.14.0&#xff09; 二、访问靶机IP 8161端口 默认账户密码都是admin 1、利用bp抓包&#xff0c;修改为PUT方法并在fileserver…...

Cloud: aws:network: limit 含有pps这种限制

https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/troubleshooting-ena.html#statistics-ena 这个是调查网络问题的一个网页; 在里面,竟然含有pps这种限制:ethtool -S;其实是比较苛刻的安全相关的策略? [ec2-user ~]$ ethtool -S ethN NIC statistics:tx_timeout: …...

28、深度学习-自学之路-NLP自然语言处理-做一个完形填空,让机器学习更多的内容程序展示

import sys,random,math from collections import Counter import numpy as npnp.random.seed(1) random.seed(1) f open(reviews.txt) raw_reviews f.readlines() f.close()tokens list(map(lambda x:(x.split(" ")),raw_reviews))#wordcnt Counter() 这行代码的…...

观察者模式说明(C语言版本)

观察者模式主要是为了实现一种一对多的依赖关系&#xff0c;让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时&#xff0c;会通知所有观察者对象&#xff0c;使它们能够自动更新自己。下面使用C语言实现了一个具体的应用示例&#xff0c;有需要的可以参考…...

LC-搜索二维矩阵II、相交链表、反转链表、回文链表、环形链表、环形链表ll

搜索二维矩阵II 方法&#xff1a;从右上角开始搜索 我们可以从矩阵的右上角开始进行搜索。如果当前元素 matrix[i][j] 等于 target&#xff0c;我们直接返回 true。如果 matrix[i][j] 大于 target&#xff0c;说明 target 只能出现在左边的列&#xff0c;所以我们将列指针向左…...

如何查看 Linux 服务器的 MAC 地址:深入解析与实践指南

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

国产FPGA开发板选择

FPGA开发板是学习和开发FPGA的重要工具&#xff0c;选择合适的开发板对学习效果和开发效率至关重要。随着国产FPGA的发展&#xff0c;淘宝上的许多FPGA开发板店铺也开始进行国产FPGA的设计和销售&#xff0c;本文将对国产FPGA和相关店铺做个简单梳理&#xff0c;帮助有需要使用…...

iOS 获取设备占用内存

获取应用占用内存 获取应用进程占用内存 - (NSUInteger)memoryUsage {task_vm_info_data_t vmInfo;mach_msg_type_number_t count TASK_VM_INFO_COUNT;kern_return_t result task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count);if (result …...

用自己的数据训练yolov11目标检测

文章目录 概要理论知识整体架构流程架构优化多任务支持多参数体量 操作实操环境配置数据准备数据标注数据放置路径 训练预测 概要 官网&#xff1a;https://github.com/ultralytics/ultralytics?tabreadme-ov-file 提示&#xff1a;以 停车场空位检测 公开数据集示例&#x…...

golang如何将结构体和函数进行绑定?

在Go语言中&#xff0c;结构体和函数的绑定通常通过方法&#xff08;method&#xff09;来实现。方法是一种特殊的函数&#xff0c;它与某个类型关联&#xff0c;特别是结构体类型。下面是如何将结构体和函数进行绑定的具体步骤&#xff1a; 定义结构体&#xff1a;首先需要定义…...

【苍穹外卖】学习

软件开发整体介绍 作为一名软件开发工程师,我们需要了解在软件开发过程中的开发流程&#xff0c; 以及软件开发过程中涉及到的岗位角色&#xff0c;角色的分工、职责&#xff0c; 并了解软件开发中涉及到的三种软件环境。那么这一小节&#xff0c;我们将从 软件开发流程、角色…...

架构——LVS负载均衡主要模式及其原理、服务水平、优缺点

LVS&#xff08;Linux Virtual Server&#xff09;是一款高性能的开源负载均衡软件&#xff0c;支持多种负载均衡模式。以下是其主要模式及其原理、服务水平、优缺点&#xff1a; 1. NAT 模式&#xff08;Network Address Translation&#xff09; 原理&#xff1a; 请求流程…...

DFS算法篇:理解递归,熟悉递归,成为递归

1.DFS原理 那么dfs就是大家熟知的一个深度优先搜索&#xff0c;那么听起来很高大尚的一个名字&#xff0c;但是实际上dfs的本质就是一个递归&#xff0c;而且是一个带路径的递归&#xff0c;那么递归大家一定很熟悉了&#xff0c;大学c语言课程里面就介绍过递归&#xff0c;我…...

让编程变成一种享受-明基RD320U显示器

引言 作为一名有着多年JAVA开发经验的从业者&#xff0c;在工作过程中&#xff0c;显示器的重要性不言而喻。它不仅是我们与代码交互的窗口&#xff0c;更是影响工作效率和体验的关键因素。在多年的编程生涯中&#xff0c;我遇到过各种各样的问题。比如&#xff0c;在进行代码…...

C语言简单练习题

文章目录 练习题一、计算n的阶乘bool类型 二、计算1!2!3!...10!三、计算数组arr中的元素个数二分法查找 四、动态打印字符Sleep()ms延时函数system("cls")清屏函数 五、模拟用户登录strcmp()函数 六、猜数字小游戏产生一个随机数randsrandRAND_MAX时间戳time() 示例 …...

基于Python的深度学习音乐推荐系统(有配套论文)

音乐推荐系统 提供实时音乐推荐功能&#xff0c;根据用户行为和偏好动态调整推荐内容 Python、Django、深度学习、卷积神经网络 、算法 数据库&#xff1a;MySQL 系统包含角色&#xff1a;管理员、用户 管理员功能&#xff1a;用户管理、系统设置、音乐管理、音乐推荐管理、系…...

Java:单例模式(Singleton Pattern)及实现方式

一、单例模式的概念 单例模式是一种创建型设计模式&#xff0c;确保一个类只有一个实例&#xff0c;并提供一个全局访问点来访问该实例&#xff0c;是 Java 中最简单的设计模式之一。该模式常用于需要全局唯一实例的场景&#xff0c;例如日志记录器、配置管理、线程池、数据库…...

解锁养生秘籍,拥抱健康生活

在这个快节奏的时代&#xff0c;人们行色匆匆&#xff0c;常常在忙碌中忽略了健康。其实&#xff0c;养生并非遥不可及&#xff0c;它就藏在生活的细微之处&#xff0c;等待我们去发现和实践。 规律作息是健康的基础。日出而作&#xff0c;日落而息&#xff0c;顺应自然规律&am…...

数据结构之堆(Heap)

数据结构之堆&#xff08;Heap&#xff09; 数据结构之堆&#xff08;Heap&#xff09;一、堆的核心概念1. 定义与性质2. 存储方式 二、核心操作与算法1. 操作复杂度概览2. 关键操作详解(1) 向上调整&#xff08;Sift Up&#xff09;(2) 向下调整&#xff08;Sift Down&#xf…...

人工智能 - 机器学习、深度学习、强化学习是人工智能领域的理论基础和方法论

机器学习、深度学习、强化学习是人工智能领域的三大核心方向,各自具有独特的理论基础和方法论。以下是它们的核心理论知识总结: 一、机器学习(Machine Learning, ML) 1. 基础概念 目标:通过数据驱动的方式,让机器从经验中学习规律,完成预测、分类或决策任务。 核心范式…...

github上文件过大无法推送问题

GitHub 对文件大小有限制&#xff0c;超过 100 MB 的文件无法直接推送到仓库中。 解决思路&#xff1a; 使用 Git Large File Storage (Git LFS) 来管理大文件不上传对应的大文件 使用Git LFS&#xff1a; 1. 安装 Git LFS 首先&#xff0c;你需要安装 Git LFS。可以按照以…...

Elasticsearch:将 Ollama 与推理 API 结合使用

作者&#xff1a;来自 Elastic Jeffrey Rengifo Ollama API 与 OpenAI API 兼容&#xff0c;因此将 Ollama 与 Elasticsearch 集成非常容易。 在本文中&#xff0c;我们将学习如何使用 Ollama 将本地模型连接到 Elasticsearch 推理模型&#xff0c;然后使用 Playground 向文档提…...

【Linux】详谈 进程控制

目录 一、进程是什么 二、task_struct 三、查看进程 四、创建进程 4.1 fork函数的认识 4.2 2. fork函数的返回值 五、进程终止 5.1. 进程退出的场景 5.2. 进程常见的退出方法 5.2.1 从main返回 5.2.1.1 错误码 5.2.2 exit函数 5.2.3 _exit函数 5.2.4 缓冲区问题补…...

构建高效智能对话前端:基于Ant Design X 的deepseek对话应用

文章目录 实现的效果前言Ant Design X添加欢迎组件创建对话气泡存储对话历史渲染对话气泡 输入组件WebSocket 连接总结 实现的效果 待机页面&#xff1a; 等待页面&#xff1a; 完成页面&#xff1a; 前言 随着人工智能技术的飞速发展&#xff0c;大模型对话系统已成为…...

WordPress“更新失败,响应不是有效的JSON响应”问题的修复

在使用WordPress搭建网站时&#xff0c;许多人在编辑或更新文章时&#xff0c;可能会遇到一个提示框&#xff0c;显示“更新失败&#xff0c;响应不是有效的JSON响应”。这个提示信息对于不了解技术细节的用户来说&#xff0c;太难懂。其实&#xff0c;这个问题并不复杂&#x…...

华为交换机trunk简介配置

目录 一、Trunk 口简介二、Trunk 口配置案例及命令&#xff08;一&#xff09;组网需求&#xff08;二&#xff09;配置步骤&#xff08;三&#xff09;验证配置 三、注意事项 一、Trunk 口简介 Trunk 口是交换机中一种重要的端口类型&#xff0c;主要用于连接交换机与交换机、…...

DeepSeek从入门到精通(清华大学)

​ DeepSeek是一款融合自然语言处理与深度学习技术的全能型AI助手&#xff0c;具备知识问答、数据分析、编程辅助、创意生成等多项核心能力。作为多模态智能系统&#xff0c;它不仅支持文本交互&#xff0c;还可处理文件、图像、代码等多种格式输入&#xff0c;其知识库更新至2…...

【SpringBoot3】面向切面 AspectJ AOP 使用详解

文章目录 一、AspectJ介绍二、简单使用步骤 1、引入依赖2、定义一个Aspect3、开启AOP支持 三、AOP 核心概念四、切点&#xff08;Pointcut&#xff09; 1. execution2. within3. this & target4. args & args5. within & target & annotation 五、通知&#xf…...

容器运行常见数据库

一.涉及镜像压缩包 均为amd架构版本&#xff1a;mysql:5.7.42、postgres:13.16、dm8:20250206_rev257733_x86_rh6_64、oceanbase-ce:v4.0、opengauss:5.0.2 通过网盘分享的文件&#xff1a;db.tgz 链接: https://pan.baidu.com/s/1EBbFPZj1FxCA4_GxjVunWg?pwd563s 提取码: 5…...

OpenGL ES学习大纲

如果您想从头学习 OpenGL ES,以下是一个详细的学习大纲,涵盖了从基础到高级的知识点,循序渐进地帮助您掌握 OpenGL ES 的核心概念、API 使用、渲染管线、着色器编程、性能优化等内容。 1. 学习前的准备 1.1 基础知识 在学习 OpenGL ES 之前,您需要掌握以下基础知识: 数学…...