一、简介
ReentrantLock 即可重入锁的意思,是 java 控制线程同步的一种方式。和 synchronized 一样都是独占锁,只允许拿到锁的线程访问临界区。
synchronized 优点是使用简单,不需要用户手动操作,加锁解锁的过程都是隐式的。
ReentrantLock 则需要我们手动加锁和解锁(解锁操作通常放在 finally 代码块中保证执行),虽然使用更复杂但是其灵活性更好,类提供了更多的方法供我们去使用(例如 tryLock、lockInterruptibly 等),适合更复杂的并发场景。
二、方法
1. 构造方法
public ReentrantLock(); // 创建一个非公平锁
public ReentrantLock(boolean fair); // 创建一个公平或非公平锁
默认情况下创建的是非公平锁。这里解释一下什么是公平锁,什么是非公平锁。
公平锁即竞争锁的几个线程,会按照 FIFO(先进先出)的顺序依次拿到锁的资源,对于竞争者是公平的。线程在尝试获取锁之前进行一次CAS运算,当且仅当当前锁处于空闲状态并且排队等候锁的队列里没有其他线程的时候,该线程可以获得锁;否则进入队列进行等待。
非公平锁则线程在尝试获取锁之前进行两次CAS运算,如果发现所空闲,则直接获得锁,如果两次CAS运算都未能获得锁的情况下,该线程才进入等候队列。由于线程的挂起和唤醒都是消耗资源的行为,非公平锁更少的挂起唤醒可以提高性能,但可能造成饥饿问题(即某个线程长时间没有获得锁)。
CAS运算介绍:
CAS 是 compare and swap 的缩写,即比较并交换,是以一种无锁的方式实现并发控制。
其过程为:有三个操作数内存值(V)、预期原值(A)和新值(B)。当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,表示修改成功。否则表示存在其它线程修改过该值了,返回 false,表示失败,并且允许再次尝试。
基于这样的原理,即使不加锁,也可以发现其他线程对当前线程的干扰。由于没有锁竞争的系统开销,也没有线程间调度带来的开销,因此拥有更优越的性能。
2. 常用方法
返回值 | 方法 | 描述 |
---|---|---|
void | lock() | 获取锁,当前线程会一直堵塞直到获取锁为止 |
void | lockInterruptibly() | 同上,但可以响应其它线程的 interrupt 操作 |
boolean | tryLock() | 尝试获取锁,锁空闲则直接获取到锁,并返回true,否则返回false。不会堵塞线程 |
boolean | tryLock(long timeout, TimeUnit unit) | 同上,但会等待指定时间 |
void | unlock() | 释放锁 |
Condition | newCondition() | 返回一个绑定到此 Lock 的 Condition 实例 |
三、生产者消费者模式实例
1. 生产者消费者模式简介:
- 生产者生产货物
- 消费者消费货物
- 生产者当货物充足时停止生产,通知消费者消费货物
- 消费者当货物不足时停止消费,通知生产者生产货物
- 生产者和消费者同时只有一方可以访问存储货物的仓库(同一时间只有一个线程能够访问公共资源)
2. 代码
a. 仓库
存储货物的地方,主要逻辑的实现者.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Storage {
private static final int MAX_COUNT = 10; // 仓库的最大容量
private List<Production> mProductions = new ArrayList<Production>(); // 公共资源,需要互斥的地方
private ReentrantLock mReentrantLock = new ReentrantLock();
private Condition mCondition = mReentrantLock.newCondition();
private int mIndex; // 货物索引
public void produce() {
try {
mReentrantLock.lock(); // 获取锁,再访问公共资源
if (mProductions.size() >= MAX_COUNT) {
System.out.println("produce await");
mCondition.await(); // 货物充足时停止生产
}
Thread.sleep((long) (Math.random() * 1000)); // 生成的耗时
Production production = new Production(mIndex++);
System.out.println("producer produce: " + production.toString());
mProductions.add(production);
mCondition.signal(); // 发个信号告知消费者
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mReentrantLock.unlock(); // 放在finally块中保证一定会释放锁
}
}
public void consume() {
try {
mReentrantLock.lock(); // 获取锁,再访问公共资源
if (mProductions.size() <= 0) {
System.out.println("consume await");
mCondition.await(); // 货物不足时停止消费
}
Thread.sleep((long) (Math.random() * 1000)); // 消费的耗时
Production production = mProductions.remove(0);
System.out.println("consumer consume: " + production.toString());
mCondition.signal(); // 发个信号告知生产者
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mReentrantLock.unlock(); // 放在finally块中保证一定会释放锁
}
}
public static class Production {
public int index;
public Production(int index) {
this.index = index;
}
@Override
public String toString() {
return "Production [index=" + index + "]";
}
}
}
b. 生产者
public class Producer extends Thread {
private Storage mStorage;
public Producer(Storage storage) {
this.mStorage = storage;
}
@Override
public void run() {
while (!Thread.interrupted()) {
mStorage.produce(); // 不停的生产
}
}
}
c. 消费者
public class Consumer extends Thread {
private Storage mStorage;
public Consumer(Storage mStorage) {
this.mStorage = mStorage;
}
@Override
public void run() {
while (!Thread.interrupted()) {
mStorage.consume(); // 不停的消费
}
}
}
d. 场景模拟
public class Main {
public static void main(String[] args) {
Storage storage = new Storage(); // 创建一个仓库
Producer producer = new Producer(storage); // 创建生产者线程
Consumer consumer = new Consumer(storage); // 创建消费者线程
producer.start();
consumer.start();
}
}
3. 运行结果
4. 其它
假设你以 ReentrantLock mReentrantLock = new ReentrantLock(true)
的方式创建了一个公平锁,这可以观察到生产者和消费者两个线程依次执行的场景。