天天看点

java多线程之重入锁ReentrantLock

在讲重入锁(ReentrantLock)之前,相信大家都synchronized很熟悉了,它也是同步控制的一个重要锁,决定了一个线程是否可以访问临界资源,同时synchronized配合Object.wait()和Object.notify()的配合使用起到了等待通知的作用。这里如果大家不是很熟悉,可以查阅资料熟悉一下synchronized的使用。那么有synchronized这个锁,为什么还要介绍ReentrantLock,这肯定是有原因,接下来我就会介绍ReentrantLock比synchronized锁的优点。再说到优点之前,我们先看看ReentrantLock的基本使用。

ReenrantLock的基本使用

public class ReenterLock implements Runnable{
	public static ReentrantLock lock = new ReentrantLock();
	public static int i =0;
	@Override
	public void run() {

		for(int j = 0;j<10000;j++){
			lock.lock();
			try {
				i++;
			} 
			finally{
				lock.unlock();
			}
		}
	}
	
public static void main(String[] args) throws InterruptedException {
	ReenterLock t = new ReenterLock();
	Thread t1 =new Thread(t);
	Thread t2 =new Thread(t);
	t1.start();t2.start();
	t1.join();t2.join();
	System.out.println(i);
}
	
           

通过上面代码我们可以看到ReentrantLock的lock()和unlock()方法要配合try/finally语句块来完成,这是ReentrantLock的语法风格,希望记住。ReentrantLock之所以叫重入锁不是根据它的名字翻译过来的哦,下面我们再看下面代码:

lock.lock();
lock.lock();
try{
	i++;
}finally{
	lock.unlock();
	lock.unlock();
}
           

在这种情况下,一个线程连续两次获得同一把锁,是允许,如果不允许这么操作,那么同一个线程在第二次获得锁时,将会与自己产生死锁。所以重入锁之所以叫重入锁,就是表示同一个线程可以多次获得锁,只要在释放锁时候,与加锁的次数必须相同。这里强调一下synchronized也是可重入的。接下来就说说ReentrantLock相对synchronized的优点:

ReetrantLock的高级功能

  • 可以中断响应

对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得锁继续执行,要么它就保持等待。而重入锁提供了另外一种可能,那就是线程可以中断。也就是在等待锁的过程,程序可以根据需要取消对锁的等待,看如下代码:

public class ReenterLock implements Runnable{

	public static ReentrantLock lock1 = new ReentrantLock();
	public static ReentrantLock lock2 = new ReentrantLock();
	int lock;
	public ReenterLock(int lock){
		this.lock = lock;
	}
	@Override
	public void run() {
		
		try{
			
			if (lock == 1){
				lock1.lockInterruptibly();
				try{
					Thread.sleep(500);
				}catch(InterruptedException e){}
				lock2.lockInterruptibly();}
			else{
				lock2.lockInterruptibly();
				try{
					Thread.sleep(500);
				}catch(InterruptedException e){}
				lock1.lockInterruptibly();
			}
		}catch(InterruptedException e){
			e.printStackTrace();
		}finally{
			if(lock1.isHeldByCurrentThread())
				lock1.unlock();
			if(lock2.isHeldByCurrentThread())
				lock2.unlock();
			System.out.println(Thread.currentThread().getId()+":线程退出");
		}
			
	}
	
	public static void main(String[] args) throws InterruptedException {
		ReenterLock r1 = new ReenterLock(1);
		ReenterLock r2 = new ReenterLock(2);
		Thread t1 =new Thread(r1);
		Thread t2 =new Thread(r2);
		t1.start();t2.start();
		Thread.sleep(1000);
		t2.interrupt();
	}
	
}
           

再看看结果:

java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:896)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1221)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:340)
	at demo.ReenterLock.run(ReenterLock.java:29)
	at java.lang.Thread.run(Thread.java:745)
10:线程退出
9:线程退出
           

阅读上面代码和结果我们就会发现线程t1和t2启动后,t1先占用lock1,再占用lock2;t2先占用lock2,再请求lock1。因此很容易形成t1和t2之间相互等待,变成死锁。但是这里我们使用了t2.interrupt()也就是上面代码最后一条语句,由于我们使用的lockInterruptibly(),这个方法是可以对中断进行响应的。所以t2就会放弃lock1的申请,同时释放已获得lock2。最后t1可以得到lock2继续执行下去。

  • 锁等待限时

除了等待外部通知之外,要避免死锁还有一种方法,那就是限时等待。举个例子,你等待朋友一起打球,在等待1小时,如果朋友迟迟不来,又无法联系到他,那么我们应该扫兴离去。对线程来说也是这样的,我们给一个线程等待锁的时间,如果在这个时间等不到锁,那么线程就会放弃。

public class ReenterLock implements Runnable{

	public static ReentrantLock lock = new ReentrantLock();
	@Override
	public void run() {
		
		try{
			if(lock.tryLock(5, TimeUnit.SECONDS))
			Thread.sleep(6000);
			else{
			System.out.println("get this lock fialed");
			}
		}catch(InterruptedException e){
			e.printStackTrace();
		}finally{
			if(lock.isHeldByCurrentThread())
				lock.unlock();
		}
			
	}
	
	public static void main(String[] args) throws InterruptedException {
		ReenterLock t = new ReenterLock();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		t1.start();t2.start();
		
	}
	
}
           

在这里,tryLock()方法接受两个参数,一个表示等待时长,另外一个表示计时单位。这里的单位为秒,时长为5,表示线程在这个锁请求中,最多等待5秒。如果超过5秒还没有得到锁,就会返回false。这里再介绍一下,ReentrantLock.tryLock()方法也可以不带参数直接运行。这种情况下,当前线程会尝试获得锁,如果所并未被其他线程占用个,则申请所成功,返回true。否则申请失败,立即返回false。

  • 公平锁

在大多数情况下,锁的申请都是非公平的。也就是说,线程1首先请求了锁A,接着线程2页请求了锁A。那么当锁A可用时,是线程1可以获得锁还是线程2可以获得锁?在synchronized这个是不一定,系统会从这个锁的等待队列中随机挑选一个。不管谁先谁后,其实这就是不公平的。而公平锁,则不是这样,它会按照时间的先后顺序,保证先到者先得。这样就不会产生饥饿现象。虽然看起来公平锁很好,但是公平锁需要维护一个有序队列,所以性能就会下降,因此默认情况下,ReentrantLock是非公平的。下面看一段代码体验一下公平锁:

public class ReenterLock implements Runnable{

	public static ReentrantLock fairlock = new ReentrantLock(true);//设置为true开启公平锁,默认是false
	@Override
	public void run() {
		
		while(true){
			try{
				fairlock.lock();
				System.out.println(Thread.currentThread().getName()+"获得锁");
			}finally{
				fairlock.unlock();
			}
		}
			
	}
	
	public static void main(String[] args) throws InterruptedException {
		ReenterLock t = new ReenterLock();
		Thread t1 = new Thread(t,"Thread_1");
		Thread t2 = new Thread(t,"Thread_2");
		t1.start();t2.start();
		
	}
	
}
           

上面代码在通过ReentranLock(boolean fair)设置为true开启公平锁,接下来看看结果:

Thread_1获得锁
Thread_2获得锁
Thread_1获得锁
Thread_2获得锁
Thread_1获得锁
Thread_2获得锁
Thread_1获得锁
Thread_2获得锁
Thread_1获得锁
           

由于代码有大量的输出,这里只截取部分进行说明。这个输出结果中,很明显可以看到,两个线程是交替执行的。

Condition条件

如果大家知道Object.wait 和 Object.notify方法的话,那么就能很容易理解Condition 了,它们的作用大致相同的。Object.wait 和 Object.notify方法的使用必须配合sysnchronized关键字合作使用。这个因为无论是wait()还是notify()都需要首先获得目标对象的一个监视器,这个监视器也就是synchronized绑定的那个对象,而wait()和notify()方法执行后,就会释放这个监视器,下次执行还是需要获得这个监视器。而Condition是与重入锁相关联的。也是基于这个原因。下面就来通过一段代码来进行解释:

public class ReenterLock implements Runnable{

	public static ReentrantLock lock = new ReentrantLock();
	public static Condition condition = lock.newCondition();
	@Override
	public void run() {
			
		try{
			lock.lock();
			condition.await();
			System.out.println("Thread is going on");
			
		}catch(InterruptedException e){
			e.printStackTrace();
			
		}finally{
			lock.unlock();
		}
	}
	
	public static void main(String[] args) throws InterruptedException {
		ReenterLock t = new ReenterLock();
		Thread t1 = new Thread(t);
		t1.start();
		Thread.sleep(2000);
		//通知线程t1继续执行
		lock.lock();
		condition.signal();
		lock.unlock();
		
	}
	
}
           

代码第三行,通过lock生成一个与之绑定的Condition对象。代码condition.await(),要求线程Condition对象上进行等待。代码condition.signa()由主线程main发出通知,告知在Condition上的线程可以继续执行了,这里看到到和wait()和notify()一样,当线程使用Condition.await()时,要求线程持有相关的重入锁,在Condition.await()调用后,这个线程就会释放。同理,在Conditon.signal()方法调用时,也要求线程先获得锁,在signal()方法调用之后,系统会从当前Condition等待对象中唤醒一个线程。