Java并发——Lock锁

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

一、为什么需要 Lock?

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

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

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

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

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

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

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

ReentrantLock 是 Lock 接口最常用的实现类,它是一个可重入的互斥锁 ,具备与 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 可重入性

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

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 能显著提升并发性能。

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

相关推荐
vx-程序开发6 小时前
【计算机毕设】51300童装超市管理系统的设计与实现
java·vue.js·spring boot·源码·课程设计·毕设·大作业
青柠之夏cc6 小时前
前端拖拽功能原生实现,不引入拖拽库完成业务
开发语言·前端·python
ym hyd 1117 小时前
题库建立与考试系统源码 Java+SpringBoot+Vue3 前后分离
java·开发语言·vue.js·spring boot·毕设
risc1234567 小时前
什么是码点?
java
OKkankan7 小时前
Python 高阶语法(一):高阶函数、闭包、lambda 与 functools——从底层真正理解装饰器
开发语言·python
geovindu7 小时前
rust: Facade Pattern
开发语言·后端·设计模式·rust·外观模式
福大大架构师每日一题7 小时前
Rust 1.99.0发布:C 可变参数、裸函数、Cargo 配置、Rustdoc 性能与大量兼容性调整全解析
c语言·开发语言·rust
JavaPub-rodert7 小时前
公共 Docker 镜像源总失效?用 Harbor Proxy Cache 自建 Docker Hub 镜像缓存
java·缓存·docker
智鸟科技GemeOpen开发者智能设备7 小时前
平安校园 AI 智能实时告警系统 智鸟科技·GemeOpen + 谷华科技·融合创新方案白皮书
java·开发语言·python·物联网·智能家居
Space655367 小时前
常见组学技术笔记:从 DNA 基因组学到 Multi-omics
笔记