欢迎光临
我们一直在努力

Java Lock 显式锁案例|ReentrantLock 解决多线程安全问题(超详细实战)

一、案例需求

自定义数组集合,开启两个线程同时执行添加元素操作:

  • 线程 1:添加字符串 "F"
  • 线程 2:添加字符串 "e"

不做同步:会出现数据覆盖、索引错乱、线程安全问题。 使用 ReentrantLock 显式锁保证多线程原子操作,保证数据安全。

拓展:除了锁互斥,多线程还经常需要线程间通信,依靠 wait()、notify() 完成线程等待与唤醒,下面知识点会详细讲解。

二、知识点铺垫

1. Lock 锁简介

Lock 是 java.util.concurrent.locks 包下的显式锁接口 常用实现类:ReentrantLock(可重入锁) 区别于 synchronized(隐式锁自动上锁解锁) 手动上锁、手动解锁,控制粒度更精细、更灵活

2. 核心方法

  • lock():手动获取锁,锁被占用则阻塞等待
  • unlock():手动释放锁(必须放在 finally 中,保证一定释放)

3. wait () /notify () 线程通信【重点新增讲解】

💡 注意:wait()、notify()、notifyAll() 是Object 类的方法,不是 Lock 的方法! 这三个方法只能写在 synchronized 同步代码块 / 同步方法里面调用,不能直接配合 Lock 锁使用。 如果使用 ReentrantLock 想要实现等待唤醒,需要搭配Condition。

方法介绍
  • wait()
    • 作用:让当前持有锁的线程释放锁,进入等待队列,进入阻塞状态。
    • 线程会一直等待,直到其他线程调用notify()/notifyAll()唤醒它。
    • 释放锁这点和Thread.sleep()完全不同!sleep 休眠不释放锁,wait 会释放锁。
  • notify()
    • 作用:唤醒在该对象监视器上随机一个正在 wait 等待的线程。
    • 调用 notify 不会立刻释放锁,需要当前同步代码块执行结束后,才会释放锁,被唤醒线程才可以竞争锁。
  • notifyAll()
    • 唤醒该对象上所有处于 wait 等待的线程,所有被唤醒线程再去争抢锁。
  • wait 和 sleep 的对比(面试高频)

    表格

    wait()sleep()
    所属类 Object 类 Thread 类
    是否释放锁 ✅ 释放锁 ❌ 不释放锁
    使用位置 必须在 synchronized 同步代码块中 任意位置都可以写
    结束条件 被 notify/notifyAll 唤醒,或 wait (long) 超时 休眠时间到自动恢复

    补充:ReentrantLock 实现等待唤醒: Lock 锁不能直接调用 wait/notify,需要调用lock.newCondition()得到 Condition 对象,使用condition.await()代替 wait,condition.signal()代替 notify。

    三、完整可运行代码

    package Test_07;

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;

    /**
    * 本案例演示:ReentrantLock 显式锁解决多线程数据安全问题
    */
    public class Test_03 {
    public static void main(String[] args) throws InterruptedException {
    // 共享资源对象(临界资源)
    MyList3 list = new MyList3();

    // 创建两个线程
    ThreadF t1 = new ThreadF(list);
    ThreadE t2 = new ThreadE(list);

    // 启动线程
    t1.start();
    t2.start();

    // 主线程等待两个子线程执行完毕
    t1.join();
    t2.join();

    // 最终遍历输出集合数据
    list.printList();
    }
    }

    // 线程1:添加元素 F
    class ThreadF extends Thread {
    private MyList3 list;

    public ThreadF(MyList3 list) {
    this.list = list;
    }

    @Override
    public void run() {
    list.add("F");
    }
    }

    // 线程2:添加元素 e
    class ThreadE extends Thread {
    private MyList3 list;

    public ThreadE(MyList3 list) {
    this.list = list;
    }

    @Override
    public void run() {
    list.add("e");
    }
    }

    // 自定义数组集合类(存在临界资源)
    class MyList3 {
    // 底层数组容器
    private String[] str = {"A", "B", "", ""};
    // 元素下标计数器(临界资源,多线程共享)
    private int index = 2;

    // 创建显式锁对象(唯一锁对象,保证线程互斥)
    private Lock lock = new ReentrantLock();

    /**
    * 加锁的添加方法
    */
    public void add(String s) {
    // 1.手动上锁
    lock.lock();
    try {
    // 原子操作:赋值 + 休眠模拟业务耗时 + 索引自增
    str[index] = s;
    Thread.sleep(500);
    index++;
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    // 2.手动释放锁(finally 保证锁一定释放,不死锁)
    lock.unlock();
    }
    }

    /**
    * 遍历打印集合所有元素
    */
    public void printList() {
    System.out.println("数据中实际存储元素的个数:" + index);
    for (int i = 0; i < str.length; i++) {
    System.out.print(str[i] + " ");
    }
    System.out.println();
    }
    }

    四、代码核心解析

    1. 临界资源

    • String[] str 数组容器
    • int index 共享索引计数器

    两个线程同时操作这两个变量,属于多线程临界资源。

    2. 加锁原理

  • 两个线程争抢 同一个 ReentrantLock 锁对象
  • 抢到锁的线程进入 try 代码块执行完整业务
  • 执行完毕进入 finally 强制释放锁
  • 另一个线程才能进入执行
  • 3. sleep 不释放锁

    Thread.sleep(500) 休眠只会释放 CPU 资源 不会释放 Lock 锁 保证整个添加操作是完整原子操作,不会被打断

    🔔 对比记忆:如果这里换成wait(),线程会直接把锁释放,其他线程就可以进入 add 方法,会破坏当前原子添加逻辑,所以本案例这里使用 sleep 而不是 wait。

    五、运行结果

    数据中实际存储元素的个数:4
    A B F e

    结果说明

    • 两个线程依次执行,没有数据覆盖
    • 索引正常递增,数据完整有序
    • 线程同步成功,解决并发安全问题

    六、如果不加锁会出现什么问题?

    • 两个线程同时赋值同一个下标
    • 数据覆盖、丢失数据
    • index 错乱,元素个数统计错误
    • 多线程并发安全问题

    七、Lock 锁 VS synchronized 面试总结

    • synchronized:隐式锁,自动上锁、自动解锁,语法简单;支持 wait/notify 线程通信
    • Lock (ReentrantLock):显式锁,手动上锁、手动解锁;灵活性更高、可重试、可超时、可公平锁;线程通信依靠 Condition 的 await/signal
    • Lock 必须在 finally 解锁,防止死锁

    八、核心考点背诵

  • ReentrantLock 属于 java.util.concurrent.locks 包
  • Lock 是显式锁,需要手动 lock()、unlock()
  • unlock() 必须放在 finally 中,保证锁释放
  • sleep 不释放锁;wait() 会释放锁,wait/notify 属于 Object 方法,配合 synchronized
  • 多线程必须使用同一个锁对象才能实现同步
  • notify 随机唤醒一个等待线程,notifyAll 唤醒全部等待线程

  • 拓展示例:wait + notify 简易代码(synchronized 版本)

    因为 wait/notify 不能直接用于 Lock 锁,这里单独写一段简易生产者消费者示例,方便你理解 wait 和 notify

    public class WaitNotifyDemo {
    public static void main(String[] args) {
    Object lockObj = new Object();
    //等待线程
    new Thread(()->{
    synchronized (lockObj){
    try {
    System.out.println("等待线程:进入等待,释放锁");
    lockObj.wait();
    System.out.println("等待线程:被唤醒,继续执行");
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }).start();

    //唤醒线程
    new Thread(()->{
    synchronized (lockObj){
    System.out.println("唤醒线程:准备唤醒其他线程");
    lockObj.notify();
    System.out.println("唤醒线程:notify调用完毕,代码块结束后释放锁");
    }
    }).start();
    }
    }

    运行流程:

  • 等待线程拿到锁,执行wait(),释放锁,进入等待
  • 唤醒线程获取锁,执行 notify,通知锁对象上等待的线程
  • 唤醒线程执行完同步块,释放锁
  • 等待线程重新竞争锁,拿到锁之后继续向下执行
  • 赞(0)
    未经允许不得转载:171主机测评 » Java Lock 显式锁案例|ReentrantLock 解决多线程安全问题(超详细实战)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址