文章目录
- 1. 简介
- 2. 可重入
- 3. 可中断
- 4. 锁超时
- 5. 使用可重入锁解决哲学家就餐问题
- 6. 公平锁
- 7. 条件变量
1. 简介
ReentrantLock也称为可重入锁,相对于synchronized它有如下特点:
- 可中断:synchronized获取了锁,除非线程自己结束,中途是不能取消锁的使用的
- 可以设置超时时间:synchronized等待锁的线程在管程的Entry-List中等待,直到获取锁,但ReentrantLock可以设置超时时间,如果超过了等待时间,线程可以结束等待过程
- 可以设置公平锁:公平锁可以防止饥饿
- 可以支持多个条件变量:所以条件变量就是管程的wait-set,synchronized只有一个wait-set而ReentrantLock可以根据不同的等待条件设置多个wait-set
与Synchronized一样,都支持可重入
基本语法
//获取锁
reentrantLock.lock();
try{//临界区
}finally{//释放锁reentrantLock.unlock();
}
2. 可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获取锁时,自己也会被锁拦住。
@Slf4j
public class Hello{private static ReentrantLock reentrantLock=new ReentrantLock();public static void main(String[] args){//获取锁reentrantLock.lock();try {log.debug("进入主方法");//再次获取锁m1();}finally {reentrantLock.unlock();}}public static void m1(){reentrantLock.lock();try {log.debug("进入m2");m2();}finally {reentrantLock.unlock();}}public static void m2(){reentrantLock.lock();try {log.debug("进入m3");}finally {reentrantLock.unlock();}}}final class Chopsticks{String name;public Chopsticks(String name){this.name=name;}@Overridepublic String toString() {return "chopsticks{" +"name='" + name + '\'' +'}';}
}
所以ReentrantLock是可重入的
3. 可中断
ReentrantLock中的lockInterruptibly()方法使得线程可以在被阻塞时响应中断,比如一个线程t1通过lockInterruptibly()方法获取到一个可重入锁,并执行一个长时间的任务,另一个线程通过interrupt()方法就可以立刻打断t1线程的执行,来获取t1持有的那个可重入锁。而通过ReentrantLock的lock()方法或者Synchronized持有锁的线程是不会响应其他线程的interrupt()方法的,直到该方法主动释放锁之后才会响应interrupt()方法。
下面演示了lockInterruptibly()的使用
public class Hello{private static ReentrantLock reentrantLock=new ReentrantLock();public static void main(String[] args){Thread t1=new Thread(()->{try{log.debug("尝试获取锁");reentrantLock.lockInterruptibly();} catch (InterruptedException e) {e.printStackTrace();log.debug("没有获取锁");return;} try {log.debug("获取锁成功");}finally {reentrantLock.unlock();}},"线程1");t1.start();}
}
下面模拟有其它线程打断的情况
@Slf4j
public class Hello{private static ReentrantLock reentrantLock=new ReentrantLock();public static void main(String[] args){Thread t1=new Thread(()->{try{Thread.sleep(1000);log.debug("尝试获取锁");reentrantLock.lockInterruptibly();} catch (InterruptedException e) {e.printStackTrace();log.debug("没有获取锁");return;}try {log.debug("获取锁成功");}finally {reentrantLock.unlock();}},"线程1");t1.start();Thread t2=new Thread(()->{reentrantLock.lock();try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}t1.interrupt();},"线程2");t2.start();}
}
可以发现t2打断了t1无限制等待锁,这种机制可以避免死锁的发生(注意通过lock方法获取的锁是不可打断的)
4. 锁超时
ReentrantLock还具备锁超时的能力,调用tryLock(long timeout, TimeUnit unit)方法,在给定时间内获取锁,获取不到就退出,这也是synchronized没有的功能。
方式一
@Slf4j
public class Hello{private static ReentrantLock reentrantLock=new ReentrantLock();public static void main(String[] args){Thread t1=new Thread(()->{//尝试获取锁if (!reentrantLock.tryLock()) {log.debug("获取不到锁");return;}try {log.debug("获得锁了");}finally {reentrantLock.unlock();}},"线程1");reentrantLock.lock();log.debug("获取到锁了");t1.start();}
}
方式二
@Slf4j
public class Hello{private static ReentrantLock reentrantLock=new ReentrantLock();public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{//尝试获取锁try {if (!reentrantLock.tryLock(1, TimeUnit.SECONDS)) {log.debug("获取不到锁:{}",System.currentTimeMillis());return;}} catch (InterruptedException e) {e.printStackTrace();}try {log.debug("获得锁了:{}",System.currentTimeMillis());}finally {reentrantLock.unlock();}},"线程1");reentrantLock.lock();log.debug("获取到锁:{}",System.currentTimeMillis());Thread.sleep(500);reentrantLock.unlock();t1.start();}
}
5. 使用可重入锁解决哲学家就餐问题
哲学家就餐问题
解决哲学家就餐问题的一种思路就是让哲学家按照顺序拿到筷子,即顺序加锁。但这种方式带来的问题就是,可能让有些线程陷入到饥饿状态,这里我们使用ReentrantLock来解决哲学家问题时,就是让每个哲学家等待筷子时有一点的时间,如果在指定时间内没有获取到筷子,就放弃手里的筷子,然后进入思考状态。具体代码实现如下:
@Slf4j
public class Hello{public static void main(String[] args){Chopsticks c1=new Chopsticks("筷子1");Chopsticks c2=new Chopsticks("筷子2");Chopsticks c3=new Chopsticks("筷子3");Chopsticks c4=new Chopsticks("筷子4");Chopsticks c5=new Chopsticks("筷子5");new Philosopher(c1,c2,"哲学家1").start();new Philosopher(c2,c3,"哲学家2").start();new Philosopher(c3,c4,"哲学家3").start();new Philosopher(c4,c5,"哲学家4").start();new Philosopher(c5,c1,"哲学家5").start();}}final class Chopsticks extends ReentrantLock{String name;public Chopsticks(String name){this.name=name;}@Overridepublic String toString() {return "chopsticks{" +"name='" + name + '\'' +'}';}
}@Slf4j
final class Philosopher extends Thread{Chopsticks left;Chopsticks right;@Overridepublic void run() {while(true){if (left.tryLock()) {try{if (right.tryLock()) {try{log.debug("eat");}finally {right.unlock();}}}finally {left.unlock();}}}}public void eat() throws InterruptedException {log.debug("两只筷子都有了,开始吃了");Thread.sleep(1000);}public Philosopher(Chopsticks left, Chopsticks right,String name){super(name);this.left=left;;this.right=right;}}
从结果可以看出,所有的哲学家都吃到了饭,且不会陷入死锁状态。
6. 公平锁
我们知道在sychronized锁住一个对象时,其它线程只能在管程中的Entry-list中被阻塞等待。当释放锁时,会随机选择一个等待线程执行。这种不考虑先来后到的情况就是说明这种锁时不公平的。ReentranLokc默认是一种不公平锁,如下:
public class Hello{private static ReentrantLock reentrantLock=new ReentrantLock();public static void main(String[] args) throws InterruptedException {//主线程获取锁reentrantLock.lock();for (int i = 0; i < 5; i++) {new Thread(()->{//尝试获取锁try {reentrantLock.lock();log.info("获取到锁了");}finally {reentrantLock.unlock();}},"t"+i).start();}Thread.sleep(1000);reentrantLock.unlock();}}
由锁的获取顺序可以知道,ReetranLock不是公平锁
我们可以通过创建ReentranLock时闯入一个boolean指来表示是否开启公平锁功能。
@Slf4j
public class Hello{private static ReentrantLock reentrantLock=new ReentrantLock(true);public static void main(String[] args) throws InterruptedException {//主线程获取锁reentrantLock.lock();for (int i = 0; i < 5; i++) {new Thread(()->{//尝试获取锁try {Thread.sleep(100);reentrantLock.lock();log.info("获取到锁了");} catch (InterruptedException e) {throw new RuntimeException(e);} finally {reentrantLock.unlock();}},"t"+i).start();}Thread.sleep(1000);reentrantLock.unlock();}}
公平锁设计的初衷时解决线程饥饿问题,但这个问题使用tryLock()解决效果更好,公平锁一般没有必要,会降低并发度。
7. 条件变量
synchronized中也有条件变量,就是Monitor锁中的waitSet,当条件不满足时线程进入waitSet等待,ReetranLock的条件变量比synchronized强大之处在于,它时支持多个条件变量的:
- synchronized就是那些不满足条件的线程都在一个waitset中等待
- ReetranLock支持多个waitset,根据不同的条件会有不同的waitset
使用流程
- await前获得锁
- await执行后,会释放锁,进入conditionObject等待
- await的线程被唤醒(或打断、或超时)去重新竞争锁
- 竞争锁成功后,从await后面代码继续执行
下面用代码演示:
public static void main(String[] args) throws InterruptedException {//创建两个条件变量Condition condition1 = reentrantLock.newCondition();Condition condition2 = reentrantLock.newCondition();reentrantLock.lock();condition1.await();//唤醒condition1中的某个线程condition1.signal();//唤醒condition1中的所有线程condition1.signalAll();}