Java并发——Lock锁

在多线程编程中,锁是保证数据一致性的关键工具。Java 从 JDK 1.5 开始提供了 java.util.concurrent.locks 包,其中的 Lock 接口及其实现类比传统的 synchronized 更加强大和灵活。本文将带你深入理解 ReentrantLockReentrantReadWriteLock 的核心特性,并通过实战代码展示它们的用法。

一、为什么需要 Lock?

synchronized 时代,我们通过关键字隐式地获取和释放锁,虽然使用简单,但存在一些局限性:

  • 不可中断:线程获取不到锁时会一直阻塞,无法响应中断。

  • 无法限时等待:无法设置获取锁的超时时间。

  • 无法实现公平锁:所有等待线程竞争锁,可能导致某些线程长时间获取不到锁。

  • 锁粒度单一:所有同步代码块共享同一把锁,无法细分读写操作。

Lock 接口的出现弥补了这些不足,它提供了更灵活的锁操作,让开发者可以手动控制锁的获取和释放,并支持公平锁、可中断锁、限时等待等高级特性。

二、ReentrantLock -- 可重入的独占锁

ReentrantLockLock 接口最常用的实现类,它是一个可重入的互斥锁 ,具备与 synchronized 相同的基础行为,但功能更丰富。

2.1 基本用法

ReentrantLock 的使用需要显式地 lock()unlock(),推荐在 finally 块中释放锁,确保即使发生异常也能释放。

下面是一个经典的"卖票"示例,展示如何使用 ReentrantLock 保证线程安全:

java 复制代码
class Ticket {
    private final ReentrantLock lock = new ReentrantLock();
    private int number = 20;

    public void sale() {
        lock.lock();  // 加锁
        try {
            if (number <= 0) {
                System.out.println(Thread.currentThread().getName() + " 票已售罄!");
                return;
            }
            System.out.println(Thread.currentThread().getName() + " 开始买票,当前票数:" + number);
            Thread.sleep(200);
            System.out.println(Thread.currentThread().getName() + " 买票结束,剩余票数:" + --number);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();  // 解锁
        }
    }
}

2.2 可重入性

可重入锁(递归锁)允许同一个线程多次获取同一把锁,而不会产生死锁。synchronizedReentrantLock 都支持可重入。

java 复制代码
public class ReentrantDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void methodA() {
        lock.lock();
        try {
            methodB();  // 内部再次获取锁
            System.out.println("methodA");
        } finally {
            lock.unlock();
        }
    }

    public void methodB() {
        lock.lock();
        try {
            System.out.println("methodB");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        new ReentrantDemo().methodA();  // 输出:methodB  methodA
    }
}

2.3 公平锁与非公平锁

  • 非公平锁(默认):线程竞争锁时不考虑等待时间,谁抢到谁执行,吞吐量更高。

  • 公平锁:按照线程请求锁的顺序依次获取锁(FIFO),避免线程"饥饿",但会降低吞吐量。

创建公平锁只需在构造时传入 true

java 复制代码
private final ReentrantLock lock = new ReentrantLock(true);

2.4 限时等待 -- tryLock()

tryLock() 方法允许线程在指定时间内尝试获取锁,如果超时仍未获取到,则返回 false,从而避免无限阻塞。这一特性可用于预防死锁

java 复制代码
// 无参:立即返回结果
boolean locked = lock.tryLock();

// 带超时参数:等待5秒,若未获取到则返回false
boolean locked = lock.tryLock(5, TimeUnit.SECONDS);
下面是一个使用 tryLock 解决死锁的示例(两个线程相互等待对方持有的锁):

java

public class DeadlockResolveDemo {
    private static final ReentrantLock lock1 = new ReentrantLock();
    private static final ReentrantLock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        new Thread(() -> {
            boolean gotLock1 = lock1.tryLock();
            if (gotLock1) {
                try {
                    System.out.println("Thread1 获得 lock1");
                    Thread.sleep(500);
                    boolean gotLock2 = lock2.tryLock();
                    if (gotLock2) {
                        try {
                            System.out.println("Thread1 获得 lock2,任务完成");
                        } finally {
                            lock2.unlock();
                        }
                    } else {
                        System.out.println("Thread1 获取 lock2 失败,释放 lock1");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock1.unlock();
                }
            }
        }).start();

        new Thread(() -> {
            boolean gotLock2 = lock2.tryLock();
            if (gotLock2) {
                try {
                    System.out.println("Thread2 获得 lock2");
                    Thread.sleep(500);
                    boolean gotLock1 = lock1.tryLock();
                    if (gotLock1) {
                        try {
                            System.out.println("Thread2 获得 lock1,任务完成");
                        } finally {
                            lock1.unlock();
                        }
                    } else {
                        System.out.println("Thread2 获取 lock1 失败,释放 lock2");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock2.unlock();
                }
            }
        }).start();
    }
}

运行结果中,两个线程会尝试获取对方的锁,若超时未成功则主动释放已持有的锁,从而避免死锁。

2.5 ReentrantLock 与 synchronized 的区别

特性 synchronized ReentrantLock
锁获取与释放 自动 手动 lock() / unlock()
可重入性 支持 支持
公平锁 不支持 支持(构造参数 true
响应中断 不支持,一直阻塞 支持 lockInterruptibly()tryLock
限时等待 不支持 支持 tryLock(timeout, unit)
条件变量(Condition) 通过 wait/notify 实现 支持多个 Condition 对象
性能(JDK 1.6+) 经过优化,与 ReentrantLock 接近 相差不大

总结 :当需要更灵活的锁控制(如公平锁、超时等待、可中断)时,选择 ReentrantLock;如果只是简单的同步需求,synchronized 更简洁。

三、ReentrantReadWriteLock -- 读写分离的共享锁

在实际业务中,很多场景是读多写少 (如缓存系统),如果使用独占锁,读操作也会互斥,严重影响并发性能。ReentrantReadWriteLock 将锁分为读锁(共享锁)写锁(独占锁),允许多个读线程同时访问,而写线程与其他所有线程互斥。

3.1 读写锁的基本规则

  • 写锁:独占,不允许其他读锁或写锁同时持有。

  • 读锁:共享,可以同时被多个线程持有。

  • 锁的互斥关系

    • 写写互斥

    • 读写互斥 / 写读互斥

    • 读读并发

3.2 示例:缓存读写

首先,不使用任何锁,模拟一个简单的缓存,观察并发读写时的问题:

java 复制代码
class MyCache {
    private Map<String, String> cache = new HashMap<>();

    public void put(String key, String value) {
        System.out.println(Thread.currentThread().getName() + " 开始写入");
        try { Thread.sleep(300); } catch (InterruptedException e) { }
        cache.put(key, value);
        System.out.println(Thread.currentThread().getName() + " 写入成功");
    }

    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + " 开始读出");
        try { Thread.sleep(300); } catch (InterruptedException e) { }
        String value = cache.get(key);
        System.out.println(Thread.currentThread().getName() + " 读出成功:" + value);
    }
}

public class ReadWriteDemo {
    public static void main(String[] args) {
        MyCache cache = new MyCache();
        // 5个写线程
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(() -> cache.put(String.valueOf(finalI), String.valueOf(finalI)), "写" + finalI).start();
        }
        // 5个读线程
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(() -> cache.get(String.valueOf(finalI)), "读" + finalI).start();
        }
    }
}

运行结果中,写操作会被读操作打断,出现数据不一致或脏读的情况。
加入读写锁后,写操作互斥,读操作并发:

java 复制代码
class MyCacheWithLock {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private Map<String, String> cache = new HashMap<>();

    public void put(String key, String value) {
        rwLock.writeLock().lock();  // 写锁
        try {
            System.out.println(Thread.currentThread().getName() + " 开始写入");
            Thread.sleep(300);
            cache.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 写入成功");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public void get(String key) {
        rwLock.readLock().lock();   // 读锁
        try {
            System.out.println(Thread.currentThread().getName() + " 开始读出");
            Thread.sleep(300);
            String value = cache.get(key);
            System.out.println(Thread.currentThread().getName() + " 读出成功:" + value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

此时,多个读线程可以同时执行,写线程之间互斥,保证了数据一致性,同时提高了读并发性能。

3.3 锁降级

锁降级是指在持有写锁的情况下,获取读锁,然后释放写锁的过程。这样做的目的是为了在完成写操作后,依然保持对数据的读取能力,同时允许其他读线程并发访问。

java 复制代码
public class LockDowngradeDemo {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private int data = 0;

    public void updateData(int newData) {
        rwLock.writeLock().lock();
        try {
            data = newData;   // 写操作
            System.out.println("数据更新为:" + newData);
            
            // 锁降级:获取读锁
            rwLock.readLock().lock();
            System.out.println("锁降级为读锁");
        } finally {
            rwLock.writeLock().unlock();  // 释放写锁,仍持有读锁
        }

        // 此时其他读线程可以同时读取
        try {
            System.out.println("当前数据:" + data);
        } finally {
            rwLock.readLock().unlock();   // 最后释放读锁
        }
    }

    public static void main(String[] args) {
        LockDowngradeDemo demo = new LockDowngradeDemo();
        new Thread(() -> demo.updateData(42)).start();
    }
}

注意 :锁降级是合法的,但锁升级(从读锁升级为写锁)是不允许的,因为读锁被多个线程持有,无法安全地升级为写锁,容易造成死锁。

3.4 读写锁的注意事项

  1. 公平性:读写锁也支持公平/非公平策略,公平模式下等待时间最长的线程优先获取锁。

  2. 可重入:读锁可以被同一线程多次获取,写锁也可以被同一线程多次获取,但获取写锁后可以再获取读锁(降级),而获取读锁后不能再获取写锁。

  3. 锁饥饿:在读多写少的场景下,写线程可能长时间获取不到锁("饥饿")。虽然公平策略能缓解,但会牺牲一定吞吐量。

四、总结

锁类型 特点
synchronized 简单、自动释放,但功能单一,不可中断,无法公平
ReentrantLock 手动控制锁,支持公平锁、可中断、限时等待,可重入
ReentrantReadWriteLock 读写分离,读读并发,写写互斥,适用于读多写少场景,支持锁降级

在实际开发中,应根据具体需求选择合适的锁:

  • 简单的同步逻辑,优先使用 synchronized

  • 需要高级功能(公平、超时、可中断)时,选用 ReentrantLock

  • 读多写少的场景,使用 ReentrantReadWriteLock 能显著提升并发性能。

掌握这些锁的特性,能够帮助我们编写出更高效、更可靠的多线程程序。

相关推荐
骇客野人1 分钟前
Java实现B+树,体会B+树做索引的精妙
java·开发语言·b树
朱一头zcy1 分钟前
Linux系列04:简单理解inode、硬链接、软链接、挂载的概念
linux·笔记
楼田莉子5 分钟前
C++数据结构:基数树
开发语言·数据结构·c++·学习
m0_518019486 分钟前
C++中的命令模式实战
开发语言·c++·算法
ProgramHan7 分钟前
十大排行榜——后端语言及要介绍
java·c++·python·php
小江的记录本7 分钟前
【反射】Java反射 全方位知识体系(附 应用场景 + 《八股文常考面试题》)
java·开发语言·前端·后端·python·spring·面试
L1624768 分钟前
Nginx+Keepalived 高可用集群实战笔记
运维·笔记·nginx
callJJ11 分钟前
Ant Design Table 批量操作踩坑总结 —— 从三个 Bug 看前端表格开发的共性问题
java·前端·经验分享·bug·管理系统
_李小白12 分钟前
【OSG学习笔记】Day 3: OSG 实用工具
笔记·图形渲染
没有bug.的程序员15 分钟前
100%采样率引发的全线熔断:Spring Boot 链路追踪的性能绞杀与物理级调优
java·spring boot·后端·生产·熔断·调优·链路追踪