Java的ConcurrentHashMap是使用的分段锁?

2024年 1月 30日 96.1k 0

了不起在前两天的时候给大家讲述了关于这个 Java 的公平锁,非公平锁,共享锁,独占锁,乐观锁,悲观锁,递归锁,读写锁,今天我们就再来了解一下其他的锁,比如,轻量级锁,重量级锁,偏向锁,以及分段锁。

轻量级锁

Java的轻量级锁(Lightweight Locking)是Java虚拟机(JVM)中的一种优化机制,用于减少多线程竞争时的性能开销。在多线程环境中,当多个线程尝试同时访问共享资源时,通常需要某种形式的同步以防止数据不一致。Java提供了多种同步机制,如synchronized关键字和ReentrantLock,但在高并发场景下,这些机制可能导致性能瓶颈。

轻量级锁是JVM中的一种锁策略,它在没有多线程竞争的情况下提供了较低的开销,同时在竞争变得激烈时能够自动升级到更重量级的锁。这种策略的目标是在不需要时避免昂贵的线程阻塞操作。

不过这种锁并不是通过Java语言直接暴露给开发者的API,而是JVM在运行时根据需要自动应用的。因此,我们不能直接通过Java代码来实现一个轻量级锁。

但是我们可以使用Java提供的synchronized关键字或java.util.concurrent.locks.Lock接口(及其实现类,如ReentrantLock)来创建同步代码块或方法,这些同步机制在底层可能会被JVM优化为使用轻量级锁。

示例代码:

public class LightweightLockExample {  
  
    private Object lock = new Object();  
    private int sharedData;  
  
    public void incrementSharedData() {  
        synchronized (lock) {  
            sharedData++;  
        }  
    }  
  
    public int getSharedData() {  
        synchronized (lock) {  
            return sharedData;  
        }  
    }  
  
    public static void main(String[] args) {  
        LightweightLockExample example = new LightweightLockExample();  
  
        // 使用多个线程来访问共享数据  
        for (int i = 0; i  {  
                for (int j = 0; j < 1000; j++) {  
                    example.incrementSharedData();  
                }  
            }).start();  
        }  
  
        // 等待所有线程执行完毕  
        try {  
            Thread.sleep(2000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
  
        // 输出共享数据的最终值  
        System.out.println("Final shared data value: " + example.getSharedData());  
    }  
}

这个示例中的同步块在JVM内部可能会使用轻量级锁(具体是否使用取决于JVM的实现和运行时环境)

在这个例子中,我们有一个sharedData变量,多个线程可能会同时访问它。我们使用synchronized块来确保每次只有一个线程能够修改sharedData。在JVM内部,这些synchronized块可能会使用轻量级锁来优化同步性能。

请注意,这个例子只是为了演示如何使用synchronized关键字,并不能保证JVM一定会使用轻量级锁。实际上,JVM可能会根据运行时的情况选择使用偏向锁、轻量级锁或重量级锁。

重量级锁

在Java中,重量级锁(Heavyweight Locking)是相对于轻量级锁而言的,它涉及到线程阻塞和操作系统级别的线程调度。当轻量级锁或偏向锁不足以解决线程间的竞争时,JVM会升级锁为重量级锁。

重量级锁通常是通过操作系统提供的互斥原语(如互斥量、信号量等)来实现的。当一个线程尝试获取已经被其他线程持有的重量级锁时,它会被阻塞(即挂起),直到持有锁的线程释放该锁。在阻塞期间,线程不会消耗CPU资源,但会导致上下文切换的开销,因为操作系统需要保存和恢复线程的上下文信息。

在Java中,synchronized关键字和java.util.concurrent.locks.ReentrantLock都可以导致重量级锁的使用,尤其是在高并发和激烈竞争的场景下。

我们来看看使用synchronized可能会涉及到重量级锁的代码:

public class HeavyweightLockExample {  
  
    private final Object lock = new Object();  
    private int counter;  
  
    public void increment() {  
        synchronized (lock) {  
            counter++;  
        }  
    }  
  
    public int getCounter() {  
        synchronized (lock) {  
            return counter;  
        }  
    }  
  
    public static void main(String[] args) {  
        HeavyweightLockExample example = new HeavyweightLockExample();  
  
        // 创建多个线程同时访问共享资源  
        for (int i = 0; i  {  
                for (int j = 0; j < 10000; j++) {  
                    example.increment();  
                }  
            }).start();  
        }  
  
        // 等待所有线程完成  
        try {  
            Thread.sleep(2000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
  
        // 输出计数器的值  
        System.out.println("Final counter value: " + example.getCounter());  
    }  
}

在这个示例中,多个线程同时访问counter变量,并使用synchronized块来确保每次只有一个线程能够修改它。如果线程间的竞争非常激烈,JVM可能会将synchronized块内部的锁升级为重量级锁。

我们说的是可能哈,毕竟内部操作还是由 JVM 具体来操控的。

我们再来看看这个ReentrantLock来实现:

import java.util.concurrent.locks.ReentrantLock;  
  
public class ReentrantLockExample {  
  
    private final ReentrantLock lock = new ReentrantLock();  
    private int counter;  
  
    public void increment() {  
        lock.lock(); // 获取锁  
        try {  
            counter++;  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
  
    public int getCounter() {  
        return counter;  
    }  
  
    public static void main(String[] args) {  
        // 类似上面的示例,创建线程并访问共享资源  
    }  
}

在这个示例中,ReentrantLock被用来同步对counter变量的访问。如果锁竞争激烈,ReentrantLock内部可能会使用重量级锁。

需要注意的是,重量级锁的使用会带来较大的性能开销,因此在设计并发系统时应尽量通过减少锁竞争、使用更细粒度的锁、使用无锁数据结构等方式来避免重量级锁的使用。

偏向锁

在Java中,偏向锁(Biased Locking)是Java虚拟机(JVM)为了提高无竞争情况下的性能而引入的一种锁优化机制。它的基本思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的Thread ID即可,这样就省去了大量有关锁申请的操作。

他和轻量级锁和重量级锁一样,并不是直接通过Java代码来控制的,而是由JVM在运行时自动进行的。因此,你不能直接编写Java代码来显式地使用偏向锁。不过,你可以编写一个使用synchronized关键字的简单示例,JVM可能会自动将其优化为使用偏向锁(取决于JVM的实现和运行时的配置)。

示例代码:

public class BiasedLockingExample {  
  
    // 这个对象用作同步的锁  
    private final Object lock = new Object();  
      
    // 共享资源  
    private int sharedData;  
  
    // 使用synchronized关键字进行同步的方法  
    public synchronized void synchronizedMethod() {  
        sharedData++;  
    }  
  
    // 使用对象锁进行同步的方法  
    public void lockedMethod() {  
        synchronized (lock) {  
            sharedData += 2;  
        }  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        // 创建示例对象  
        BiasedLockingExample example = new BiasedLockingExample();  
  
        // 使用Lambda表达式和Stream API创建并启动多个线程  
        IntStream.range(0, 10).forEach(i -> {  
            new Thread(() -> {  
                // 每个线程多次调用同步方法  
                for (int j = 0; j < 10000; j++) {  
                    example.synchronizedMethod();  
                    example.lockedMethod();  
                }  
            }).start();  
        });  
  
        // 让主线程睡眠一段时间,等待其他线程执行完毕  
        Thread.sleep(2000);  
  
        // 输出共享数据的最终值  
        System.out.println("Final sharedData value: " + example.sharedData);  
    }  
}

在这个示例中,我们有一个BiasedLockingExample类,它有两个同步方法:synchronizedMethod和lockedMethod。synchronizedMethod是一个实例同步方法,它隐式地使用this作为锁对象。lockedMethod是一个使用显式对象锁的方法,它使用lock对象作为锁。

当多个线程调用这些方法时,JVM可能会观察到只有一个线程在反复获取同一个锁,并且没有其他线程竞争该锁。在这种情况下,JVM可能会将锁偏向到这个线程,以减少获取和释放锁的开销。

然而,请注意以下几点:

  • 偏向锁的使用是由JVM动态决定的,你不能强制JVM使用偏向锁。
  • 在高并发环境下,如果锁竞争激烈,偏向锁可能会被撤销并升级到更重的锁状态,如轻量级锁或重量级锁。
  • 偏向锁适用于锁被同一个线程多次获取的场景。如果锁被多个线程频繁地争用,偏向锁可能不是最优的选择。

由于偏向锁是透明的优化,因此你不需要在代码中做任何特殊的事情来利用它。只需编写正常的同步代码,让JVM来决定是否应用偏向锁优化。

分段锁

在Java中,"分段锁"并不是一个官方的术语,但它通常被用来描述一种并发控制策略,其中数据结构或资源被分成多个段,并且每个段都有自己的锁。这种策略的目的是提高并发性能,允许多个线程同时访问不同的段,而不会相互阻塞。

而在 Java 里面的经典例子则是ConcurrentHashMap,在早期的ConcurrentHashMap实现中,内部采用了一个称为Segment的类来表示哈希表的各个段,每个Segment对象都持有一个锁。这种设计允许多个线程同时读写哈希表的不同部分,而不会产生锁竞争,从而提高了并发性能。

然而,需要注意的是,从Java 8开始,ConcurrentHashMap的内部实现发生了重大变化。它不再使用Segment,而是采用了一种基于CAS(Compare-and-Swap)操作和Node数组的新设计,以及红黑树来处理哈希冲突。这种新设计提供了更高的并发性和更好的性能。尽管如此,"分段锁"这个概念仍然可以用来描述这种将数据结构分成多个可独立锁定的部分的通用策略。

我们看一个分段锁实现安全计数器的代码:

import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;  
  
public class SegmentedCounter {  
    private final int size;  
    private final Lock[] locks;  
    private final int[] counters;  
  
    public SegmentedCounter(int size) {  
        this.size = size;  
        this.locks = new Lock[size];  
        this.counters = new int[size];  
        for (int i = 0; i < size; i++) {  
            locks[i] = new ReentrantLock();  
        }  
    }  
  
    public void increment(int index) {  
        locks[index].lock();  
        try {  
            counters[index]++;  
        } finally {  
            locks[index].unlock();  
        }  
    }  
  
    public int getValue(int index) {  
        locks[index].lock();  
        try {  
            return counters[index];  
        } finally {  
            locks[index].unlock();  
        }  
    }  
}

在这个例子中,SegmentedCounter类有一个counters数组和一个locks数组。每个计数器都有一个与之对应的锁,这使得线程可以独立地更新不同的计数器,而不会相互干扰。当然,这个简单的例子并没有考虑一些高级的并发问题,比如锁的粒度选择、锁争用和公平性等问题。在实际应用中,你可能需要根据具体的需求和性能目标来调整设计。

所以,你学会了么?

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论