ReentrantLock

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;
	 }

	@Override
	public 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;
	 }

	@Override
	public String toString() {
		return "chopsticks{" +
				"name='" + name + '\'' +
				'}';
	}
}

@Slf4j
final class Philosopher extends Thread{
	Chopsticks left;
	Chopsticks right;

	@Override
	public 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();
	}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值