(一)概念
1.1 可重入锁
可重入锁又叫递归锁,同一个线程可以重复获取同一把锁,不会因为之前的锁没有释放而阻塞。
1.2 公平锁和非公平锁
假设一共有3个线程T0,T1,T2;此时T0持有资源的锁,那么公平与非公平的定义如下
- 公平锁:按照线程的等待顺序获取锁,T1和T2线程到达后会进行等待。
- 非公平锁:T1或T2线程到达后会先尝试获取锁,如果可以拿到则直接用。
1.3 独占锁
像synchronized、ReentranLock都是独占锁,同一个时刻只允许一个线程获取锁。
1.4 ReentrantLock
它是一种可重入的独占锁,默认非公平锁,功能类似于synchronized,相对于synchronized它具备如下特点:
- 可以手动释放锁
- 可以设置超时时间
- 可以设置为公平锁
(二)常用API
ReentrantLock实现了Lock接口规范,常见API如下:
void lock() | [阻塞] 获取锁,调用该方法当前线程会获取锁 |
void lockInterruptibly() throws InterruptedException | [阻塞] 可中断的获取锁,和lock()方法不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程 |
boolean tryLock() | [非阻塞] 尝试非阻塞的获取锁,调用该方法后立即返回。如果能够获取到返回true,否则返回false |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 超时获取锁,当前线程在以下三种情况下会被返回false: 当前线程在超时时间内获取了锁 当前线程在超时时间内被中断 超时时间结束 |
void unlock() | 释放锁 |
Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将释放锁 |
2.1 基本使用
在使用时要注意 4 个问题:
- 默认情况下 ReentrantLock 为非公平锁而非公平锁;
- 加锁次数和释放锁次数一定要保持一致,否则会导致线程阻塞或程序异常;
- 加锁操作一定要放在 try 代码之前,这样可以避免未加锁成功又释放锁的异常;
- 释放锁一定要放在 finally 中,否则会导致线程阻塞。
//加锁 阻塞
lock.lock();
try {
...
} finally {
// 解锁
lock.unlock();
}
//尝试加锁 非阻塞
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}
2.2 基本案例
余票有8张,现在有10个用户去抢票,未对资源加锁的情况
public class Test {
//余票总数
static int count = 8;
public static void main(String[] args) {
//开启10个线程去抢票
for (int i = 0; i < 10; i++) {
new Thread(() ->{
if (count > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count--;
System.out.println(Thread.currentThread().getName() + "抢到了第" + count +"张票");
} else {
System.out.println(Thread.currentThread().getName() + "没有抢到票");
}
},i + "号用户").start();
}
}
}
因为没有对资源进行加锁处理,本应有两个用户没有抢到票,现在出现了超卖的情况
2.2.1 加锁处理
public class Test {
//余票总数
static int count = 8;
public static void main(String[] args) {
//创建锁对象
ReentrantLock lock = new ReentrantLock();
//开启10个线程去抢票
for (int i = 0; i < 10; i++) {
new Thread(() ->{
lock.lock();
try {
if (count > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count--;
System.out.println(Thread.currentThread().getName() + "抢到了第" + count +"张票");
} else {
System.out.println(Thread.currentThread().getName() + "没有抢到票");
}
}finally {
lock.unlock();
}
},i + "号用户").start();
}
}
}
2.3 tryLock使用场景
2.3.1 尝试获取锁
尝试非阻塞的获取锁,调用该方法后立即返回。如果能够获取到返回true,否则返回false
案例:执行装车任务,一个车道同时只允许一个车辆执行,其他车辆执行要打印错误报告。
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new CarNo(),"一号线程");
Thread thread2 = new Thread(new CarNo(),"二号线程");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
static class CarNo implements Runnable{
static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
String name = Thread.currentThread().getName();
if (lock.tryLock()) {
try{
System.out.println(name + "开始执行装车任务...");
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
System.out.println(name + "装车任务执行完成!");
}finally {
lock.unlock();
}
} else {
System.out.println(name + "进入装车站失败,当前存正在执行的任务");
}
}
}
}
2.3.2 超时获取锁
超时获取锁,当前线程在以下三种情况下会被返回false:
- 当前线程在超时时间内获取了锁
- 当前线程在超时时间内被中断
- 超时时间结束
案例:执行装车任务,一个车道同时只允许一个车辆执行,其他车辆超时未执行要打印错误报告。
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new CarNo(),"一号线程");
Thread thread2 = new Thread(new CarNo(),"二号线程");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
static class CarNo implements Runnable{
static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
String name = Thread.currentThread().getName();
try {
if (lock.tryLock(1100, TimeUnit.MILLISECONDS)) {
try{
System.out.println(name + "开始执行装车任务...");
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
System.out.println(name + "装车任务执行完成!");
}finally {
lock.unlock();
}
} else {
System.out.println(name + "进入装车站失败,当前存正在执行的任务");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
2.3.3 等待锁时允许中断
当某个线程在等待锁时,如果不想一直等待我们可以给它设置超时时间,也可以通过外部来中断等待。
案例:公司有两名员工,一个是甩锅员工,霸占着电脑不用;另外一名是背锅员工,想干活没有电脑干;老板看着背锅员工一直没有活干就把他开除了。
public class Test {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread thread0 = new Thread(() -> {
lock.lock();
try {
while (true) {
}
} finally {
lock.unlock();
}
},"甩锅员工");
Thread thread1 = new Thread(() -> {
try {
//此时锁一直被thread0锁持有
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被开除了");
}finally {
lock.unlock();
}
},"背锅员工");
Thread thread2 = new Thread(thread1::interrupt,"老板");
thread0.start();
thread1.start();
Thread.sleep(1000);
thread2.start();
thread1.join();
thread2.join();
}
}
2.3.4 等待与通知
与Object的wait和notify类似,只不过Object的notify无法通知到具体的某个线程及某个任务;而Condition可以作为多任务条件,比如ReentrantLock创建了两个执行条件A和B,你可以控制到具体的条件是否执行。
注意:必须先获得锁才能执行该API
案例:学生填写请假单等待老师审批,只有老师审批过后学生才能离校。
public class Test {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
//请假审批执行条件
Condition A = lock.newCondition();
Thread thread1 = new Thread(() -> {
lock.lock();
try {
String name = Thread.currentThread().getName();
System.out.println(name + "提交请假单,等待班主任审批...");
A.await();
System.out.println(name + "班主任审批完成,允许离校。");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
},"学生");
Thread thread2 = new Thread(() -> {
lock.lock();
try {
A.signal();
} finally {
lock.unlock();
}
},"班主任");
thread1.start();
Thread.sleep(1500);
thread2.start();
thread1.join();
thread2.join();
}
}