相信不少人在编程时会感受到syncronized关键字实现线程同步所带来的功能限制。而在java中,还有一种手动控制实现线程同步的锁,即实现Lock接口ReentraintLock类,该类为线程同步带来了更为强大的功能。
在介绍ReentraintLock之前,我先简要的分析一下几种锁的概念,在介绍这篇文章之前,首先需要有基础的线程同步概念,若对syncronized关键字还不太了解,建议先看一下我的这篇文章:juejin.cn/post/695325…
-
独占锁
让线程互斥的访问临界区资源,即某一线程获得了锁之后,其他线程若想继续获得这把锁,将被挂起或阻塞。直到获得锁的线程释放这把锁,各线程才能去争夺这把锁。
-
重入锁
一个线程在获得某一把锁之后,可以继续尝试获得这把锁。线程获得这把锁几次,最后就需要释放这把锁几次。
对于syncronized关键字来说,重入锁主要体现在两个方面
1: 递归调用加锁的方法,方法入栈时再一次获得对象的锁,方法出栈时候自动释放对象的锁
2: 在某一方法栈中调用其他方法或其他类的方法,但调用方法是需要获得与当前线程相同的一把锁。
- 公平锁
当线程在等待锁期间被挂起后,锁被释放时,等待时间最长的线程优先获得这把锁(这里线程其实可以理解为排队获得锁,谁被挂起的时间最长,岁就优先获得锁)。
为什么syncronized已经能完成大部分业务场景了,还会出现ReentraintLock
而对syncronized关键字来说,该关键字实现线程同步符合独占锁的概念,当某一线程获得对象或类的锁之后,其他线程若想获得锁就要等待挂起。也符合重如锁的概念,线程可以调用或递归方法,若获取的是通一把锁,不会因为锁已被获取而影响有锁的线程调用,方法入栈时候获得锁,方法出栈时释放锁。但是,对于公平锁来说,syncronized关键字就显的吃力了,因为获得锁、释放锁的流程是自动控制的,我们没法对其进行干预。
因此,对于上述分析,也不难发现syncronized关键字的问题,他的优点往往也可能成为他最致命的缺点,即无法人工对何时加锁、解锁进行干预(虽然wait、notify能够实现部分功能)。也无法响应中断,对线程进行中断后,若非调用方法本身会分析线程时候中断(如Thread.sleep()函数)、或人为进行判断,无法自动的在线程等待锁期间抛出线程中断异常(InteruptException)。同时,无法实现公平锁,各线程能否获得锁,完全取决于cpu是否轮询到该线程,若一个线程运气不好,将一直无法获得到锁,最重要的是,不能实现对锁的尝试获取,若一个线程准备获得一把锁,那么在获得锁之前,他将一直等待下去。
对于上述缺点,能否有一种解决方案或替代方案呢?这时候,ReentraintLock出现了。
ReentraintLock是一种替代syncronized实现线程同步的方案,其加锁、解锁依靠手动方法实现,同时,也支持响应中断、同时,还支持对锁的尝试获得、在一定时间内获得,若某一设定时间内获得不到锁,就会放弃获取锁,最重要的是,实现了对锁的尝试性获取,程序中能够在当前线程在完成一次获取锁的行为后判断是否成功获得锁。
基本使用
class Resource{
void use(MyThread thread){
System.out.println(thread.getName()+"使用了资源");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyThread implements Runnable{
private Lock lock;
private Resource resource;
private String name;
MyThread(Lock lock,Resource resource,String name){
this.lock = lock;
this.resource = resource;
this.name = name;
}
@Override
public void run() {
while (true){
try {
lock.lock();
System.out.println(this.getName()+"获得到了锁");
this.resource.use(this);
}finally {
System.out.println(this.getName()+"释放了锁");
lock.unlock();
try {
Thread.sleep((int)(Math.random()*1000d));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public String getName() {
return name;
}
}
public class Main{
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Resource resource = new Resource();
for (int i=0;i<5;i++){
new Thread(new MyThread(lock,resource,"线程"+i)).start();
}
}
}
复制代码
该例子是对ReentraintLock的基本使用,程序中,起了5个线程,各线程竞争获得锁,在线程开始运行时,调用lock.lock()方法加锁,退出方法时候,调用lock.unlock()方法进行解锁。
如何实现对锁的尝试性获取
Lock接口中,定义了这样的一个方法,boolean tryLock() 和 boolean tryLock(long time,TimeUtil util)。前者支持尝试一次获得锁,若获得锁成功,则返回true,若获得锁失败,则返回false。后者支持设置一个最大响应时间,若改改时间段内成功获得锁,则返回true,否则,返回false。在一定程度上,该机制补充了syncronized关键字的不足。
我们对上述程序进行改进
class MyThread implements Runnable{
private Lock lock;
private Resource resource;
private String name;
MyThread(Lock lock,Resource resource,String name){
this.lock = lock;
this.resource = resource;
this.name = name;
}
@Override
public void run() {
while (true){
try {
while (!lock.tryLock()){
}
System.out.println(this.getName()+"获得到了锁");
this.resource.use(this);
}finally {
System.out.println(this.getName()+"释放了锁");
lock.unlock();
try {
Thread.sleep((int)(Math.random()*1000d));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public String getName() {
return name;
}
}
复制代码
使用while语句,尝试获得锁,若获得锁成功,则退出循环,实现自己的业务方法,最后在再对锁进行一个释放。
同时,这里允许我调皮一下,如何利用boolean tryLock(long time,TimeUtil util)在某一时间段内尝试获得锁,若线程获得锁失败后,即打印抱怨的语句抱怨一下呢?
class Resource{
void use(MyThread thread){
System.out.println(thread.getName()+"使用了资源");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyThread implements Runnable{
private Lock lock;
private Resource resource;
private String name;
MyThread(Lock lock,Resource resource,String name){
this.lock = lock;
this.resource = resource;
this.name = name;
}
@Override
public void run() {
while (true){
try {
while (!lock.tryLock(3,TimeUnit.SECONDS)){
System.out.println(this.getName()+"快点把,我等到花都谢了");
}
System.out.println(this.getName()+"获得到了锁");
this.resource.use(this);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(this.getName()+"释放了锁");
lock.unlock();
try {
Thread.sleep((int)(Math.random()*1000d));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public String getName() {
return name;
}
}
复制代码
main方法不变。
如何实现公平锁
在Reentraint的构造方法中
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
复制代码
fair代编是否实现公平锁,若该参数为true,则以公平锁方式实现,否则,实现非公平锁。默认构造方法实现非公平锁。
public ReentrantLock() {
sync = new NonfairSync();
}
复制代码
大家可以看下下图,若使用公平锁,个线程可以有序执行,谁等待时间最长,谁有限获得锁。
响应中断,若线程带等待锁时被中断中断了,线程能够及时作出反应,lockInterruptibly()方法,首先,我们先看一下该方法的源码,分析其是如何实现响应中断的。
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
复制代码
通过阅读源码可以发现,在线程调用改方法获得锁之后,会先查看线程是否中断,若中断即抛出中断异常InteruptException,若没有中断,进行一次对锁的尝试性获取,若获取失败,则调用doAcquireInterruptibly()方法,在该方法中,会进行如下循环
尝试获得锁,若成功则退出循环,方法结束。若失败,则查看线程是否中断,若中断,抛出中断异常(InterruptedException),否则,进入下一次循环。
结果上诉分析,lockInterruptibly响应中断可以总结为若在线程等待获得锁被挂起期间中断了,就会抛出中断异常让线程进行处理,而改异常属于check类型的异常,必须使用try…cache语句进行捕获,因此,程序能够在异常处理中对线程状态的中断镜像处理。
但是,需要注意的是,调用Thread类的interrupt()只会将线程懂得状态修改为中断,并不会在调用该方法之后,线程就马上停止运行了,除非在线程中运行到判断线程是否中断的语句上,如Thread.sleep()方法,还有刚刚介绍的lockInterruptibly()方法等。
其实,我想写这篇文章最主要的原因是,我看到网上太多例子(其实都是一个啦),这些demo对于改方法的分析不太准确,都是在线程sleep()期间中断线程。
class MyThread implements Runnable {
private Lock lock1;
private Lock lock2;
MyThread(Lock lock1, Lock lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
try {
lock1.lockInterruptibly();
/**
* 模拟耗时操作
* */
Thread.sleep(1000);
lock2.lockInterruptibly();
} catch (InterruptedException exception) {
System.out.println(Thread.currentThread().getName() + "被中断");
} finally {
try {
lock1.unlock();
} catch (IllegalMonitorStateException e) {
}
try {
lock2.unlock();
} catch (IllegalMonitorStateException e) {
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
Thread thread1 = new Thread(new MyThread(lock1,lock2));
Thread thread2 = new Thread(new MyThread(lock2,lock1));
thread1.start();
thread2.start();
Thread.sleep(100);
thread1.interrupt();
}
复制代码
网上大多例子都长这样子,即让2个线程模拟进入死锁状态,线程1获得锁1,休息1000ms后去获得锁2.线程2获得锁2,休息1000ms后去获得锁1.而锁1 锁2已经分别被线程1 2持有了,线程1 若想获得锁2, 除非线程2释放锁2,线程2若想获得锁1,除非线程1释放锁了(这句话有点拗口,大家可以多看几遍理解一下,就是为了让2个线程进入死锁这种无解题)。
在线程1休息期间,在主线程调用线程1的中断方法,模拟线程中断,我认为上述例子是最容易误解人的地方,因为sleep()方法本身就会抛出InteruptExcetion,若在sleep上进行try…cache补货,即将程序修改为如下
class MyThread implements Runnable {
private Lock lock1;
private Lock lock2;
MyThread(Lock lock1, Lock lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
try {
lock1.lockInterruptibly();
/**
* 模拟耗时操作
* */
try{
Thread.sleep(1000);
}cache(InterruptedException exception){
}
lock2.lockInterruptibly();
} catch (InterruptedException exception) {
System.out.println(Thread.currentThread().getName() + "被中断");
} finally {
try {
lock1.unlock();
} catch (IllegalMonitorStateException e) {
}
try {
lock2.unlock();
} catch (IllegalMonitorStateException e) {
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
Thread thread1 = new Thread(new MyThread(lock1,lock2));
Thread thread2 = new Thread(new MyThread(lock2,lock1));
thread1.start();
thread2.start();
Thread.sleep(100);
thread1.interrupt();
}
复制代码
程序还是会进入死锁,因为在sleep方法抛出中断异常后,线程状态有编程非中断的了(不等于抛出中断异常就会设置线程状态为非中断,具体还是代码中设置的)。
总结:
本篇文章算是比较啰嗦吧,其实我的本意是希望有从源代码角度出发,分析问题为什么出现,问题从何而来,如何讲清楚一个问题,本文的总结如下:
syncronized和ReentraintLock的比较
相同点:
- 两者都是独占锁
- 两者都是可重入锁,只不过syncronized获得锁,释放锁是自动的,在进入同步代码块自动获得锁,在退出同步代码块自动释放锁。ReentraintLock为手动,调用几次lock()方法就需要调用几次unLock()方法进行抵消。
不同点:
- syncronized关键字自动获得、释放锁。ReentraintLock需要手动调用lock方法获得锁,调用unlock方法释放锁
- ReentraintLock可以实现公平锁,只需要在构造方法添加参数true即可,默认构造方法味实现非公平锁。
- ReentraintLock支持响应中断,对锁的尝试获得,对锁在一定时间内获得,synchronized关键字不支持。
最后的最后,一定要注意的2点,
- 为什么unlock方法需要在finally中执行?
因为,finally提供一种代码肯定执行的机制,及时在获得锁后执行的业务方法中出现了异常,程序不执行了,但是finally中的方法还是会执行去释放获得锁,否则,其他线程会因为锁一直没有被释放而一直被挂起或在尝试获得锁失败后一直执行替代方案。
- 为什么释放锁unlock()需要使用IllegalMonitorStateException异常捕获,其实不用也可以啊?
当然可以,没有人会限制他人如何写代码。但是,在一段程序中,我们往往可能会获得多把锁,如实现对多个文件的读写,就需要多把锁了。但是,若在获得第一把锁之后,运行业务方法,在获得第二把、第三把锁,如果在业务方法中出现一次,那么就不会运行获得第二把、第三把锁的代码,那么在finally中,锁总是要释放的,但若没有获得锁,又要释放锁,是会抛出IllegalMonitorStateException异常的,该异常继承自RuntimeExcetion,属于uncheck类型的一次,如果不使用异常捕获机制,当然可以,但是出现bug时就需要背锅了。
最后,因本人水平有限,如若文章中有不对的地方,欢迎大佬指正,爱心(^-^)。