一、重入锁ReentrantLock简介
重入锁可以完全替代synchronized关键字。在JDK 5.0的早期版本中,重入锁的性能远远好于synchronized,但从JDK 6.0开始,JDK在synchronized上做了大量的优化,使得两者的性能差距并不大。
重入锁使用java.util.concurrent.locks.ReentrantLock类来实现。重入锁使用案例:
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<10000000;j++){ lock.lock(); try{ i++; }finally{ lock.unlock(); } } } public static void main(String[] args) throws InterruptedException { ReenterLock tl=new ReenterLock(); Thread t1=new Thread(tl); Thread t2=new Thread(tl); t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); } }
重入锁对逻辑控制的灵活性要远远好于synchronized。
重入锁是可以反复进入的。
lock.lock(); lock.lock(); try{ i++; }finally{ lock.unlock(); lock.unlock(); }
重入锁可以提供中断处理的能力
对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。而使用重入锁,则提供另外一种可能,那就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。有些时候,这么做是非常有必要的。中断响应可帮助处理死锁
lockInterruptibly() 方法的作用:如果当前线程未被中断则获得锁,如果当前线程被中断则出现异常.
public class TestMain { static class IntLock implements Runnable { // 创建两个ReentrantLock 锁对象 public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); int lockNum; // 定义整数变量,决定使用哪个锁 public IntLock(int lockNum) { this.lockNum = lockNum; } @Override public void run() { try { if (lockNum % 2 == 1) { // 奇数,先锁1,再锁2 lock1.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + "获得锁1,还需要获得锁2"); Thread.sleep(new Random().nextInt(500)); lock2.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + "同时获得了锁 与锁2...."); } else { // 偶数,先锁2,再锁1 lock2.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + "获得锁2,还需要获得锁1"); Thread.sleep(new Random().nextInt(500)); lock1.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + "同时获得了锁1 与锁2...."); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock1.isHeldByCurrentThread()) // 判断当前线程是否持有该锁 lock1.unlock(); if (lock2.isHeldByCurrentThread()) lock2.unlock(); System.out.println(Thread.currentThread().getName() + "线程退出"); } } } public static void main(String[] args) throws InterruptedException { IntLock intLock1 = new IntLock(11); IntLock intLock2 = new IntLock(22); Thread t1 = new Thread(intLock1); Thread t2 = new Thread(intLock2); t1.start(); t2.start(); // 在main 线程,等待3000 秒,如果还有线程没有结束就中断该线程 Thread.sleep(3000);// 可以中断任何一个线程来解决死锁, t2 线程会放弃对锁1 的申请,同时释放锁2,t1 // 线程会完成它的任务 if (t2.isAlive()) { t2.interrupt(); } } }
打印:
Thread-0获得锁1,还需要获得锁2 Thread-1获得锁2,还需要获得锁1 java.lang.InterruptedException at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222) at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) at com.suxiaodong.thread.TestMain$IntLock.run(TestMain.java:33) at java.lang.Thread.run(Thread.java:748) Thread-1线程退出 Thread-0同时获得了锁 与锁2.... Thread-0线程退出
锁申请等待限时
除了等待外部通知之外,要避免死锁还有另外一种方法,那就是限时等待。依然以约朋友打球为例,如果朋友迟迟不来,又无法联系到他。那么,在等待1~2个小时后,我想大部分人都会扫兴离去。对线程来说也是这样。通常,我们无法判断为什么一个线程迟迟拿不到锁。也许是因为死锁了,也许是因为产生了饥饿。但如果给定一个等待时间,让线程自动放弃,那么对系统来说是有意义的
public class TimeLock 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 lock failed"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock.isHeldByCurrentThread()) lock.unlock(); } } public static void main(String[] args) { TimeLock tl = new TimeLock(); Thread t1 = new Thread(tl); Thread t2 = new Thread(tl); t1.start(); t2.start(); } }
在这里,tryLock()方法接收两个参数,一个表示等待时长,另外一个表示计时单位。这里的单位设置为秒,时长为5,表示线程在这个锁请求中,最多等待5秒。如果超过5秒还没有得到锁,就会返回false。如果成功获得锁,则返回true
在本例中,由于占用锁的线程会持有锁长达6秒,故另一个线程无法在5秒的等待时间内获得锁,因此,请求锁会失败
ReentrantLock.tryLock()方法也可以不带参数直接运行。在这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,并立即返回true。如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回false。这种模式不会引起线程等待,因此也不会产生死锁。
public class TryLock implements Runnable { public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); int lock; public TryLock(int lock) { this.lock = lock; } @Override public void run() { if (lock == 1) { while (true) { if (lock1.tryLock()) { try { try { Thread.sleep(500); } catch (InterruptedException e) { } if (lock2.tryLock()) { try { System.out.println(Thread.currentThread() .getId() + ":My Job done"); return; } finally { lock2.unlock(); } } } finally { lock1.unlock(); } } } } else { while (true) { if (lock2.tryLock()) { try { try { Thread.sleep(500); } catch (InterruptedException e) { } if (lock1.tryLock()) { try { System.out.println(Thread.currentThread()getId() + ":My Job done"); return; } finally { lock1.unlock(); } } } finally { lock2.unlock(); } } } } } public static void main(String[] args) throws InterruptedException { TryLock r1 = new TryLock(1); TryLock r2 = new TryLock(2); Thread t1 = new Thread(r1); Thread t2 = new Thread(r2); t1.start(); t2.start(); } }
上述代码中,采用了非常容易死锁的加锁顺序。也就是先让t1获得lock1,再让t2获得lock2,接着做反向请求,让t1申请lock2,t2申请lock1。在一般情况下,这会导致t1和t2相互等待,从而引起死锁。
但是使用tryLock()后,这种情况就大大改善了。由于线程不会傻傻地等待,而是不停地尝试,因此,只要执行足够长的时间,线程总是会得到所有需要的资源,从而正常执行(这里以线程同时获得lock1和lock2两把锁,作为其可以正常执行的条件)。在同时获得lock1和lock2后,线程就打印出标志着任务完成的信息“My Job done”。
执行上述代码,等待一会儿(由于线程中包含休眠500毫秒的代码)。最终你还是可以欣喜地看到程序执行完毕,并产生如下输出,表示两个线程双双正常执行。
9:My Job done
8:My Job done
公平锁
大多数情况下,锁的申请都是非公平的. 如果线程1 与线程2 都在请求锁A, 当锁A 可用时, 系统只是会从阻塞队列中随机的选择一个线程,不能保证其公平性.
公平的锁会按照时间先后顺序,保证先到先得, 公平锁的这一特点不会出现线程饥饿现象.
synchronized 内部锁就是非公平的. ReentrantLock 重入锁提供了一个构造方法:ReentrantLock(boolean fair) ,当在创建锁对象时实参传递true 可以把该锁设置为公平锁. 公平锁看起来很公平,但是要实现公平锁必须要求系统维护一个有序队列,公平锁的实现成本较高,性能也低. 因此默认情况下锁是非公平的. 不是特别的需求,一般不使用公平锁.
public ReentrantLock(boolean fair)
ReentrantLock的几个重要方法整理如下。
- lock():获得锁,如果锁已经被占用,则等待。
- lockInterruptibly():获得锁,但优先响应中断。
- tryLock():尝试获得锁,如果成功,返回true,失败返回false。该方法不等待,立即返回。
- tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。
- unlock():释放锁。
二、 重入锁的好搭档:Condition条件
关键字synchronized 与wait()/notify()这两个方法一起使用可以实现等待/通知模式. Lock 锁的newContition()方法返回Condition 对象,Condition 类也可以实现等待/通知模式.
使用notify()通知时, JVM 会随机唤醒某个等待的线程. 使用Condition 类可以进行选择性通知. Condition 比较常用的两个方法:
await()会使当前线程等待,同时会释放锁,当其他线程调用signal()时,线程会重新获得锁并继续执行.
signal()用于唤醒一个等待的线程
注意:在调用Condition 的await()/signal()方法前,也需要线程持有相关的Lock 锁. 调用await()后线程会释放这个锁,在singal()调用后会从当前Condition 对象的等待队列中,唤醒一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行.
public class ConditionalTest { private static ReentrantLock lock = new ReentrantLock(); private static Condition condition = lock.newCondition(); private static volatile int i = 0; public static class PrintJsThread extends Thread { @Override public void run() { try { lock.lock(); while (i <= 100) { if (i % 2 == 1) { System.out.println(Thread.currentThread().getName() + "print" + i); i++; } condition.signalAll(); condition.await(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public static class PrintOsThread extends Thread { @Override public void run() { try { lock.lock(); while (i <= 100) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + "print" + i); i++; } condition.signalAll(); condition.await(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public static void main(String[] args) { PrintJsThread js = new PrintJsThread(); PrintOsThread os = new PrintOsThread(); js.start(); os.start(); } }
三、Lock与synchronized区别:
