
1、乐观锁 VS 悲观锁
1、乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

java
package com.taiyuan.javademoone.lockdemo;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 乐观锁示例类,使用CAS(Compare And Swap)操作实现线程安全的计数器
*/
public class OptimisticLockCASExample {
// 使用AtomicInteger来保证计数器的线程安全
private AtomicInteger counter = new AtomicInteger(0);
/**
* 尝试递增计数器的值
* 如果当前计数器的值等于期望值,则将其递增1,否则不进行任何操作
* 此方法是非阻塞的,如果更新失败则立即返回false
* 注意:此方法可能会导致CPU空转,因此建议使用自旋锁或其他方式来避免
*
* @param expect 期望的当前计数器值
* @return 如果更新成功则返回true,否则返回false
*/
public boolean increment(int expect) {
return counter.compareAndSet(expect, expect + 1);
}
/**
* 使用自旋加重试的方式递增计数器的值,确保线程安全
* 这种方式可以避免因线程调度不均导致的饥饿问题
*/
public void incrementWithRetry() {
while (true) {
int current = counter.get();
if (counter.compareAndSet(current, current + 1)) {
break;
}
//Thread.yield() 的作用是让当前线程主动放弃CPU使用权,提示调度器将CPU资源让给其他线程,
// 以避免在自旋过程中长时间占用CPU造成浪费。 在此处用于减轻因CAS失败持续重试带来的性能损耗。
Thread.yield();
}
}
/**
* 获取当前计数器的值
*
* @return 当前计数器的值
*/
public int getCounter() {
return counter.get();
}
/**
* 主函数,用于演示使用乐观锁的计数器在多线程环境下的使用
*
* @param args 命令行参数
* @throws InterruptedException 如果线程被中断
*/
public static void main(String[] args) throws InterruptedException {
OptimisticLockCASExample example = new OptimisticLockCASExample();
// 创建两个线程,每个线程尝试递增计数器1000次
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.incrementWithRetry();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.incrementWithRetry();
}
});
// 启动线程
t1.start();
t2.start();
// 等待线程结束
t1.join();
t2.join();
// 输出最终的计数器值
System.out.println("最终计数器值: " + example.getCounter());
}
}
2、悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

Synchronized使用
csharp
package com.taiyuan.javademoone.lockdemo;
/**
* PessimisticLockExample 类用于演示乐观锁和悲观锁的概念
* 通过对比加锁和不加锁的情况下来展示并发环境下数据一致性的问题
*/
public class PessimisticLockExample {
// 计数器,用于演示并发环境下数据的不一致性问题
private int counter = 0;
/**
* increment 方法用于增加计数器的值
* 注意:此方法在并发环境下可能会导致数据不一致
*/
public void increment() {
counter++;
}
/**
* incrementWithLock 方法用于增加计数器的值
* 此方法使用了 synchronized 关键字,确保在并发环境下数据的一致性
*/
public synchronized void incrementWithLock() {
counter++;
}
/**
* getCounter 方法用于获取计数器的值
*
* @return 计数器的当前值
*/
public int getCounter() {
return counter;
}
/**
* testWithoutLock 方法用于测试在不加锁的情况下,计数器的值
* 此方法创建了两个线程,分别对计数器进行增加操作
*
* @throws InterruptedException 如果在等待线程结束时被中断
*/
public static void testWithoutLock() throws InterruptedException {
PessimisticLockExample example = new PessimisticLockExample();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) { // 增大循环次数
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
example.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("不加锁的结果: " + example.getCounter());
}
/**
* testWithLock 方法用于测试在加锁的情况下,计数器的值
* 此方法创建了两个线程,分别对计数器进行增加操作
*
* @throws InterruptedException 如果在等待线程结束时被中断
*/
public static void testWithLock() throws InterruptedException {
PessimisticLockExample example = new PessimisticLockExample();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
example.incrementWithLock();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
example.incrementWithLock();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("加锁的结果: " + example.getCounter());
}
/**
* main 方法作为程序的入口点
* 此方法分别测试了不加锁和加锁的情况下,计数器的值
*
* @param args 命令行参数
* @throws InterruptedException 如果在等待线程结束时被中断
*/
public static void main(String[] args) throws InterruptedException {
testWithoutLock(); // 测试不加锁的情况
testWithLock(); // 测试加锁的情况
}
}
ReentrantLock 可重入锁
java
package com.taiyuan.javademoone.lockdemo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* ReentrantLockExample001 类用于演示使用 ReentrantLock 来同步代码段
* 该类提供了一个线程安全的计数器,多个线程可以安全地对其进行递增操作
*/
public class ReentrantLockExample001 {
// 计数器,初始值为 0
private int counter = 0;
// ReentrantLock 实例,用于同步对共享资源(counter)的访问
private final Lock lock = new ReentrantLock();
/**
* 对计数器进行递增操作
* 该方法通过获取和释放锁来确保在同一时刻只有一个线程可以执行此方法
*/
public void increment() {
lock.lock(); // 获取锁
try {
counter++;
} finally {
lock.unlock(); // 确保锁被释放
}
}
/**
* 获取计数器的值
*
* @return 计数器的当前值
*/
public int getCounter() {
return counter;
}
/**
* 主方法,用于测试计数器的线程安全递增操作
* 创建两个线程,每个线程对计数器递增 10000 次,最终输出计数器的值
*
* @param args 命令行参数
* @throws InterruptedException 如果线程被中断
*/
public static void main(String[] args) throws InterruptedException {
ReentrantLockExample001 example = new ReentrantLockExample001();
// 创建并启动两个线程,每个线程递增计数器 10000 次
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
// 输出最终的计数器值
System.out.println(" 加锁输出的结果 " + example.getCounter());
}
}
2、自旋锁 VS 适应性自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cup 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁。
自旋锁时间阈值 ( 1.6 引入了适应性自旋锁 )自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
在JDK 1.6中引入了自适应自旋锁。这就意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋 时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么JVM会认为该锁自旋获取到锁的可能性很大,会自动增加等待时间。比如增加到100此循环。相反,如果对于某个锁,自旋很少成功获取锁。那再以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,JVM对程序的锁的状态预测会越来越准确,JVM也会越来越聪明。

java
package com.taiyuan.javademoone.lockdemo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 自定义自旋锁实现
*/
public class SpinLock implements Lock {
// 使用原子引用保存当前持有锁的线程
// 定义了一个AtomicReference类型的成员变量owner,用于以原子方式引用一个线程对象。
// AtomicReference<Thread>:保证对Thread类型引用的原子操作
// owner:通常用于实现如自旋锁等并发控制机制,记录当前持有锁的线程
private AtomicReference<Thread> owner = new AtomicReference<>();
// 记录重入次数
// 线程重入锁,即同一个线程可以多次获取锁,但必须释放锁的次数与获取锁的次数一致才能释放锁
// 定义了一个int类型的成员变量count,用于记录当前线程重入锁的次数。
// count:用于记录当前线程重入锁的次数,初始值为0。
private int count = 0;
/**
* 获取锁
* 如果当前线程已经持有锁,则增加重入计数
* 否则,自旋等待获取锁
*/
@Override
public void lock() {
Thread current = Thread.currentThread();
// 如果是当前线程已经持有锁,则重入次数+1
if (current == owner.get()) {
count++;
System.out.println(Thread.currentThread().getName() + " 重入了锁"+ count);
return;
}
// 自旋等待获取锁
/*
* owner.compareAndSet(null, current):尝试将owner的值从null更新为当前线程current
* 如果更新成功,说明当前锁没有被其他线程占用,当前线程成功获取锁
* 执行流程详解
* 1、首次获取锁:
* 当锁未被占用时 (owner == null)
* compareAndSet(null, current) 会成功,将 owner 设置为当前线程
* !true 为 false,循环不执行,直接获取锁
* 2、锁被占用时:
* 当 owner 不为 null (被其他线程持有)
* compareAndSet(null, current) 会失败,返回 false
* !false 为 true,进入循环体,开始自旋等待
* 3、自旋等待期间:
* 打印自旋等待信息
* 执行 Thread.sleep(100) 短暂休眠(实际自旋锁通常不会休眠)
* 然后再次尝试 CAS 操作
*/
while (!owner.compareAndSet(null, current)) {
// 空循环,模拟自旋
System.out.println(Thread.currentThread().getName() + " 正在自旋等待锁...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 在实际应用中,可以加入一些优化策略,如短暂休眠
}
System.out.println(Thread.currentThread().getName() + " 成功获取锁");
}
/**
* 释放锁
* 如果当前线程是锁的持有者,则根据重入次数决定是减少重入计数还是完全释放锁
*/
@Override
public void unlock() {
Thread current = Thread.currentThread();
// 如果当前线程是锁的持有者
if (current == owner.get()) {
if (count > 0) {
// 如果是重入的情况,减少重入次数
System.out.println(Thread.currentThread().getName() + " 减少重入计数器到: " + count);
count--;
} else {
// 释放锁
owner.set(null);
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
}
}
// 其他Lock接口方法实现(省略部分不相关方法)
@Override
public void lockInterruptibly() throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public boolean tryLock() {
throw new UnsupportedOperationException();
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
csharp
package com.taiyuan.javademoone.lockdemo;
public class SpinLockDemo {
private static SpinLock spinLock = new SpinLock();
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
// 创建5个线程竞争锁
for (int i = 0; i < 5; i++) {
new Thread(() -> {
spinLock.lock();
try {
// 模拟短时间持有锁
System.out.println(Thread.currentThread().getName() + " 开始执行业务逻辑");
for (int j = 0; j < 1000; j++) {
counter++;
}
// 模拟可重入锁
spinLock.lock();
System.out.println(Thread.currentThread().getName() + " 再次获取锁");
try {
System.out.println(Thread.currentThread().getName()+"第二次获取锁后执行");
} finally {
spinLock.unlock();
}
// 模拟业务处理时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
}
}, "线程-" + i).start();
}
Thread.sleep(2000);
System.out.println("最终计数器值: " + counter);
}
}
3、公平锁与非公平锁
公平锁( Fair )
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

java
package com.taiyuan.javademoone.lockdemo;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 公平锁实现 - 先来先服务
*/
public class FairLock implements Lock {
// 锁状态:true=被占用,false=空闲
private AtomicBoolean locked = new AtomicBoolean(false);
// 等待队列,存放等待锁的线程
private Queue<Thread> waitingQueue = new ConcurrentLinkedQueue<>();
// 当前持有锁的线程
private Thread currentHolder = null;
// 重入计数
private int holdCount = 0;
/**
* 获取锁
* 如果锁已经被其他线程持有,则当前线程将被阻塞,直到锁被释放
*/
@Override
public void lock() {
// 获取当前线程
Thread current = Thread.currentThread();
// 重入检查
if (current == currentHolder) {
// 如果当前线程已经持有锁,则增加持有计数
holdCount++;
return;
}
// 将当前线程加入等待队列
waitingQueue.add(current);
// 自旋等待,直到成为队列头部且成功获取锁
while (true) {
// 检查当前线程是否为队列头部且尝试设置锁状态为已锁
if (waitingQueue.peek() == current && locked.compareAndSet(false, true)) {
// 成功获取锁,从队列中移除
waitingQueue.remove();
// 设置当前线程为锁持有者
currentHolder = current;
// 初始化持有计数
holdCount = 1;
return;
}
// 不是队列头部或获取失败,短暂休眠避免忙等待
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// 中断当前线程并抛出运行时异常
Thread.currentThread().interrupt();
throw new RuntimeException("Thread interrupted while waiting for lock", e);
}
}
}
/**
* 释放锁
* <p>
* 此方法用于释放当前线程持有的锁如果当前线程不是锁的持有者,则抛出IllegalMonitorStateException异常
* 当锁的持有计数减为0时,认为锁已经被完全释放,可以被其他线程获取
*/
@Override
public void unlock() {
// 获取当前线程
Thread current = Thread.currentThread();
// 检查当前线程是否是锁的持有者
if (current != currentHolder) {
// 如果不是,抛出异常
throw new IllegalMonitorStateException("Current thread does not hold the lock");
}
// 减少锁的持有计数
if (--holdCount == 0) {
// 当持有计数为0时,释放锁
currentHolder = null;
locked.set(false);
}
}
/**
* 可中断的获取锁
* 如果当前线程已经持有锁,则增加持有计数
* 如果锁被其他线程持有,则当前线程进入等待队列,并可以被中断
* 当锁可用时,当前线程从等待队列中移除,并获取锁
*
* @throws InterruptedException 如果当前线程在等待时被中断
*/
@Override
public void lockInterruptibly() throws InterruptedException {
// 获取当前线程
Thread current = Thread.currentThread();
// 重入检查
if (current == currentHolder) {
holdCount++;
return;
}
// 将当前线程添加到等待队列
waitingQueue.add(current);
while (true) {
// 检查当前线程是否被中断
if (Thread.interrupted()) {
waitingQueue.remove(current);
throw new InterruptedException();
}
// 尝试获取锁
if (waitingQueue.peek() == current && locked.compareAndSet(false, true)) {
waitingQueue.remove();
currentHolder = current;
holdCount = 1;
return;
}
// 短暂休眠,避免忙等待
try {
Thread.sleep(10);
} catch (InterruptedException e) {
waitingQueue.remove(current);
throw e;
}
}
}
/**
* 尝试获取锁
* <p>
* 此方法首先检查当前线程是否已经持有锁(重入情况),如果是,则增加持有计数并返回true
* 如果当前线程未持有锁,它会进一步检查当前线程是否处于等待队列的头部且锁是否可用
* 如果这两个条件都满足,则将当前线程标记为锁的持有者,从等待队列中移除,并设置锁状态为已占用
* 如果条件不满足,则返回false,表示获取锁失败
*
* @return 如果成功获取锁,则返回true;否则返回false
*/
@Override
public boolean tryLock() {
// 获取当前线程
Thread current = Thread.currentThread();
// 重入情况
if (current == currentHolder) {
holdCount++;
return true;
}
// 检查是否是队列头部且锁可用
if (waitingQueue.peek() == current && locked.compareAndSet(false, true)) {
waitingQueue.remove();
currentHolder = current;
holdCount = 1;
return true;
}
// 如果当前线程不在队列头部或者锁不可用,则获取锁失败
return false;
}
/**
* 尝试在指定的等待时间内获取锁
* 如果当前线程已经持有锁,则增加持有计数并返回true
* 如果锁不可用,当前线程会加入等待队列,并在指定时间内重复尝试获取锁
* 如果在指定时间内无法获取锁,或者线程被中断,则抛出InterruptedException
*
* @param time 最长等待时间
* @param unit 时间单位
* @return 如果获取到锁则返回true,否则返回false
* @throws InterruptedException 如果线程被中断
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// 将等待时间转换为纳秒
long remainingNanos = unit.toNanos(time);
// 获取当前线程
Thread current = Thread.currentThread();
// 重入检查
if (current == currentHolder) {
holdCount++;
return true;
}
// 将当前线程添加到等待队列
waitingQueue.add(current);
try {
// 计算截止时间
long deadline = System.nanoTime() + remainingNanos;
while (true) {
// 检查线程是否被中断
if (Thread.interrupted()) {
throw new InterruptedException();
}
// 尝试获取锁
if (waitingQueue.peek() == current && locked.compareAndSet(false, true)) {
// 获取锁成功,移除等待队列中的当前线程
waitingQueue.remove();
// 更新当前锁持有者和持有计数
currentHolder = current;
holdCount = 1;
return true;
}
// 检查是否超过指定的等待时间
if (System.nanoTime() > deadline) {
// 超时,移除等待队列中的当前线程
waitingQueue.remove(current);
return false;
}
// 短暂休眠避免忙等待
Thread.sleep(1);
}
} catch (InterruptedException e) {
// 移除等待队列中的当前线程
waitingQueue.remove(current);
throw e;
}
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException("Conditions not supported by this lock");
}
}
csharp
package com.taiyuan.javademoone.lockdemo;
/**
* FairLockDemo 类用于演示公平锁的使用
* 公平锁是指多个线程按照申请锁的顺序来获取锁,避免了线程饿死的情况
*/
public class FairLockDemo {
// 初始化一个公平锁
private static FairLock fairLock = new FairLock();
// 初始化一个共享计数器
private static int sharedCounter = 0;
/**
* 程序的入口点
* 创建并启动多个线程来竞争公平锁,每个线程对共享计数器进行递增操作
*
* @param args 命令行参数
* @throws InterruptedException 如果线程被中断
*/
public static void main(String[] args) throws InterruptedException {
// 创建10个线程竞争锁
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
final int threadNum = i;
threads[i] = new Thread(() -> {
fairLock.lock();
try {
System.out.println("线程" + threadNum + " 获取锁,开始操作");
// 模拟工作
for (int j = 0; j < 100; j++) {
sharedCounter++;
}
Thread.sleep(100); // 模拟工作耗时
System.out.println("线程" + threadNum + " 完成操作,释放锁");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
fairLock.unlock();
}
});
}
// 启动所有线程
for (Thread t : threads) {
t.start();
// 稍微间隔一下,确保线程按顺序启动
Thread.sleep(10);
}
// 等待所有线程完成
for (Thread t : threads) {
t.join();
}
System.out.println("最终计数器值: " + sharedCounter);
}
}
非公平锁( Nonfair )
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
-
非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
-
Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

java
package com.taiyuan.javademoone.lockdemo;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.TimeUnit;
/**
* 非公平锁实现 - 不保证线程获取锁的顺序
* 特点:
* 1. 线程尝试获取锁时不考虑等待队列,直接尝试获取
* 2. 获取失败才进入等待状态
* 3. 吞吐量比公平锁高,但可能导致某些线程等待时间过长
*/
public class NonfairLock implements Lock {
// 锁状态标记,true表示锁被占用,false表示锁空闲
private AtomicBoolean locked = new AtomicBoolean(false);
// 当前持有锁的线程(用于实现可重入)
private Thread ownerThread = null;
// 重入计数器
private int holdCount = 0;
@Override
public void lock() {
Thread current = Thread.currentThread();
// 检查是否是重入情况
if (ownerThread == current) {
holdCount++;
System.out.println(current.getName() + " 重入锁,重入次数: " + holdCount);
return;
}
// 非公平锁核心:直接尝试获取锁,不考虑等待队列
boolean acquired = locked.compareAndSet(false, true);
if (acquired) {
// 成功获取锁
ownerThread = current;
holdCount = 1;
System.out.println(current.getName() + " 直接获取锁成功");
} else {
// 获取失败,自旋等待
spinWaitForLock(current);
}
}
/**
* 自旋等待锁
*/
private void spinWaitForLock(Thread current) {
int spinCount = 0;
while (true) {
spinCount++;
System.out.println(current.getName() + " 第" + spinCount + "次尝试获取锁...");
// 再次尝试获取
if (locked.compareAndSet(false, true)) {
ownerThread = current;
holdCount = 1;
System.out.println(current.getName() + " 经过" + spinCount + "次尝试后获取锁成功");
return;
}
// 短暂休眠,避免过度消耗CPU
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println(current.getName() + " 等待锁时被中断");
throw new RuntimeException("锁获取被中断", e);
}
}
}
@Override
public void unlock() {
Thread current = Thread.currentThread();
if (current != ownerThread) {
throw new IllegalMonitorStateException(current.getName() + " 尝试释放不属于它的锁");
}
holdCount--;
if (holdCount == 0) {
ownerThread = null;
locked.set(false);
System.out.println(current.getName() + " 完全释放锁");
} else {
System.out.println(current.getName() + " 释放重入锁,剩余重入次数: " + holdCount);
}
}
// 其他Lock接口方法实现(省略部分不相关方法)
@Override
public void lockInterruptibly() throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public boolean tryLock() {
throw new UnsupportedOperationException();
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
csharp
package com.taiyuan.javademoone.lockdemo;
/**
* 非公平锁演示类
* 本类通过多个工作线程竞争非公平锁来演示线程同步和锁的机制
*/
public class NonfairLockDemo {
// 定义一个非公平锁实例
private static NonfairLock lock = new NonfairLock();
// 定义一个共享资源,初始值为0
private static int sharedResource = 0;
/**
* 程序入口
* 创建并启动多个工作线程,这些线程将竞争同一个非公平锁
* @param args 命令行参数
* @throws InterruptedException 如果线程被中断
*/
public static void main(String[] args) throws InterruptedException {
// 创建5个工作线程
for (int i = 0; i < 5; i++) {
new WorkerThread("Worker-" + i).start();
// 稍微间隔一下,模拟线程不同时启动
Thread.sleep(50);
}
}
/**
* 工作线程类
* 本类代表一个工作线程,每个工作线程会多次尝试获取锁并操作共享资源
*/
static class WorkerThread extends Thread {
/**
* 构造方法
* @param name 线程名称
*/
public WorkerThread(String name) {
super(name);
}
/**
* 线程运行方法
* 模拟多次获取锁并操作共享资源的过程
*/
@Override
public void run() {
// 模拟多次获取锁
for (int i = 0; i < 2; i++) {
lock.lock();
try {
// 进入临界区,操作共享资源
System.out.println(getName() + " 进入临界区,操作共享资源");
sharedResource++;
// 模拟工作耗时
Thread.sleep(200);
// 完成操作,打印共享资源值
System.out.println(getName() + " 完成操作,共享资源值: " + sharedResource);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
// 两次操作之间间隔
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
4、可重入锁 VS 非可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
csharp
package com.taiyuan.javademoone.lockdemo;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantExample {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
System.out.println("外层方法获取锁");
inner(); // 调用内层方法
} finally {
lock.unlock();
System.out.println("外层方法释放锁");
}
}
public void inner() {
lock.lock();
try {
System.out.println("内层方法获取锁");
} finally {
lock.unlock();
System.out.println("内层方法释放锁");
}
}
public static void main(String[] args) {
ReentrantExample example = new ReentrantExample();
example.outer();
}
}
非可重入锁是指同一个线程不能重复获取已经持有的锁,如果尝试这样做会导致线程自己阻塞,最终产生死锁。
arduino
package com.taiyuan.javademoone.lockdemo;
/**
* NonReentrantLock 类提供了一个非可重入锁的实现
* 它允许外部调用者通过 lock() 和 unlock() 方法来获取和释放锁
*/
public class NonReentrantLock {
// 标记锁是否已被获取
private boolean isLocked = false;
/**
* 获取锁的方法
* 如果锁已经被获取,当前线程会等待,直到锁被释放
*
* @throws InterruptedException 如果线程在等待过程中被中断
*/
public synchronized void lock() throws InterruptedException {
// 检查锁状态,如果已锁定,则当前线程等待
while (isLocked) {
wait(); // 如果已锁定,当前线程等待
}
// 获取锁,设置锁状态为已锁定
isLocked = true;
}
/**
* 释放锁的方法
* 释放锁后,会通知其他等待获取锁的线程
*/
public synchronized void unlock() {
// 释放锁,设置锁状态为未锁定
isLocked = false;
// 通知其他等待获取锁的线程
notify();
}
}
csharp
package com.taiyuan.javademoone.lockdemo;
/**
* NonReentrantExample 类用于演示非可重入锁的行为
* 非可重入锁意味着线程在获取锁后,再次请求同一线程的锁时会阻塞
*/
public class NonReentrantExample {
// 初始化一个非可重入锁
private NonReentrantLock lock = new NonReentrantLock();
/**
* 外层方法,用于演示非可重入锁导致的死锁
* @throws InterruptedException 如果在等待锁时线程被中断
*/
public void outer() throws InterruptedException {
lock.lock();
try {
System.out.println("外层方法获取锁");
inner(); // 这里会导致死锁
} finally {
lock.unlock();
System.out.println("外层方法释放锁");
}
}
/**
* 内层方法,尝试获取锁并执行某些操作
* @throws InterruptedException 如果在等待锁时线程被中断
*/
public void inner() throws InterruptedException {
lock.lock();
try {
System.out.println("内层方法获取锁");
} finally {
lock.unlock();
System.out.println("内层方法释放锁");
}
}
/**
* 程序入口点,创建 NonReentrantExample 实例并调用 outer 方法
* @param args 命令行参数
* @throws InterruptedException 如果在执行过程中线程被中断
*/
public static void main(String[] args) throws InterruptedException {
NonReentrantExample example = new NonReentrantExample();
example.outer(); // 调用会导致死锁
}
}
5、共享锁和独占锁(排它锁)
共享锁
共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
-
AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式。
-
java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。
独占锁(排他锁)
独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
独占锁(如 ReentrantLock
)是一种悲观锁,它通过强制互斥保证线程安全,但不区分操作类型。当一个线程(即使是只读线程)持有锁时,其他所有线程(包括读线程)都必须等待。这种策略虽然安全,但会导致不必要的串行化,尤其是在读多写少的场景下性能较差。
java
package com.taiyuan.javademoone.lockdemo;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 共享锁(ReadWriteLock)演示案例
*
* ReadWriteLock 维护了一对锁:读锁和写锁。
* 读锁是共享锁,可以被多个线程同时持有;
* 写锁是独占锁,同一时刻只能被一个线程持有。
*/
public class SharedLockExample {
// 共享资源
private static int sharedData = 0;
// 创建读写锁
private static final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* 读任务 - 获取读锁(共享锁)
*/
static class ReadTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
// 获取读锁
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取了读锁,读取共享数据: " + sharedData);
// 模拟读取操作耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放读锁
readWriteLock.readLock().unlock();
System.out.println(Thread.currentThread().getName() + " 释放了读锁");
}
// 模拟其他操作
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 写任务 - 获取写锁(独占锁)
*/
static class WriteTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 2; i++) {
// 获取写锁
readWriteLock.writeLock().lock();
try {
System.out.println("\n" + Thread.currentThread().getName() + " 获取了写锁,准备修改数据");
// 修改共享数据
sharedData++;
System.out.println(Thread.currentThread().getName() + " 修改共享数据为: " + sharedData);
// 模拟写入操作耗时
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放写锁
readWriteLock.writeLock().unlock();
System.out.println(Thread.currentThread().getName() + " 释放了写锁\n");
}
// 模拟其他操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
// 创建并启动多个读线程和写线程
for (int i = 1; i <= 3; i++) {
new Thread(new ReadTask(), "读线程-" + i).start();
}
for (int i = 1; i <= 2; i++) {
new Thread(new WriteTask(), "写线程-" + i).start();
}
}
}
6、重量级锁 ( Mutex Lock )
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为"重量级锁"。JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。
JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了"轻量级锁"和"偏向锁"。
7、轻量级锁
轻量级锁(lightweight lock)是Java虚拟机为了提高同步性能而引入的一种锁优化技术。
轻量级锁的优势在于,在无竞争的情况下,它避免了线程上下文切换、线程阻塞等操作,从而提高了程序的性能。
但是,轻量级锁在多线程竞争不激烈的情况下,可以有效地提高同步性能,但如果多个线程同时竞争同一个锁对象,轻量级锁的优化会失效,转而使用重量级锁。
"轻量级"是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
8、偏向锁
Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线
程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起
来让这个线程得到了偏护。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级
锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换
ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所
以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过,轻
量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进
一步提高性能。
9、锁升级
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。
锁的状态:
1、无锁(No Lock):对象没有被任何线程持有锁。
2、偏向锁(Biased Lock):锁偏向于第一个获取它的线程,减少无竞争情况下的锁开销。
3、轻量级锁(Lightweight Lock):在竞争不激烈的情况下使用,通过自旋锁来避免线程阻塞。
4、重量级锁(Heavyweight Lock):在高竞争情况下使用,涉及操作系统的互斥量(mutex),会导致线程阻塞和唤醒。
锁升级:
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,
也就是说只能从低到高升级,不会出现锁的降级)。
锁升级的具体过程:
- 无锁(No Lock)
当一个对象刚刚被创建时,它处于无锁状态。此时,对象头(Object Header)中没有锁相关的信息。 - 偏向锁(Biased Lock)
偏向锁是为了优化无竞争情况下的锁操作。当一个线程第一次获取锁时,JVM 会将该线程的 ID 记录在对象头中,并将锁标志位设置为偏向锁。之后,如果同一个线程再次获取锁,无需进行任何同步操作,只需检查对象头中的线程 ID 是否匹配即可。
java
synchronized (obj) {
// 线程第一次获取锁,进入偏向锁状态
}
如果另一个线程尝试获取这个偏向锁,JVM 会撤销偏向锁,升级为轻量级锁。
- 轻量级锁(Lightweight Lock)
当偏向锁被撤销或多个线程竞争同一个锁时,JVM 会将锁升级为轻量级锁。轻量级锁使用自旋锁(spinlock)来避免线程阻塞。自旋锁会让线程在短时间内不断尝试获取锁,而不是立即进入阻塞状态。
java
synchronized (obj) {
// 线程竞争,进入轻量级锁状态
}
如果自旋次数超过一定阈值,或者锁竞争变得激烈,轻量级锁会升级为重量级锁。
- 重量级锁(Heavyweight Lock)
当锁竞争非常激烈时,轻量级锁会升级为重量级锁,使用操作系统的互斥量(mutex)来进行线程同步。这会导致线程阻塞和唤醒,开销较大。
java
synchronized (obj) {
// 竞争激烈,进入重量级锁状态
}
锁升级的实现细节
锁的升级和降级主要通过对象头(Object Header)中的锁标志位和相关数据结构实现。
对象头(Object Header) :包含锁标志位和其他锁相关信息。
Mark Word :对象头的一部分,用于存储锁信息。根据锁的状态,Mark Word 中的内容会有所不同。Mark Word 是 Java 对象头(Object Header)的重要组成部分,它记录了对象在运行时的各种关键元数据。理解 Mark Word 对于掌握 Java 对象内存布局、锁机制和垃圾回收等核心概念至关重要。
以下是对象头中 Mark Word 的不同状态:
1、无锁状态 :Mark Word 中包含对象的哈希码(HashCode)等信息。
2、偏向锁状态 :Mark Word 中包含偏向线程的 ID。
3、轻量级锁状态 :Mark Word 中包含指向栈中锁记录(Lock Record)的指针。
4、重量级锁状态:Mark Word 中包含指向重量级锁(monitor)的指针。
Mark Word 的状态转换:
Mark Word 的内容会随着对象状态的变化而改变:
- 新建对象:处于无锁状态,存储哈希码(当调用hashCode()时生成)、分代年龄等
- 首次同步:
-
- 如果启用偏向锁:转换为偏向锁状态,记录线程ID
- 如果禁用偏向锁:直接进入轻量级锁状态
- 出现竞争:
-
- 偏向锁撤销:回到无锁状态
- 轻量级锁膨胀:转为重量级锁状态
- GC发生时:转为GC标记状态
Java 对象内存布局
scss
┌─────────────────────────┬───────────────┬─────────────────┬────────────┐
│ Mark Word │ Class Pointer │ Instance Data │ Padding │
│ (8 bytes/64bit) │ (4 bytes) │ (按字段顺序排列) │ (可选对齐) │
└─────────────────────────┴───────────────┴─────────────────┴────────────┘
- Mark Word:存储对象运行时数据
- Class Pointer:指向对象所属类的元数据指针
- Instance Data:对象实际存储的有效数据
- Padding:对齐填充(可选)
10、分段锁(Segment Locking)
一、分段锁核心原理
分段锁是一种并发容器优化技术,通过将数据划分为多个独立段(Segment),每个段拥有自己的锁,从而实现更细粒度的并发控制。与传统的全局锁相比,分段锁允许多个线程同时访问容器的不同段,显著提高了并发性能。
核心特点:
- 锁分离:将单个全局锁拆分为多个段锁
- 减小锁粒度:线程只需锁定操作涉及的段
- 提高并发度:不同段上的操作可以并行执行
二、分段锁实现详解
1. 基本结构设计
java
/**
* 自定义分段锁Map实现
* @param <K> 键类型
* @param <V> 值类型
*/
public class SegmentLockMap<K, V> {
// 默认分段数量
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 分段数组,每个段包含自己的HashMap和锁
private final Segment<K, V>[] segments;
// 哈希算法计算段索引
private int segmentMask;
private int segmentShift;
/**
* 分段内部类
*/
static final class Segment<K, V> {
// 每个段维护独立的HashMap
final HashMap<K, V> map = new HashMap<>();
// 每个段有自己的锁
final ReentrantLock lock = new ReentrantLock();
}
/**
* 构造函数
* @param concurrencyLevel 并发级别(分段数量)
*/
@SuppressWarnings("unchecked")
public SegmentLockMap(int concurrencyLevel) {
// 保证分段数是2的幂次方
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
// 初始化分段数组
this.segments = new Segment[ssize];
for (int i = 0; i < ssize; ++i) {
segments[i] = new Segment<>();
}
}
/**
* 根据key的hash计算段索引
*/
private Segment<K, V> segmentFor(K key) {
int hash = hash(key.hashCode());
return segments[(hash >>> segmentShift) & segmentMask];
}
// 高性能哈希算法
private static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
}
2. 线程安全操作方法
csharp
/**
* 线程安全的put操作
*/
public V put(K key, V value) {
Segment<K, V> seg = segmentFor(key);
seg.lock.lock(); // 获取段锁
try {
return seg.map.put(key, value); // 操作段内数据
} finally {
seg.lock.unlock(); // 释放段锁
}
}
/**
* 线程安全的get操作
*/
public V get(K key) {
Segment<K, V> seg = segmentFor(key);
seg.lock.lock(); // 获取段锁
try {
return seg.map.get(key); // 读取段内数据
} finally {
seg.lock.unlock(); // 释放段锁
}
}
/**
* 线程安全的size操作(需要锁定所有段)
*/
public int size() {
// 按顺序锁定所有段(避免死锁)
for (int i = 0; i < segments.length; ++i) {
segments[i].lock.lock();
}
try {
int count = 0;
// 统计所有段的大小
for (Segment<K, V> seg : segments) {
count += seg.map.size();
}
return count;
} finally {
// 逆序释放所有锁
for (int i = segments.length - 1; i >= 0; --i) {
segments[i].lock.unlock();
}
}
}
三、完整案例演示
1. 测试类实现
ini
public class SegmentLockDemo {
public static void main(String[] args) throws InterruptedException {
// 创建分段锁Map,16个段
SegmentLockMap<String, Integer> map = new SegmentLockMap<>(16);
// 创建10个写线程
List<Thread> writers = new ArrayList<>();
for (int i = 0; i < 10; i++) {
final int threadId = i;
writers.add(new Thread(() -> {
for (int j = 0; j < 100; j++) {
String key = "key-" + threadId + "-" + j;
map.put(key, j);
System.out.println(Thread.currentThread().getName() + " 写入: " + key);
}
}, "Writer-" + i));
}
// 创建5个读线程
List<Thread> readers = new ArrayList<>();
for (int i = 0; i < 5; i++) {
final int threadId = i;
readers.add(new Thread(() -> {
for (int j = 0; j < 200; j++) {
String key = "key-" + (j % 10) + "-" + (j % 100);
Integer value = map.get(key);
System.out.println(Thread.currentThread().getName() + " 读取: " + key + "=" + value);
}
}, "Reader-" + i));
}
// 启动所有线程
writers.forEach(Thread::start);
readers.forEach(Thread::start);
// 等待所有线程完成
for (Thread writer : writers) {
writer.join();
}
for (Thread reader : readers) {
reader.join();
}
// 打印最终大小
System.out.println("最终Map大小: " + map.size());
}
}
2. 输出示例
vbnet
Writer-0 写入: key-0-0
Reader-0 读取: key-0-0=0
Writer-1 写入: key-1-0
Reader-1 读取: key-1-0=0
Writer-2 写入: key-2-0
...
Writer-9 写入: key-9-99
Reader-4 读取: key-4-4=4
最终Map大小: 1000
四、关键机制解析
1. 哈希分段算法
scss
// 计算段索引的哈希算法
private Segment<K, V> segmentFor(K key) {
int hash = hash(key.hashCode());
return segments[(hash >>> segmentShift) & segmentMask];
}
// 优化后的哈希函数
private static int hash(int h) {
// 通过多次位移和异或运算打乱哈希值
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
设计要点:
- 使用高位哈希值确定段位置
- 保证键的均匀分布,避免段倾斜
- 段数量必须是2的幂次方,便于位运算优化
2. 锁分段策略对比
策略类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
哈希分段 | 实现简单,分布均匀 | 需要好的哈希算法 | 通用键值存储 |
范围分段 | 支持范围查询 | 可能分布不均 | 有序数据集 |
功能分段 | 按功能隔离 | 设计复杂 | 多功能容器 |
3. 分段锁性能优化
1. 段数量选择:
ini
// 根据CPU核心数动态设置段数
int concurrencyLevel = Runtime.getRuntime().availableProcessors() * 2;
SegmentLockMap<String, Integer> map = new SegmentLockMap<>(concurrencyLevel);
2. 锁升级优化:
csharp
// 尝试获取锁,失败后降级为同步块
if (seg.lock.tryLock()) {
try {
// 快速路径
} finally {
seg.lock.unlock();
}
} else {
synchronized (seg) {
// 慢速路径
}
}
五、与JDK实现的对比
1. JDK7 ConcurrentHashMap实现
ini
// JDK7中的分段实现
static final class Segment<K,V> extends ReentrantLock {
transient volatile HashEntry<K,V>[] table;
transient int count;
}
// 分段查找
public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
int h = hash(key.hashCode());
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
2. JDK8+的改进
JDK8后改为数组+链表+红黑树 结构,使用CAS+synchronized优化:
java
// JDK8的Node实现
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
// 使用synchronized锁定链表头节点
Node<K,V> f;
synchronized (f) {
if (tabAt(tab, i) == f) {
// 链表操作
}
}
总结
分段锁通过减小锁粒度显著提升了并发容器的性能,其核心在于:
- 哈希分段实现数据均匀分布
- 独立段锁支持并行访问
- 精细控制锁竞争范围
虽然JDK8后的ConcurrentHashMap已改用更先进的实现,但分段锁设计思想仍是理解高性能并发数据结构的重要基础。掌握分段锁原理有助于开发者设计自定义的并发容器,并在适当场景下实现最优性能。