常用并发同步工具类的真实应用场景
首先我们来思考一个问题:并发编程是为了解决什么问题的?可以帮我们提升系统性能并且解决线程安全问题。
jdk提供了比synchronized更加高级的各种同步工具,包括ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等,可以实现更加丰富的多线程操作。
Java并发知识体系| ProcessOn免费在线作图,在线流程图,在线思维导图
1. ReentrantLock
ReentrantLock是一种可重入的独占锁,它允许同一个线程多次获取同一个锁而不会被阻塞。它的功能类似于synchronized是一种互斥锁,可以保证线程安全。相对于 synchronized, ReentrantLock具备如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 与 synchronized 一样,都支持可重入
它的主要应用场景是在多线程环境下对共享资源进行独占式访问,以保证数据的一致性和安全性。
1.1. 常用API
Lock接口
ReentrantLock实现了Lock接口规范,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()方法,而调用后,当前线程将释放锁 |
基本语法
//加锁 阻塞
lock.lock();
try {
...
} finally {
// 解锁
lock.unlock();
}
//尝试加锁 非阻塞
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}
在使用时要注意 4 个问题:
- 默认情况下 ReentrantLock 为非公平锁而非公平锁;
- 加锁操作一定要放在 try 代码之前,这样可以避免未加锁成功又释放锁的异常,释放锁一定要放在 finally 中,否则可能会导致线程阻塞;
- ReentrantLock 和 synchronized 不一样,需要手动释放锁,所以使用 ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样;
- 加锁次数和释放锁次数一定要保持一致,否则会导致线程阻塞或程序异常(可能会造成某一线程由于没有得到解锁指示而一直在等待解锁);
1.2. ReentrantLock使用
1.2.1. 独占锁:模拟抢票场景
8张票,10个人抢,如果不加锁,会出现什么问题?
/**
* 模拟抢票场景
*/
public class ReentrantLockDemo {
/**
* 默认是非公平锁
*/
private final static ReentrantLock lock = new ReentrantLock();
/**
* 总票数
*/
private static int tickets = 8;
public void buyTicket(){
lock.lock(); //获取锁 如果不加锁,会出现票超卖的情况
try {
if(tickets > 0){ //还有票 读操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "购买了第" + tickets-- + "张票"); //写操作
// buyTicket(); //用于演示可重入锁
}else{
System.out.println("票已经卖完了," + Thread.currentThread().getName() + "抢票失败");
}
}finally{
lock.unlock(); //释放锁
}
}
public static void main(String[] args) {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
//开启10个线程抢票
for (int i = 1; i <= 10; i++){
new Thread(() -> {
//抢票
reentrantLockDemo.buyTicket();
},"线程"+i).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("剩余票数:" + tickets);
}
}
不加锁效果:出现超卖问题
加锁效果:结果正常,两个人抢票失败
1.2.2. 公平锁和非公平锁
ReentrantLock支持公平锁和非公平锁两种模式:
- 公平锁:线程在获取锁时,按照等待的先后顺序获取锁。
- 非公平锁:线程在获取锁时,不按照等待的先后顺序获取锁,而是随机获取锁。ReentrantLock默认是非公平锁
ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
ReentrantLock lock = new ReentrantLock(true); //公平锁
比如买票的时候就有可能出现插队的场景,允许插队就是非公平锁,如下图:
1.2.3. 可重入锁
概述
可重入锁又名递归锁,是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。在实际开发中,可重入锁常常应用于递归操作、调用同一个类中的其他方法、锁嵌套等场景中。
解释
可:可以
重:再次
入:进入
锁:同步锁进入什么:进入同步域(即同步代码块/方法或显式锁锁定的代码)
一句话:可重入锁就是支持可重入,在没有释放锁时,还可以再次进入,因为持有锁的对象没有变,一直是一个对象持有这把锁。
可重入锁如下
1. synchronized示例
/**
* @Description:使用synchronized
* @Auther: Carl
*/
public class WhatReentrant1 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (this) {
System.out.println("第1次获取锁,这个锁是:" + this);
int index = 1;
while (true) {
synchronized (this) {
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
}
if (index == 10) {
break;
}
}
}
}
}).start();
}
}
2. ReentrantLock示例
/**
* @Description: 使用ReentrantLock
* @Auther: Carl
*/
public class WhatReentrant2 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println("第1次获取锁,这个锁是:" + lock);
int index = 1;
while (true) {
try {
lock.lock();
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
try {
Thread.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (index == 10) {
break;
}
} finally {
lock.unlock();
}
}
} finally {
lock.unlock();
}
}
}).start();
}
}