【面试必问系列】之ReentrantLock详解

题记:了解ReentrantLock必须提及的AQS和Lock

ReentrantLock,谈到它又不能的不说AQS,AQS的全称是AbstractQueuedSynchronizer,这个类也是在java.util.concurrent.locks下面,提供了一个FIFO的队列,可以用于构建锁的基础框架,内部通过原子变量state来表示锁的状态,当state大于0的时候表示锁被占用,如果state等于0时表示没有占用锁,ReentrantLock是一个重入锁,表现在state上,如果持有锁的线程重复获取锁时,它会将state状态进行递增,也就是获得一个信号量,当释放锁时,同时也是释放了信号量,信号量跟随减少,如果上一个线程还没有完成任务,则会进行入队等待操作。

ReentrantLock类实现了Lock接口和Serializable接口。

java.util.concurrent.locks包下Lock接口的方法介绍:
在这里插入图片描述

一、ReentrantLock可重入(lock)

/***
* ReentrantLock可重入
*/

import java.util.concurrent.locks.ReentrantLock;
	public class TestReentrantLock1{
	 static ReentrantLock  lock = new ReentrantLock();
	
	 //方法lock1
	  public static  void lock1() {
        try {
            lock.lock();
             log.dubug("---------lock1执行---------");
             lock2();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    //方法lock2
	     public static  void lock2() {
	        try {
	            lock.lock();
	            log.dubug("---------lock2执行---------");      
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        } finally {
	            lock.unlock();
	        }
	    }
    //测试
		   public static void main (string[] args){
			 lock1();
			 }
}

控制台打印结果:

---------lock1执行---------
---------lock2执行---------

1)重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程(此时的lock1)如果再次请求这个锁(lock2),就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
2)synchronized也是可重入锁

二、ReentrantLock可中断(lockInterruptibly)

/***
* ReentrantLock可中断
*/
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TestReentrantLock2{
	 static ReentrantLock  lock = new ReentrantLock();
	 public static void main (string[] args)throws InterruptedException {
	 		//主线程main执行,创建线程t2
			new Thread (()->
			 try {
	            lock.lock();
	            log.dubug("---------t2线程获取锁,然后去睡5秒---------");   
	            TimeUnit.SECONDS.sleep(timeout:5);   
	            log.dubug("---------t2线程睡5秒后醒来了---------");   
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        } finally {
	            lock.unlock();
	        }
			},name:"t2").start();
			
			//主线程main睡2秒,然后创建线程t1
			TimeUnit.SECONDS.sleep(timeout:2); //此处主线程睡眠主要为了确保t2先拿到锁,实现效果
			
			//创建线程t1
			 Thread  t1=new Thread (()->
			 try {
	            lock.lockInterruptibly();//(此处t1尝试加锁失败,睡眠,因为等待上面的t2线程释放锁)
	            log.dubug("---------t1获取到锁---------");   
	        } catch (InterruptedException e) {
	        	log.dubug("---------t1被主线程打断了,没有获取到锁---------");   
	            e.printStackTrace();
	            return;
	        } finally {
	            lock.unlock();
	        }
			},name:"t1");
			t1.start();
			
			//主线程睡完2秒后醒来,打断t1
			log.dubug("---------主线程睡完2秒后醒来,打断t1---------");   
			t1.interrupt();//打断t1,所以t1不再等待t2释放锁了
		}
}

控制台打印结果:

---------t2线程获取锁,然后去睡5---------
---------主线程睡完2秒后醒来,打断t1---------
---------t1被主线程打断了,没有获取到锁---------
java. lang. InterruptedException <3 internal ca1ls>
     at com. TestReentrantLock2.lambda$main$1(TestReentrantLock2. java:31)
     at java. lang. Thread. run(Thread. jiava:748)
Exception in thread "t1" java. lang. IllegalMonitorStateException <3 internal ca1ls>
      at com. TestReentrantLock2.lambda$main$1(TestReentrantLock2. java:38)
      at java.lang. Thread.run(Thread. iava:748)
---------t2线程睡5秒后醒来了---------

小结:1)java.lang.InterruptedException被中止异常
当某个线程处于长时间的等待、休眠或其他暂停状态,而此时其他的线程通过Thread的interrupt方法终止该线程时抛出该异常。
2)lockInterruptibly
通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

三、ReentrantLock超时(tryLock)

/***
* ReentrantLock超时
*/
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TestReentrantLock3{
	 static ReentrantLock  lock = new ReentrantLock();
	 public static void main (string[] args)throws InterruptedException {
	 	
			//创建线程t1
			 Thread  t1=new Thread (()->
			 try {
			 	log.dubug("---------t1启动--------");   
				 if(!lock.tryLock(timeout:2,TimeUnit.SECONDS){//tryLock尝试获取锁,此处可以带参数,设置超时时间,等待2秒,拿不到就返回,若设置成4秒,就可以拿到锁了。不带参数的话只尝试一次,拿不到就返回了。
				 log.dubug("---------t1获取不到锁,返回---------");   
				 return;
				 }
	          } catch (Exception e) {
	             e.printStackTrace();
	         } 
			 try {
			  log.dubug("---------t1获取到锁了---------");  
			 }finally {
			 lock.unlock();
	         }
			},name:"t1");
					
			//主线程先获取锁
			lock.lock();
			log.dubug("---------主线程获取锁---------");
			//启动t1   
			t1.start();
			//然后主线程睡了3秒
			 try {
			 log.dubug("---------主线程开始睡眠3秒--------");  
			 TimeUnit.SECONDS.sleep(timeout:3); 
			 }finally {
			 lock.unlock();
	         }
		}
}

控制台打印结果:

---------主线程获取锁---------
---------主线程开始睡眠3--------
---------t1启动--------
---------t1获取不到锁,返回---------

四、ReentrantLock锁绑定多个条件(newCondition、await、signal)

ReentrantLock锁可以绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。

/***
* ReentrantLock锁可以绑定多个条件
*/
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;

public class TestReentrantLock4{
	 static ReentrantLock  lock = new ReentrantLock();
	 //创建两个回家条件:需要钱、需要票
	 static Condition moneyCondition=lock.newCondition();
	 static Condition ticketCondition=lock.newCondition();
	 Boolean havaMoney=false;//是否有钱
	 Boolean haveTicket=false;//是否有票
	 
	 public static void main (string[] args)throws InterruptedException {
	 	
			//线程farmer1
			 new Thread (()->
			 log.dubug("--------第一个农民想回家--------");   
			 while(!havaMoney){
			 try {
				 lock.lock();
				 log.dubug("---------第一个农民没有钱,回不去家了,等钱---------");   
				 moneyCondition.await();
	          } catch (Exception e) {
	             e.printStackTrace();
	         } finally {
			 lock.unlock();
	         }
	         }
			},name:"farmer1").start();
			
			//线程farmer2
			 new Thread (()->
			 log.dubug("--------第二个农民想回家--------");   
			 while(!haveTicket){
			 try {
				 lock.lock();
				 log.dubug("---------第二个农民没有票,回不去家了,等票---------");   
				 ticketCondition.await();
	          } catch (Exception e) {
	             e.printStackTrace();
	         } finally {
			 lock.unlock();
	         }
	         }
			},name:"farmer2").start();
			
			//主线程12306睡一会	
			Thread.sleep(millis:1000);	
			try{
				lock.lock();
				havaMoney=true;
				log.dubug("---------第一个农民有钱了,唤醒他可以回家了---------");
				moneyCondition.signal();//唤醒等钱的farm1线程执行回家。
				
				haveTicket=true;
				log.dubug("---------第二个农民有票了,唤醒他可以回家了---------");
				ticketCondition.signal();//唤醒等票的farm2线程执行回家。
			}catch (Exception e) {
	             e.printStackTrace();
	         } finally {
			 lock.unlock();
	         }
		},name:"12306").start();
    }
}

精确条件精确唤醒,控制台打印结果:

--------第一个农民想回家--------
---------第一个农民没有钱,回不去家了,等钱---------
--------第二个农民想回家--------
---------第二个农民没有票,回不去家了,等票---------

---------第一个农民有钱了,唤醒他可以回家了---------
---------第二个农民有票了,唤醒他可以回家了---------

小结:1)ReentrantLock锁可以绑定多个条件,synchronized不可以;
2)Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。

五、ReentrantLock实现公平锁

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。

ReentrantLock内部提供了两种AQS的实现,一种公平模式,一种是非公平模式,如果没有特别指定在构造器中,默认是非公平的模式。但可以通过带布尔值的构造函数要求使用公平锁。如下所示:

//源码分析:无参的构造函数
public ReentrantLock() {
    sync = new NonfairSync();
}

当调用有参构造函数时,指定使用哪种模式来进行操作,参数为布尔类型,如果指定为false的话代表非公平模式,如果指定为true的话代表的是公平模式,如下所示:

//源码分析:有参的构造函数
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

公平锁和非公平锁继承关系:
在这里插入图片描述

公平锁和非公平锁内部实现:
通过源码分析可知,如果创建的是无参的ReentrantLock的对象,内部实际调用的ock(),其实是调用了NonfairSync对象的lock方法。如果创建的是有参的ReentrantLock的对象,内部实际调用的lock(),其实是调用的是fairSync对象的lock方法。如下所示:

//源码:
public void lock() {
    sync.lock();
}
/**
 * 非公平模式锁源码
 */
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * 执行锁动作,先进行修改状态,如果锁被占用则进行请求申请锁,申请锁失败则将线程放到队列中
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
		// 继承自AQS的tryAcquire方法,尝试获取锁操作,这个方法会被AQS的acquire调用
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
 非公平源码分析:
 1.调用lock()方法时,首先去通过CAS尝试设置锁资源的state变量,如果设置成功,则设置当前持有锁资源的线程为当前请求线程,申请失败则将线程放到队列中
 2.调用tryAcquire方法时,首先获取当前锁资源的state变量,如果为0,则通过CAS去尝试设置state,如果设置成功,则设置当前持有锁资源的线程为当前请求线程
 以上两步都属于插队现象,非公平,可以提高系统吞吐量。
/**
 * 公平模式锁源码
 */
 static final class FairSync extends Sync {
    private static final long serialVersionUID = -300089789090466540L;
    final void lock() {
	acquire(1);
	}
	protected final boolean tryAcquire(int acquires) {
   final Thread current = Thread.currentThread();
   int c = getState();
   if (c == 0) {
       if (!hasQueuedPredecessors() &&
           compareAndSetState(0, acquires)) {
           setExclusiveOwnerThread(current);
           return true;
       }
   }
   else if (current == getExclusiveOwnerThread()) {
       int nextc = c + acquires;
       if (nextc < 0)
           throw new Error("Maximum lock count exceeded");
       setState(nextc);
       return true;
   }
   return false;
  }
}
 公平源码分析:
 1.调用lock()方法时,不进行CAS尝试
 2.调用tryAcuqire方法时,首先获取当前锁资源的state变量,如果为0,则判断该节点是否是头节点可以去获取锁资源,如果可以才通过CAS去尝试设置state,通过判断该线程是否是队列的头结点,从而保证公平性.

此处只是简单描述,深层次代码调用可以自行分析理解,此处不过多展开。

小结:synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只IT攻城狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值