欢迎关注我的公众号:观知小阁。包含各种类的文章,内容更丰富,更新及时且不迷路。

1. 从synchronized的局限说起

在Java并发编程的早期,synchronized关键字是线程同步的唯一选择。它简单易用,JVM自动管理锁的获取和释放,但正是这种“自动化”带来了诸多限制:

public synchronized void transfer(Account from, Account to, int amount) {
    // 如果这里发生死锁,线程将永远阻塞
    from.withdraw(amount);
    to.deposit(amount);
}

synchronized的三大痛点:

  • 无法中断:线程获取锁失败会一直阻塞,无法响应中断请求

  • 非公平锁:默认采用非公平策略,可能导致线程饥饿

  • 单一条件队列:所有等待线程都在同一个队列中,唤醒不够精准

2. ReentrantLock的登场:更灵活的并发控制

JDK 1.5引入的java.util.concurrent包带来了ReentrantLock,它基于AQS(AbstractQueuedSynchronizer)框架实现,提供了更丰富的功能。

2.1 基础使用:手动控制的艺术

public class Account {
    private final ReentrantLock lock = new ReentrantLock();
    private int balance;
    
    public void deposit(int amount) {
        lock.lock();  // 手动获取锁
        try {
            balance += amount;
            System.out.println(Thread.currentThread().getName() + 
                             " 存入: " + amount + ",余额: " + balance);
        } finally {
            lock.unlock();  // 必须在finally中释放锁
        }
    }
}

关键要点:

  • lock()和unlock()必须成对出现

  • unlock()必须放在finally块中,确保异常时也能释放锁

  • 相比synchronized的自动管理,ReentrantLock需要开发者更谨慎

2.2 可重入性:同一线程的多次加锁

可重入锁允许同一个线程多次获取同一把锁,这是避免死锁的关键特性。

public class ReentrantDemo {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void outerMethod() {
        lock.lock();  // 第一次加锁,state从0→1
        try {
            System.out.println("Outer method acquired lock");
            innerMethod();  // 调用内部方法,再次加锁
        } finally {
            lock.unlock();  // 最终释放锁
        }
    }
    
    private void innerMethod() {
        lock.lock();  // 第二次加锁,state从1→2(可重入)
        try {
            System.out.println("Inner method re-acquired lock");
        } finally {
            lock.unlock();  // state从2→1
        }
    }
}

底层原理:通过AQS的state变量记录重入次数,exclusiveOwnerThread记录当前持有锁的线程。

3. 深入AQS:并发框架的基石

3.1 AQS的核心结构

AQS(抽象队列同步器)是Java并发包的灵魂,采用模板方法模式设计。

组件作用说明
state同步状态volatile int类型,不同同步器有不同含义
CLH队列线程等待队列FIFO双向链表,保证公平性
Node队列节点封装线程信息和等待状态

在ReentrantLock中,state的含义是:

  • state = 0:锁空闲

  • state = 1:锁被占用

  • state > 1:锁被重入,数值表示重入次数

3.2 加锁流程解析

非公平锁(默认)流程:

  1. 线程调用lock()方法

  2. 直接尝试CAS将state从0改为1

  3. 成功:获取锁,记录当前线程为持有者

  4. 失败:调用AQS的acquire(1)入队等待

公平锁流程:

  1. 线程调用lock()方法

  2. 先检查队列是否有等待线程(hasQueuedPredecessors())

  3. 有等待线程:直接入队

  4. 无等待线程:尝试CAS获取锁

公平锁的核心代码:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 关键区别:先检查队列
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // ... 可重入逻辑
}

4. 公平锁 vs 非公平锁:性能与公平的权衡

4.1 性能对比

特性公平锁非公平锁
获取顺序严格按照FIFO顺序允许插队,性能更高
吞吐量较低(约低20%-30%)较高
适用场景对公平性要求高的系统高并发性能优先

实际测试数据(JDK 17环境下):

  • 低竞争场景:synchronized性能更优

  • 高竞争场景:ReentrantLock(非公平)性能更稳定

  • 超高频场景:synchronized的偏向锁优化效果明显

4.2 选型建议

  • 电商秒杀系统:优先选择非公平锁,保证高吞吐量

  • 银行转账系统:使用公平锁,确保先来先服务

  • 内容审核队列:公平锁避免线程饥饿

5. ReentrantLock vs synchronized:全面对比

5.1 功能特性对比表

维度synchronizedReentrantLock
实现方式JVM内置关键字JDK API类
锁释放自动释放手动unlock()
可中断性不支持支持(lockInterruptibly())
公平锁仅非公平支持公平/非公平
超时获取不支持支持(tryLock(timeout))
条件变量单一队列多Condition支持
性能JDK 1.6后优化良好高并发下更稳定

5.2 内存语义一致性

两者都提供相同的内存语义:

  • 可见性:happens-before原则保证

  • 原子性:临界区操作不可分割

  • 有序性:防止指令重排序

6. 高级特性详解

6.1 可中断锁:优雅处理死锁

public class InterruptibleLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void performTask() throws InterruptedException {
        lock.lockInterruptibly();  // 可中断的锁获取
        try {
            // 执行耗时操作
            while (!Thread.currentThread().isInterrupted()) {
                // 业务逻辑
            }
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

中断流程:false → true → false

  1. 线程调用interrupt(),中断标志置为true

  2. lockInterruptibly()触发InterruptedException

  3. 异常处理后,中断标志被清空(false)

6.2 超时锁:防止系统雪崩

public class TimeoutLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    
    public boolean addData(String item, long timeout, TimeUnit unit) 
            throws InterruptedException {
        if (lock.tryLock(timeout, unit)) {  // 尝试获取锁,最多等待timeout
            try {
                return data.add(item);
            } finally {
                lock.unlock();
            }
        } else {
            // 超时降级策略
            log.warn("获取锁超时,执行异步处理");
            asyncService.handleAsync(item);
            return false;
        }
    }
}

超时时间设置原则:

  • 参考接口响应时间阈值(如500ms)

  • 预留降级处理时间(如300ms超时,200ms降级)

  • 根据业务重要性动态调整

6.3 条件变量:精细化线程协作

public class ProducerConsumer {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();  // 队列未满条件
    private final Condition notEmpty = lock.newCondition(); // 队列非空条件
    
    public void put(Object item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.isFull()) {
                notFull.await();  // 等待队列未满
            }
            queue.put(item);
            notEmpty.signal();  // 唤醒消费者
        } finally {
            lock.unlock();
        }
    }
}

相比synchronized的wait()/notify():

  • 精准唤醒:只唤醒特定条件的线程

  • 多队列管理:不同条件使用不同队列

  • 减少无效唤醒:提升并发效率

7. 实战应用场景

7.1 高并发缓存更新(防缓存击穿)

public class CacheService {
    private final Map<String, Object> cache = new ConcurrentHashMap<>();
    private final Map<String, ReentrantLock> keyLocks = new ConcurrentHashMap<>();
    
    public Object get(String key) {
        Object value = cache.get(key);
        if (value == null) {
            ReentrantLock lock = keyLocks.computeIfAbsent(key, 
                k -> new ReentrantLock());
            if (lock.tryLock()) {  // 非公平锁,保证吞吐量
                try {
                    // 双重检查
                    value = cache.get(key);
                    if (value == null) {
                        value = loadFromDB(key);
                        cache.put(key, value);
                    }
                } finally {
                    lock.unlock();
                    keyLocks.remove(key);
                }
            } else {
                // 其他线程正在加载,等待
                lock.lock();
                lock.unlock();
                return cache.get(key);
            }
        }
        return value;
    }
}

优化要点:

  • 锁粒度控制:每个key独立锁,避免全局竞争

  • 双重检查:防止重复加载

  • 非公平锁:高并发下性能优先

7.2 分布式锁兜底方案

public class DistributedLockWithFallback {
    private final ReentrantLock localLock = new ReentrantLock();
    private final DistributedLock distributedLock = new RedisDistributedLock();
    
    public boolean deductStock(String productId, int num) {
        // 1. 先获取本地锁(300ms超时)
        if (!localLock.tryLock(300, TimeUnit.MILLISECONDS)) {
            log.warn("本地锁获取失败,请求过于频繁");
            return false;
        }
        
        try {
            // 2. 获取分布式锁(3秒超时)
            if (!distributedLock.tryLock(productId, 3, TimeUnit.SECONDS)) {
                log.warn("分布式锁获取失败");
                return false;
            }
            
            try {
                // 3. 执行业务逻辑
                return doDeductStock(productId, num);
            } finally {
                distributedLock.unlock(productId);
            }
        } finally {
            localLock.unlock();
        }
    }
}

释放顺序原则:先释放分布式锁,再释放本地锁

  • 防止其他节点误认为资源可用

  • 避免跨节点并发问题

8. 最佳实践与避坑指南

8.1 使用规范

8.1.1 必须使用try-finally:
lock.lock();
try {
    // 业务逻辑
} finally {
    lock.unlock();
}
8.1.2 避免锁泄漏:
  • 锁获取次数必须等于释放次数

  • 重入N次必须解锁N次

8.1.3 合理设置超时:
  • 根据业务响应时间设置

  • 配合降级策略使用

8.2 性能优化

8.2.1 缩小锁粒度:
  • 避免大范围同步

  • 使用分段锁或读写锁

8.2.2 读多写少场景:
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
8.2.3 虚拟线程注意事项:
  • JDK 21+中,虚拟线程不建议使用synchronized

  • 优先使用ReentrantLock避免线程固定

8.3 常见陷阱

陷阱现象解决方案
忘记解锁死锁,线程阻塞必须使用finally块
重入计数不匹配锁未真正释放确保lock/unlock成对
滥用公平锁性能下降20%-30%非公平锁优先
锁粒度过大并发度低细粒度锁设计

9. 总结

9.1 技术选型决策树

是否需要高级功能?
├── 否 → 优先使用synchronized(简洁安全)
└── 是 → 选择ReentrantLock
    ├── 需要公平性? → 使用公平锁
    ├── 需要超时控制? → 使用tryLock(timeout)
    ├── 需要可中断? → 使用lockInterruptibly()
    └── 需要多条件? → 使用Condition

9.2 未来发展趋势

  • 虚拟线程普及:ReentrantLock在虚拟线程中的优势更加明显

  • 硬件并发优化:随着CPU核心数增加,锁竞争优化更加重要

  • 混合锁策略:根据场景动态选择锁类型将成为趋势

9.3 核心思想

没有最好的锁,只有最适合的锁。ReentrantLock和synchronized各有优劣,关键在于:

  • 理解业务场景:根据并发特点选择合适锁

  • 掌握底层原理:知其然更要知其所以然

  • 持续性能优化:监控、测试、调优闭环

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐