一、案例需求
自定义数组集合,开启两个线程同时执行添加元素操作:
- 线程 1:添加字符串
"F" - 线程 2:添加字符串
"e"
不做同步:会出现数据覆盖、索引错乱、线程安全问题 使用 ReentrantLock 显式锁 保证多线程原子操作,保证数据安全。
二、知识点铺垫
1. Lock 锁简介
Lock是java.util.concurrent.locks包下的显式锁接口- 常用实现类:
ReentrantLock(可重入锁) - 区别于
synchronized(隐式锁自动上锁解锁) - 手动上锁、手动解锁,控制粒度更精细、更灵活
2. 核心方法
lock():手动获取锁,锁被占用则阻塞等待unlock():手动释放锁(必须放在 finally 中,保证一定释放)
三、完整可运行代码
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 锁
- 保证整个添加操作是完整原子操作,不会被打断
五、运行结果
数据中实际存储元素的个数:4
A B F e
结果说明
- 两个线程依次执行,没有数据覆盖
- 索引正常递增,数据完整有序
- 线程同步成功,解决并发安全问题
六、如果不加锁会出现什么问题?
- 两个线程同时赋值同一个下标
- 数据覆盖、丢失数据
- index 错乱,元素个数统计错误
- 多线程并发安全问题
七、Lock 锁 VS synchronized 面试总结
synchronized:隐式锁,自动上锁、自动解锁,语法简单Lock(ReentrantLock):显式锁,手动上锁、手动解锁- Lock 灵活性更高、可重试、可超时、可公平锁
- Lock 必须在 finally 解锁,防止死锁
八、核心考点背诵
- ReentrantLock 属于 java.util.concurrent.locks 包
- Lock 是显式锁,需要手动 lock ()、unlock ()
- unlock () 必须放在 finally 中,保证锁释放
- sleep 不释放锁、保证原子操作完整性
- 多线程必须使用同一个锁对象才能实现同步