Java并发编程02:可重入锁ReentrantLock
可重入锁ReentrantLock
ReentrantLock
的使用
ReentrantLock
可以完全替代synchronized
,提供了一种更灵活的锁.
ReenTrantLock
必须手动释放锁,为防止发生异常,必须将同步代码用try
包裹起来,在finally
代码块中释放锁.
public class T {
ReentrantLock lock = new ReentrantLock();
// 使用ReentrantLock的写法
private void m1() {
// 尝试获得锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName());
} finally {
lock.unlock();
}
}
// 使用synchronized的写法
private synchronized void m2() {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m1, "t1").start();
new Thread(t::m2, "t2").start();
}
}
ReentrantLock
获取锁的方法
尝试锁tryLock()
使用tryLock()
方法可以尝试获得锁,返回一个boolean
值,指示是否获得锁.
可以给tryLock
方法传入阻塞时长,当超出阻塞时长时,线程退出阻塞状态转而执行其他操作.
public class T {
ReentrantLock lock = new ReentrantLock();
void m() {
boolean isLocked = false; // 记录是否得到锁
// 改变下面两个量的大小关系,观察输出
int synTime = 4; // 同步操作耗时
int waitTime = 2; // 获取锁的等待时间
try {
isLocked = lock.tryLock(waitTime, TimeUnit.SECONDS); // 线程在这里阻塞waitTime秒,尝试获取锁
if (isLocked) {
// 若waitTime秒内得到锁,则执行同步操作
for (int i = 1; i <= synTime; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "持有锁,执行同步操作");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 使用tryLock()方法,尝试解除标记时,一定要先判断当前线程是否持有锁
if (isLocked) {
lock.unlock();
}
}
// 执行非同步操作
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "没持有锁,执行非同步操作");
}
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m, "线程1").start();
new Thread(t::m, "线程2").start();
}
}
若我们设置同步操作耗时4秒,获取锁的等待时间为2秒,则程序执行结果如下. 我们发现线程2
在阻塞时间内没能抢到锁,直接执行非阻塞方法:
线程1持有锁,执行同步操作
线程1持有锁,执行同步操作
线程2没持有锁,执行非同步操作
线程1持有锁,执行同步操作
线程2没持有锁,执行非同步操作
线程1持有锁,执行同步操作
线程2没持有锁,执行非同步操作
线程1没持有锁,执行非同步操作
线程2没持有锁,执行非同步操作
线程1没持有锁,执行非同步操作
...
若我们设置同步操作耗时4秒,获取锁的等待时间为5秒,则程序执行结果如下. 我们发现线程2
在阻塞时间内成功抢到锁,先执行完同步方法才执行非同步方法:
线程1持有锁,执行同步操作
线程1持有锁,执行同步操作
线程1持有锁,执行同步操作
线程1持有锁,执行同步操作
线程2持有锁,执行同步操作
线程1没持有锁,执行非同步操作
线程2持有锁,执行同步操作
线程1没持有锁,执行非同步操作
线程2持有锁,执行同步操作
线程1没持有锁,执行非同步操作
线程2持有锁,执行同步操作
线程1没持有锁,执行非同步操作
线程2没持有锁,执行非同步操作
线程1没持有锁,执行非同步操作
线程2没持有锁,执行非同步操作
....
可中断锁lockInterruptibly()
使用lockInterruptibly()
以一种可被中断的方式获取锁.获取不到锁时线程进入阻塞状态,但这种阻塞状态可以被中断.调用被阻塞线程的interrupt()
方法可以中断该线程的阻塞状态,并抛出InterruptedException
异常.
interrupt()
方法只能中断线程的阻塞状态
.若某线程已经得到锁或根本没去尝试获得锁,则该线程当前没有处于阻塞状态
,因此不能被interrupt()
方法中断.
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
// 线程1一直占用着lock锁
new Thread(() -> {
lock.lock();
try {
System.out.println("线程1启动");
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE); // 线程一直占用锁
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "线程1").start();
// 线程2抢不到lock锁,若不被中断则一直被阻塞
Thread t2 = new Thread(() -> {
try {
lock.lockInterruptibly(); // 尝试获取锁,若获取不到锁则一直阻塞
System.out.println("线程2启动");
} catch (InterruptedException e) {
System.out.println("线程2阻塞过程中被中断");
} finally {
if (lock.isLocked()) {
try {
lock.unlock(); // 没有锁定进行unlock就会抛出IllegalMonitorStateException异常
} catch (Exception e) {
}
}
}
}, "线程2");
t2.start();
// 4秒后中断线程2
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.interrupt();
}
程序输出如下:
线程1启动
线程2阻塞过程中被中断
并不是所有处于阻塞状态
的线程都可以被interrupt()
方法中断,要看该线程处于具体的哪种阻塞状态
.阻塞状态
包括普通阻塞
,等待队列
,锁池队列
.
普通阻塞
: 调用sleep()
方法的线程处于普通阻塞
,调用其interrupt()
方法可以中断其阻塞状态并抛出InterruptedException
异常等待队列
: 调用锁的wait()
方法将持有当前锁的线程转入等待队列
,这种阻塞状态只能由锁对象的notify()
方法唤醒,而不能被线程的interrupt()
方法中断.锁池队列
: 尝试获取锁但没能成功抢到锁的线程会进入锁池队列
:- 争抢
synchronized
锁的线程的阻塞状态不能被中断. - 使用
ReentrantLock
的lock()
方法争抢锁的线程的阻塞状态不能被中断. - 使用
ReentrantLock
的tryLock()
和lockInterruptibly()
方法争抢锁的线程的阻塞状态可以被中断.
- 争抢
关于
interrupted()
方法的使用,可以查看这篇文章Java中interrupt的使用,总结来说,就是interrupt()
方法不能打断线程,但是会给该线程发送一个interrupt
信号,让该线程自己决定如何处理该信号,但有一种特殊情况:若该线程正处于阻塞状态
,调用其interrupt()
方法会抛出InterruptedException
.
公平锁
在初始化ReentrantLock
时给其fair
参数传入true
,可以指定该锁为公平锁
.
CPU默认的进程调度是不公平的
,也就是说,CPU不能保证等待时间较长的线程先被执行.但公平锁
可以保证等待时间较长的线程先被执行.
public class T implements Runnable {
private static ReentrantLock lock = new ReentrantLock(true);// 指定锁为公平锁
@Override
public void run() {
for (int i = 0; i < 100; i++) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "持有锁");
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
T t = new T();
new Thread(t, "线程1").start();
new Thread(t, "线程2").start();
}
}
程序输出如下,发现两个线程严格交替执行
线程1持有锁
线程2持有锁
线程1持有锁
线程2持有锁
线程1持有锁
线程2持有锁
...
等待/通知(await/signal)机制
await()
和signal()
方法
与synchronized
关键字类似,ReentrantLock
锁也支持等待/通知机制.与synchronized
不同的是,不是将线程阻塞在锁上,而是将其阻塞在条件Condition
对象上,要通过Condition
对象调用这些方法.
public void await()
: 将当前线程阻塞在调用该方法的Condition
对象上public void signal()
: 唤醒一个阻塞在调用该方法的Condition
对象上的线程public void signalAll()
: 唤醒所有阻塞在调用该方法的Condition
对象上的线程
理解Condition
对象
要深入理解Condition
对象,可以阅读这篇文章:使用Condition进行线程间通信(若微信打不开链接,可以点击阅读原文点开该链接),对该文章总结如下:
Condition
对象将Object
的监视器方法(wait()
,notify()
和notifyAll()
)分解成截然不同的条件对象,使等待/通知机制支持多路等待.
多个Condition
对象被绑定到一个ReentrantLock
对象上,一个锁上可以绑定多个Condition
对象,用来控制多个执行路线的等待通知,可以通过锁对象的newCondition()
方法得到一个绑定到当前对象上的Condition
对象.
要注意的是,
Condition
对象是绑定到锁对象上的(可以理解为一种细粒度更高的锁),而不是绑定在线程上的.因此分析其wait()
,notify()
和notifyAll()
还是要针对锁来进行分析,而不是直接分析Condition
对应哪个线程.
使用Condition
对象实现 生产者/消费者模式
使用synchronized
的wait()/notify()
可以实现生产者/消费者模式,具体见我的上一篇文章Java并发编程01:Java多线程基础(synchronized,volatile,wait/notify)
下面我们使用Condition
对象实现 生产者/消费者模式:
// 同步栈,用于在生产者线程和消费者线程之间通信
class SyncStack {
private Lock lock = new ReentrantLock(); // 锁对象
// 绑定在锁上的一个条件,阻塞在该条件上的线程为生产者线程
private Condition producerCondition = lock.newCondition();
// 绑定在锁上的一个条件,阻塞在该条件上的线程为消费者线程
private Condition consumerCondition = lock.newCondition();
int MAX_SIZE = 5; // 同步队列的最大容量
Queue<Product> products = new LinkedList<>(); // 同步队列
// 向栈中送入产品
public void push(Product product) {
lock.lock();
try {
while (products.size() == MAX_SIZE) {
try {
// 当前线程应该是生产者线程,因此将当前线程阻塞在producerCondition上
producerCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
products.add(product);
// 唤醒所有阻塞在consumerCondition的线程,这些被唤醒的线程都应该是消费者线程
consumerCondition.signalAll();
} finally {
lock.unlock();
}
}
// 从栈中取出产品
public Product pop() {
lock.lock();
try {
while (products.size() == 0) {
try {
// 当前线程应该是消费者线程,因此将当前线程阻塞在consumerCondition上
consumerCondition.await();
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Product product = products.peek();
products.remove();
// 唤醒所有阻塞在producerCondition的线程,这些被唤醒的线程都应该是生产者线程
producerCondition.signalAll();
return product;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
SyncStack syncStack = new SyncStack();
// 启动2个生产者线程
for (int i = 0; i < 2; i++) {
new Thread(() -> {
// 生产10个产品
for (int j = 0; j < 10; j++) {
Product product = new Product(j);
syncStack.push(product);
System.out.println(Thread.currentThread().getName() + "produce" + product);
try {
Thread.sleep((int) (Math.random() * 100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者线程" + i).start();
}
// 启动2个消费者线程
for (int i = 0; i < 2; i++) {
new Thread(() -> {
// 消费10个产品
for (int j = 0; j < 10; j++) {
Product product = syncStack.pop();
System.out.println(Thread.currentThread().getName() + "consume" + product);
try {
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者线程" + i).start();
}
}
}