星球的眼睛 发表于 2024-6-15 00:57:34

Java 并发编程中的 synchronized 关键字及其当代优化技能

简介

在早期版本的 Java 中,synchronized 关键字被以为是重量级锁,效率较低,这与其底层实现机制密切相关。要深入理解这个问题,需要联合计算机体系的相关原理,包括操作体系、线程管理、以及用户态和内核态的转换。
1. 监视器锁(Monitor Lock)与操作体系的 Mutex Lock

synchronized 关键字的实现依赖于监视器锁(Monitor Lock)。在 Java 中,每个对象都有一个隐含的监视器锁。当一个线程进入一个同步块或同步方法时,它必须首先得到该对象的监视器锁。
监视器锁的底层实现通常依赖于操作体系提供的互斥锁(Mutex Lock)。互斥锁是一种用于在多线程环境中实现互斥访问共享资源的机制,它确保在任意时刻只有一个线程可以访问特定的资源。
互斥锁的基本原理是通过操作体系内核来控制对共享资源的访问,防止多个线程同时访问共享资源造成的数据不一致问题。在实现互斥锁时,操作体系通常使用硬件提供的原子操作来保证锁的正确性。比方,x86架构提供了 LOCK 指令,可以确保对内存的原子操作,从而实现互斥锁。
1.1 监视器锁的工作机制

监视器锁的工作机制可以分为以下几个步调:

[*]获取锁:当一个线程试图进入一个同步块时,它必须首先获取对象的监视器锁。假如锁已经被其他线程持有,该线程将被阻塞,直到锁被开释。
[*]实行同步代码:一旦线程得到了锁,它就可以实行同步块中的代码。在此期间,其他试图进入该同步块的线程将被阻塞。
[*]开释锁:当线程退出同步块时,它将开释监视器锁,以便其他线程可以获取该锁并进入同步块。
1.2 互斥锁的实现

操作体系提供的互斥锁通常依赖于底层硬件的支持。以下是互斥锁的一种简单实现方式:
typedef struct {
    int locked;
} mutex_t;

void lock(mutex_t *mutex) {
    while (__sync_lock_test_and_set(&(mutex->locked), 1)) {
      // 自旋等待
    }
}

void unlock(mutex_t *mutex) {
    __sync_lock_release(&(mutex->locked));
}
在这个实现中,__sync_lock_test_and_set 是一个原子操作,它将 mutex->locked 设置为 1,并返回之前的值。假如之前的值是 0,则体现锁未被持有,线程可以乐成获取锁;假如之前的值是 1,则体现锁已经被其他线程持有,当前线程需要自旋等待。
2. Java 线程与操作体系原生线程

Java 线程是通过 JVM 实现的,而 JVM 中的线程直接映射到操作体系的原生线程(如在 Windows 上的 Thread,在 Unix 体系上的 pthread)。这意味着 Java 线程的创建、调度和管理完全依赖于底层操作体系的线程管理机制。
2.1 Java 线程的实现

Java 线程的实现依赖于 JVM 的线程调度器。JVM 的线程调度器将 Java 线程映射到操作体系的原生线程上,并负责线程的创建、切换和销毁。在 JVM 中,线程的状态可以分为以下几种:
新建(New):线程对象被创建,但尚未启动。
就绪(Runnable):线程已经启动,可以运行,但实际运行时间由线程调度器决定。
运行(Running):线程正在 CPU 上运行。
阻塞(Blocked):线程因等待某个资源而被阻塞,无法运行。
等待(Waiting):线程进入等待状态,等待其他线程显式叫醒。
定时等待(Timed Waiting):线程进入定时等待状态,在指定时间后自动叫醒。
停止(Terminated):线程已经结束实行,无法再次运行。
2.2 操作体系的线程管理

操作体系的线程管理包括线程的创建、调度和销毁。线程的调度通常依赖于调度算法,如时间片轮转(Round Robin)、优先级调度(Priority Scheduling)等。在多核处置惩罚器上,操作体系还需要负责线程的负载平衡,以确保每个 CPU 核心的工作负载均匀。
2.3 Java 线程与操作体系线程的关系

由于 Java 线程直接映射到操作体系的原生线程,Java 线程的调度和管理完全依赖于操作体系的线程管理机制。这种映射关系使得 Java 线程可以充分使用操作体系提供的多线程本领,但也带来了性能开销。
3. 线程的挂起和叫醒

当一个线程实验获取一个已经被其他线程持有的锁时,它会被阻塞并进入等待状态,直到该锁被开释。这个过程需要操作体系的参与:
挂起线程:操作体系将当前运行的线程从 CPU 上移除,并将其状态保存到线程控制块(Thread Control Block, TCB)中。这个过程涉及到上下文切换,即保存当前线程的上下文(如寄存器、步伐计数器等)并加载下一个要运行线程的上下文。
叫醒线程:当锁被开释时,操作体系将叫醒等待获取该锁的线程,将其状态从等待队列中移除,并重新调度该线程运行。
3.1 上下文切换

上下文切换是指操作体系将一个正在运行的线程换出 CPU,并将另一个线程调入 CPU 运行的过程。上下文切换包括保存当前线程的状态(如寄存器值、步伐计数器等),并加载下一个线程的状态。上下文切换的重要步调如下:
保存当前线程的上下文:操作体系保存当前线程的寄存器值、步伐计数器等状态到其线程控制块(TCB)中。
选择下一个线程:操作体系根据调度算法选择下一个要运行的线程。
加载新线程的上下文:操作体系从选定线程的 TCB 中恢复其寄存器值、步伐计数器等状态。
切换到新线程:操作体系将 CPU 切换到新线程,开始实行。
3.2 上下文切换的开销

上下文切换是一个开销较大的操作,重要包括以下几个方面:
CPU 时间:保存和恢复线程的上下文需要耗费 CPU 时间。
缓存失效:上下文切换可能导致 CPU 缓存失效,需要重新加载缓存数据。
TLB 刷新:上下文切换可能导致 TLB(Translation Lookaside Buffer)刷新,从而增加内存访问延迟。
在高并发环境下,频仍的上下文切换会明显低落体系性能。因此,淘汰上下文切换是提高多线程应用性能的重要本领之一。
4. 用户态与内核态的转换

线程挂起和叫醒的操作需要从用户态(user mode)转换到内核态(kernel mode),这是因为这些操作需要操作体系内核的支持。
用户态:应用步伐运行的模式,具有受限的访问权限,不能直接访问硬件或内核数据结构。
内核态:操作体系内核运行的模式,具有完全的访问权限,可以实行任何 CPU 指令并访问任何内存地址。
用户态到内核态的转换涉及以下步调:
陷入(Trap)指令:当线程需要举行体系调用(如线程挂起或叫醒)时,会触发一个陷入指令,使 CPU 从用户态切换到内核态。
保存上下文:操作体系内核保存当前线程的上下文,包括寄存器、步伐计数器等。
实行内核代码:操作体系内核实行挂起或叫醒线程的代码。
恢复上下文:操作体系内核恢复目的线程的上下文。
返回用户态:实行返回指令,将 CPU 从内核态切换回用户态。
4.1 体系调用

体系调用是用户态步伐请求操作体系内核提供服务的接口。常见的体系调用包括文件操作、历程管理、内存管理和网络通讯等。体系调用的重要步调如下:
用户步伐发起体系调用:用户步伐通过库函数或直接使用体系调用接口发起请求。这通常涉及将体系调用号和参数传递给操作体系内核。
陷入内核态:通过触发陷入(Trap)指令,CPU 从用户态切换到内核态。陷入指令使恰当前运行的步伐暂停,并将控制权转交给操作体系内核。
内核态处置惩罚:操作体系内核根据体系调用号确定要实行的服务例程,并实行相应的内核代码。这可能涉及文件读写、内存分配、历程调度等操作。
返回用户态:内核代码实行完成后,操作体系内核将结果返回给用户步伐,并通过返回指令将 CPU 从内核态切换回用户态。
体系调用的过程中,用户态和内核态的频仍切换会带来明显的性能开销。这是因为每次切换都需要保存和恢复上下文,同时涉及到缓存失效和 TLB 刷新等额外开销。
4.2 用户态与内核态切换的优化

为了淘汰用户态与内核态之间的频仍切换带来的性能开销,当代操作体系和 JVM 引入了多种优化技能。比方,使用更高效的锁机制(如自旋锁)在用户态处置惩罚线程同步,以淘汰进入内核态的须要性。别的,JVM 的各种优化技能也在很大程度上淘汰了锁操作导致的用户态和内核态的切换。
5. 时间本钱

用户态到内核态的转换(及其逆过程)黑白常昂贵的操作,因为它涉及到多次上下文切换,这些切换会带来额外的开销,如缓存失效、TLB(Translation Lookaside Buffer)刷新等。上下文切换频仍发生会导致体系性能下降,尤其在高并发场景下,这种开销会更加明显。
5.1 上下文切换的开销

上下文切换的重要开销来源包括:
CPU 时间:保存和恢复线程的上下文需要耗费 CPU 时间。
缓存失效:上下文切换可能导致 CPU 缓存失效,需要重新加载缓存数据。
TLB 刷新:上下文切换可能导致 TLB(Translation Lookaside Buffer)刷新,从而增加内存访问延迟。
5.2 高并发场景下的影响

在高并发场景下,频仍的上下文切换会明显低落体系性能。多个线程同时竞争 CPU 资源,导致频仍的线程切换,增加了 CPU 的开销。为了淘汰上下文切换带来的影响,可以通过优化锁机制和淘汰锁竞争来提高并发性能。
6. 早期版本的 synchronized 效率低的原因

归纳起来,早期版本的 synchronized 被以为是效率低下的重量级锁,重要原因包括:
依赖操作体系的互斥锁:需要操作体系参与,增加了开销。
Java 线程映射到操作体系原生线程:线程管理和调度完全依赖操作体系。
线程挂起和叫醒:这些操作需要操作体系的参与,并涉及昂贵的用户态和内核态转换。
高昂的时间本钱:用户态到内核态的转换和频仍的上下文切换带来明显的性能开销。
6.1 依赖操作体系的互斥锁

早期版本的 synchronized 依赖于操作体系提供的互斥锁,这意味着每次线程获取和开释锁时,都需要通过体系调用进入内核态。这种设计增加了体系开销,并且在高并发环境下,频仍的体系调用会导致性能瓶颈。
6.2 Java 线程与操作体系原生线程的映射

Java 线程直接映射到操作体系的原生线程,虽然这种设计使得 Java 线程可以使用操作体系的线程管理机制,但也带来了额外的开销。操作体系的线程调度和管理通常涉及复杂的算法和数据结构,这些操作会增加线程的创建、销毁和调度的本钱。
6.3 线程挂起和叫醒的开销

当一个线程实验获取已经被其他线程持有的锁时,它会被阻塞并进入等待状态,直到该锁被开释。这个过程需要操作体系的参与,导致线程被挂起和叫醒,而线程的挂起和叫醒涉及上下文切换和用户态与内核态的转换,增加了体系开销。
6.4 高昂的时间本钱

用户态到内核态的转换是一个高昂的操作,特别是在高并发场景下,频仍的上下文切换和体系调用会明显低落体系性能。因此,淘汰用户态与内核态的转换次数是提高并发性能的关键。
当代 JVM 的优化

为了改善 synchronized 的性能,当代 JVM(从 JDK 1.6 开始)引入了多种优化技能,如自旋锁、顺应性自旋锁、锁消除、锁粗化、方向锁和轻量级锁。这些技能大大淘汰了锁操作的开销,提高了 synchronized 的效率。
1 自旋锁(Spin Lock)

自旋锁通过忙等待(自旋)来避免线程挂起和叫醒的开销。当一个线程实验获取已经被其他线程持有的锁时,它不会立刻挂起,而是循环检查锁的状态,直到获取锁或到达自旋次数限制。
2 顺应性自旋锁(Adaptive Spin Lock)

顺应性自旋锁根据前一次自旋的效果动态调整自旋时间。假如前一次自旋等待乐成,则延长自旋时间;假如前一次自旋等待失败,则缩短自旋时间或直接挂起线程。这种自顺应机制在用户态举行更多判断和操作,淘汰了不须要的线程挂起。
3 锁消除(Lock Elimination)

通过逃逸分析,JVM 可以确定某些锁在单线程环境中是多余的,并在编译时消除这些锁。锁消除技能基于逃逸分析,逃逸分析是一种优化技能,用于确定对象的生命周期和作用范围。假如 JVM 确定某个锁对象不会被其他线程访问,则可以安全地消除该锁。
4 锁粗化(Lock Coarsening)

锁粗化技能通过将多次小范围的锁合并为一次大的锁操作,淘汰了锁的频仍获取和开释,从而淘汰了线程切换和进入内核态的机会。比方,在一个循环中反复举行加锁息争锁操作,锁粗化技能会将整个循环的操作合并为一次加锁息争锁。
5 方向锁(Biased Locking)

方向锁假设大多数环境下锁由同一个线程持有,因此初次加锁后,该锁会方向于该线程,再次进入同步块时无需真正的锁竞争和切换,避免了进入内核态。只有在其他线程实验竞争锁时,才会取消方向锁并举行锁竞争。
6 轻量级锁(Lightweight Locking)

轻量级锁通过使用 CAS(Compare-And-Swap)操作在用户态举行锁竞争。这种操作避免了传统重量级锁需要的内核态的线程挂起和叫醒。轻量级锁适用于短时间持有锁的场景,通过在用户态举行锁竞争,淘汰了体系调用的开销。
7 自顺应自旋锁的实现

自顺应自旋锁在当代 JVM 中得到了广泛应用。以下是自顺应自旋锁的一种实现方式:
复制代码
class AdaptiveSpinLock {
    private AtomicBoolean lock = new AtomicBoolean(false);

    public void lock() {
      int spinCount = 0;
      while (!lock.compareAndSet(false, true)) {
            if (++spinCount > MAX_SPIN) {
                // 如果自旋次数超过阈值,则挂起线程
                Thread.yield();
                spinCount = 0;
            }
      }
    }
   
    public void unlock() {
      lock.set(false);
    }
}
在这个实现中,lock() 方法使用 AtomicBoolean 举行原子操作,假如锁未被持有,则乐成获取锁;假如锁已被持有,则自旋等待。自旋次数超过阈值后,线程会自动让出 CPU(Thread.yield()),避免长时间的忙等待。
8 方向锁的实现

方向锁在无竞争环境下性能极高,因为它几乎不需要举行任何同步操作。方向锁的基本头脑是将锁的所有权方向于第一次获取锁的线程。只要没有其他线程竞争该锁,持有方向锁的线程在进入和退出同步块时几乎不需要任何操作。
8.1 方向锁的工作机制

方向锁的实现依赖于对象头的锁标记位和线程 ID。当一个线程第一次获取方向锁时,JVM 将该线程的 ID 记载在对象头中,并将锁标记位设置为方向状态。之后,当该线程再次进入同步块时,不需要举行锁竞争,直接进入临界区。
8.2 方向锁的取消

当有其他线程实验获取方向锁时,JVM 会取消方向锁,并将其升级为轻量级锁或重量级锁。方向锁的取消过程如下:
暂停方向锁持有线程:JVM 暂停持有方向锁的线程,确保不会在取消过程中修改对象头。
检查方向锁状态:JVM 检查对象头的锁标记位和线程 ID,确定是否需要取消方向锁。
升级锁:假如需要取消方向锁,JVM 将其升级为轻量级锁或重量级锁,并恢复线程的实行。
###.8.3 方向锁的性能优势
方向锁适用于大多数锁在无竞争环境下使用的场景。由于无竞争时不需要举行同步操作,方向锁的性能极高,适用于高频次、低竞争的锁使用场景。
9. 轻量级锁(Lightweight Lock)

轻量级锁通过 CAS(Compare-And-Swap)操作在用户态举行锁竞争,避免了重量级锁的高昂开销。轻量级锁适用于短时间持有锁的场景,通过在用户态举行锁竞争,淘汰了体系调用的开销。
9.1 轻量级锁的工作机制

轻量级锁的实现依赖于 CAS 操作。CAS 是一种原子操作,用于比较并交换变量的值,确保多个线程可以安全地更新共享变量而不引入竞争条件。轻量级锁的基本工作流程如下:
实验获取锁:线程通过 CAS 操作实验获取锁。假如乐成,则进入临界区;假如失败,则进入自旋等待。
自旋等待:在短时间内,自旋等待实验再次获取锁。假如锁在短时间内被开释,线程可以避免被挂起。
获取锁失败:假如自旋等待失败,线程将被挂起,等待锁被开释。
9.2 轻量级锁的优势

轻量级锁适用于竞争不剧烈、持有时间短的锁操作场景。通过在用户态举行锁竞争,轻量级锁避免了进入内核态的高昂开销,提高了锁的性能。在高并发场景下,轻量级锁可以明显淘汰上下文切换和体系调用的次数。
10. 自顺应自旋锁(Adaptive Spin Lock)

自顺应自旋锁是一种根据锁的竞争环境动态调整自旋时间的锁机制。相比固定自旋次数的自旋锁,自顺应自旋锁更加智能,可以根据前一次自旋的结果动态调整自旋时间,从而提高锁的性能。
10.1 自顺应自旋锁的工作机制

自顺应自旋锁的工作机制如下:
实验获取锁:线程通过 CAS 操作实验获取锁。假如乐成,则进入临界区;假如失败,则进入自旋等待。
自旋等待:根据前一次自旋的结果,动态调整自旋时间。假如前一次自旋乐成,则延长自旋时间;假如前一次自旋失败,则缩短自旋时间或直接挂起线程。
自顺应调整:自顺应自旋锁会根据锁的竞争环境动态调整自旋计谋,提高锁的获取效率。
10.2 自顺应自旋锁的实现

以下是自顺应自旋锁的一种实现方式:
复制代码
class AdaptiveSpinLock {
    private AtomicBoolean lock = new AtomicBoolean(false);

    public void lock() {
      int spinCount = 0;
      while (!lock.compareAndSet(false, true)) {
            if (++spinCount > MAX_SPIN) {
                // 如果自旋次数超过阈值,则挂起线程
                Thread.yield();
                spinCount = 0;
            }
      }
    }
   
    public void unlock() {
      lock.set(false);
    }
}
在这个实现中,lock() 方法使用 AtomicBoolean 举行原子操作,假如锁未被持有,则乐成获取锁;假如锁已被持有,则自旋等待。自旋次数超过阈值后,线程会自动让出 CPU(Thread.yield()),避免长时间的忙等待。
10.3 自顺应自旋锁的优势

自顺应自旋锁通过动态调整自旋时间,可以在锁竞争不剧烈的环境下避免线程挂起,从而提高锁的性能。在高并发场景下,自顺应自旋锁可以明显淘汰上下文切换和体系调用的次数,提高体系的整体性能。
11. 锁消除(Lock Elimination)

锁消除是一种通过编译器优化来消除不须要锁的技能。通过逃逸分析,JVM 可以确定某些锁在单线程环境中是多余的,并在编译时消除这些锁,从而淘汰锁操作的开销。
11.1 逃逸分析

逃逸分析是一种编译器优化技能,用于确定对象的生命周期和作用范围。假如 JVM 确定某个锁对象不会被其他线程访问,则可以安全地消除该锁。比方,局部变量的对象通常不会逃逸出方法的作用范围,因此可以消除对这些对象的锁操作。
11.2 锁消除的实现

以下是锁消除的一种示例:
复制代码
public void example() {
    Object lock = new Object();
    synchronized (lock) {
      // 代码块
    }
}
在这个示例中,lock 对象是一个局部变量,不会逃逸出 example 方法的作用范围。因此,JVM 可以在编译时消除对 lock 对象的锁操作,从而淘汰锁的开销。
11.3 锁消除的优势

锁消除通过淘汰不须要的锁操作,可以明显提高步伐的性能。特别是在高并发场景下,锁消除可以淘汰锁竞争和上下文切换,从而提高体系的整体性能。
12. 锁粗化(Lock Coarsening)

锁粗化是一种通过合并多个小范围的锁操作来淘汰锁竞争和上下文切换的技能。锁粗化可以淘汰锁的频仍获取和开释,从而提高锁的性能。
12.1 锁粗化的工作机制

锁粗化通过将多个小范围的锁操作合并为一次大的锁操作,从而淘汰锁的频仍获取和开释。比方,在一个循环中反复举行加锁息争锁操作,锁粗化技能会将整个循环的操作合并为一次加锁息争锁。
12.2 锁粗化的示例

以下是锁粗化的一种示例:
复制代码
public void example() {
    for (int i = 0; i < 100; i++) {
      synchronized (this) {
            // 代码块
      }
    }
}
在这个示例中,循环中的每次迭代都需要举行加锁息争锁操作。通过锁粗化,JVM 可以将整个循环的操作合并为一次加锁息争锁,从而淘汰锁的频仍获取和开释。
12.3 锁粗化的优势

锁粗化通过淘汰锁的频仍获取和开释,可以明显提高步伐的性能。特别是在高并发场景下,锁粗化可以淘汰锁竞争和上下文切换,从而提高体系的整体性能。
当代 JVM 的其他优化技能

除了上述的几种优化技能,当代 JVM 还引入了其他一些优化技能来提高锁的性能和并发处置惩罚本领。这些技能包括锁膨胀、无锁编程和分段锁等。
1 锁膨胀(Lock Inflation)

锁膨胀是指当一个轻量级锁竞争剧烈时,JVM 会将其升级为重量级锁。锁膨胀的目的是淘汰轻量级锁在高竞争环境下的自旋等待时间,从而提高体系的整体性能。
2 无锁编程(Lock-free Programming)

无锁编程是一种通过使用原子操作(如 CAS)来实现并发控制的技能。无锁编程可以避免传统锁机制带来的锁竞争和上下文切换,从而提高体系的并发性能。无锁编程的核心头脑是使用硬件提供的原子操作来确保多个线程可以安全地举行并发操作,而不需要使用传统的锁机制。
2.1 CAS 操作

CAS(Compare-And-Swap)是一种原子操作,用于比较和交换变量的值。CAS 操作的基本流程如下:
比较:比较变量的当前值是否等于预期值。
交换:假如变量的当前值等于预期值,则将变量的值更新为新值;否则,不做任何操作。
CAS 操作的原子性由硬件提供的指令支持,比方 x86 架构中的 LOCK CMPXCHG 指令。
2.2 无锁数据结构

无锁编程常用于实现高效的并发数据结构,如无锁队列、无锁堆栈和无锁链表等。以下是无锁队列的一种实现:
class LockFreeQueue<T> {
    private static class Node<T> {
      final T item;
      volatile Node<T> next;

      Node(T item, Node<T> next) {
            this.item = item;
            this.next = next;
      }
    }
   
    private final Node<T> dummy = new Node<>(null, null);
    private final AtomicReference<Node<T>> head = new AtomicReference<>(dummy);
    private final AtomicReference<Node<T>> tail = new AtomicReference<>(dummy);
   
    public void enqueue(T item) {
      Node<T> newNode = new Node<>(item, null);
      while (true) {
            Node<T> currentTail = tail.get();
            Node<T> tailNext = currentTail.next;
            if (currentTail == tail.get()) {
                if (tailNext != null) {
                  tail.compareAndSet(currentTail, tailNext);
                } else {
                  if (currentTail.next.compareAndSet(null, newNode)) {
                        tail.compareAndSet(currentTail, newNode);
                        return;
                  }
                }
            }
      }
    }
   
    public T dequeue() {
      while (true) {
            Node<T> currentHead = head.get();
            Node<T> currentTail = tail.get();
            Node<T> headNext = currentHead.next;
            if (currentHead == head.get()) {
                if (currentHead == currentTail) {
                  if (headNext == null) {
                        return null;
                  }
                  tail.compareAndSet(currentTail, headNext);
                } else {
                  T item = headNext.item;
                  if (head.compareAndSet(currentHead, headNext)) {
                        return item;
                  }
                }
            }
      }
    }
}
在这个实现中,enqueue 和 dequeue 方法使用 CAS 操作来确保多个线程可以安全地并发访问队列,而不需要使用传统的锁机制。
2.3 无锁编程的优势

无锁编程通过避免传统锁机制的锁竞争和上下文切换,可以明显提高体系的并发性能。无锁编程适用于高并发场景,特别是在需要频仍访问共享数据结构的环境下。
2.4 无锁编程的挑战

尽管无锁编程在性能上具有明显优势,但实在现相对复杂,轻易引入难以调试的并发错误。别的,无锁编程通常依赖于硬件提供的原子操作,因此在差别硬件平台上的可移植性可能受限。
3. 分段锁(Segmented Locking)

分段锁是一种将锁分解为多个独立部分的技能,以淘汰锁竞争和提高并发性能。分段锁常用于并发哈希表和其他需要高效并发访问的数据结构。
3.1 分段锁的工作机制

分段锁通过将锁分解为多个独立部分,每个部分掩护数据结构的一个子集。线程在访问数据结构时,只需要获取与该子集对应的锁,从而淘汰了锁的竞争范围。比方,在并发哈希表中,分段锁可以将哈希表分为多个段,每个段由一个独立的锁掩护。
3.2 分段锁的示例

以下是分段锁哈希表的一种实现:
class SegmentLockHashTable<K, V> {
    private static final int SEGMENT_COUNT = 16;
    private final Segment<K, V>[] segments;

    private static class Segment<K, V> {
      private final Map<K, V> map = new HashMap<>();
      private final ReentrantLock lock = new ReentrantLock();
   
      void put(K key, V value) {
            lock.lock();
            try {
                map.put(key, value);
            } finally {
                lock.unlock();
            }
      }
   
      V get(K key) {
            lock.lock();
            try {
                return map.get(key);
            } finally {
                lock.unlock();
            }
      }
    }
   
    public SegmentLockHashTable() {
      segments = new Segment;
      for (int i = 0; i < SEGMENT_COUNT; i++) {
            segments = new Segment<>();
      }
    }
   
    private Segment<K, V> getSegment(Object key) {
      int hash = key.hashCode();
      int segmentIndex = (hash >>> 16) ^ hash & (SEGMENT_COUNT - 1);
      return segments;
    }
   
    public void put(K key, V value) {
      Segment<K, V> segment = getSegment(key);
      segment.put(key, value);
    }
   
    public V get(K key) {
      Segment<K, V> segment = getSegment(key);
      return segment.get(key);
    }
}
在这个实现中,哈希表被分为多个段,每个段由一个独立的 ReentrantLock 掩护。通过这种方式,线程在访问哈希表时只需获取与段对应的锁,从而淘汰了锁的竞争范围。
3.3 分段锁的优势

分段锁通过将锁分解为多个独立部分,淘汰了锁竞争和上下文切换,提高了数据结构的并发性能。分段锁特别适用于需要高效并发访问的大型数据结构,如并发哈希表。
3.4 分段锁的局限性

尽管分段锁可以明显提高并发性能,但它也有一些局限性。比方,分段锁的实现相对复杂,需要仔细设计和调试。别的,分段锁在某些环境下可能无法完全避免锁竞争,特别是当大量线程同时访问同一个段时。
4. 总结

Java 的 synchronized 关键字在早期版本中被以为是重量级锁,效率较低。这重要是因为 synchronized 依赖于操作体系提供的互斥锁,并且涉及频仍的用户态和内核态转换,导致高昂的性能开销。然而,当代 JVM 通过引入多种优化技能,如自旋锁、顺应性自旋锁、锁消除、锁粗化、方向锁和轻量级锁,大大提高了 synchronized 的性能。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Java 并发编程中的 synchronized 关键字及其当代优化技能