前言
在并发编程学习(8) —— 管程的文章中介绍了管程,管程中最需要解决的问题是互斥和同步,而Lock就负责解决互斥问题,Condition就负责解决同步问题。
但是,之前提到的synchronized也是一种管程的实现方式,那既然有了synchronized为什么还要再造Lock和Condition?在之前的并发编程学习(5) —— 如何解决死锁中提到过死锁这个问题,死锁的解决方法有三种:
- 破坏不可占用条件
- 破坏循环等待
- 一次性申请所需的所有资源
其中第一点破坏不可占用条件是synchronized不能够实现的,因为synchronized在申请资源的时候若申请不到,则线程会直接进入阻塞状态。而我们的目的是,当某个线程申请资源后,进一步申请资源,若申请不到,就会释放它现有的资源。
一共有三个方法来实现这个目的:
- 支持超时。如果这个已占有资源的线程在一定时间内没有获取到它所需的另一部分资源,则自动释放现有资源。
- 响应中断。当线程获取锁A后,如果获取锁B失败的话,线程就会进入阻塞状态,倘若能够给该线程一个中断信号,释放持有锁A,这样就能破坏不可占用条件。
- 非阻塞地获取锁。倘若线程获取锁A失败,不是进入阻塞状态,而是直接返回,释放持有资源。
因为synchronized不能满足上述3个方法,因此就产生了Lock,下面我们来看看如何通过Lock保证可见性,先看一下测试代码:
public class LockConditionTest {
public int testValue;
public void addValue() {
for (int i = 0; i < 100000; i++) {
testValue += 1;
}
}
public int add() throws InterruptedException {
Thread t1 = new Thread(() -> addValue());
Thread t2 = new Thread(() -> addValue());
t1.start();
t2.start();
t1.join();
t2.join();
return testValue;
}
public static void main(String[] args) throws InterruptedException {
LockConditionTest lockConditionTest = new LockConditionTest();
System.out.println(lockConditionTest.add());
}
}
结果相信大家都不是200000,因为存在可见性问题,之前可以用sychronized来解决,但现在我们要用Lock来看看如何实现:
private final Lock lk1 = new ReentrantLock();
public int add() throws InterruptedException {
Thread t1 = new Thread(() ->
{
lk1.lock();
System.out.println("线程t1获取到锁,执行");
addValue();
System.out.println("线程t1释放锁");
lk1.unlock();
});
Thread t2 = new Thread(() -> {
lk1.lock();
System.out.println("线程t2获取到锁,执行");
addValue();
System.out.println("线程t2释放锁");
lk1.unlock();
});
t1.start();
t2.start();
t1.join();
t2.join();
return testValue;
}
可以看到结果是200000,看来Lock的用法也不是特别难,我们进去看看Lock的接口。里面一共有六个方法:
// 后面会讲
Condition newCondition();
// 这个很好理解,就是释放锁
void unlock();
// 该方法在超时时间内没获取锁的话就会自动中断获取锁的过程,并且返回false。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 加锁成功就返回true,失败就false。
boolean tryLock();
// 中断等待获取锁的线程,例如A,B两线程抢锁,A抢到后,B就在等待A释放锁,如果这时候调用Thread.interrupt(),B就会放弃锁的获取。
void lockInterruptibly() throws InterruptedException;
// 当锁没有线程占用,则获取,并且获取后如果不调用unlock()则锁一直不会释放。
void lock();
通过以上方法,我们可以根据实际情况来解决遇到的死锁。
ReentrantLock
看回上述代码,我们可以看到这一段代码:
private final Lock lk1 = new ReentrantLock();
锁lk1对象是通过ReentrantLock来实例化,那ReentrantLock是什么呢?其实就是重入锁。
那什么是重入锁?其实就是线程可以获取同一把锁,如果不明白的话可以看如下代码:
public void addValue() {
for (int i = 0; i < 100000; i++) {
lk1.lock();
testValue += 1;
lk1.unlock();
}
}
public int add() throws InterruptedException {
Thread t1 = new Thread(() ->
{
lk1.lock();
System.out.println("线程t1获取到锁,执行");
addValue();
System.out.println("线程t1释放锁");
lk1.unlock();
});
可以看到我们调用add()方法的时候,线程t1首先会用lk1进行加锁,加锁后当代码执行addValue()的时候,addValue()用同一把锁lk1进行加锁,因为是重入锁,所以这里加锁是成功的,如果是非重入锁,这里线程t1就会阻塞。
另外,这里可以提一下公平和非公平锁,当一个线程未获取到锁就会进入等待队列,公平锁就是谁等待时间长就唤醒谁,非公平锁就随机选。ReentrantLock的构造方法提供是否构造公平锁:
// true为构造公平锁,false为非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 默认为非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
结尾
比起synchronized,lock的优点显而易见,但是有一点不足就是需要手动释放锁,这里我建议大家可以用try{}finally{}的形式,将释放锁的操作放在finally,这样就不会忘记锁的释放。
最后说一句,各位喜欢的话可以点个赞哦。