java多线程之重入锁ReentrantLock

xiaoxiao2021-02-28  73

在讲重入锁(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等待对象中唤醒一个线程。
转载请注明原文地址: https://www.6miu.com/read-76108.html

最新回复(0)