ReentrantLock可以完全替代synchronized关键字,在Java1.5之前,ReentrantLock的性能远远好与synchronized,在1.5之后,Java1.6开始,JDK在synchronized上做了大量的优化,使得两者的性能差距并不大,但是在使用的灵活性上,ReentrantLock更加的灵活可控。这篇文章将于synchronized关键字比较,若不了解请看我上篇文章 synchronized。
ReentrantLock的几个重要的方法:
❤ lock():获得锁,如果锁被占用,则等待。
❤ lockInterruptibly():获得锁,但优先响应中断。
❤ tryLock():尝试获得锁,如果成功,返回true;失败返回false。该方法不会等待,立即返回。
❤ tryLock(long time,TimeUnit unit):在给定时间内获得锁;如果在时间内成功获得锁,返回true,反之返回false。
❤ unlock():释放锁。
来一个简单案例,示例怎么使用ReentrantLock:
1 public class ReentrantLockDemo implements Runnable{ 2 public static ReentrantLock lock = new ReentrantLock(); 3 public static int i = 0; 4 5 @Override 6 public void run() { 7 for (int j = 0;j < 10000;j++){ 8 lock.lock();//加锁 9 try { 10 i++; 11 }finally { 12 lock.unlock();//释放锁 13 } 14 } 15 } 16 17 //测试 18 public static void main(String[] args) throws InterruptedException { 19 ReentrantLockDemo demo = new ReentrantLockDemo(); 20 Thread t1 = new Thread(demo); 21 Thread t2 = new Thread(demo); 22 t1.start(); 23 t2.start(); 24 t1.join(); 25 t2.join(); 26 System.out.println(i); 27 } 28 }
输出结果:
20000
从输出看出,ReentrantLock保证了多线程的安全性。
从上面代码可以看出,ReentrantLock相比于synchronized,ReentrantLock有着显示的操作过程,使用人员必须手动指定何时加锁,何时释放锁。正因为是这样,ReentrantLock对逻辑控制的灵活性要远远好于synchronized,但必须注意,退出临界区时,必须释放锁,否则,其他线程就没有机会访问到临界区了。
lockInterruptibly()
对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么就继续等待。而对于ReentrantLock来说,它可以使线程在等待的过程中使其中断,这种情况对死锁从处理是有一定的帮助的。
来看下面的例子:
1 public class ReentrantDeadLock implements Runnable { 2 3 //定义两个全局锁 4 public static ReentrantLock lock1 = new ReentrantLock(); 5 public static ReentrantLock lock2 = new ReentrantLock(); 6 //方便构造死锁 7 int lock; 8 9 private ReentrantDeadLock(int lock){ 10 this.lock = lock; 11 } 12 13 @Override 14 public void run() { 15 try { 16 if (lock == 1){ 17 try { 18 lock1.lockInterruptibly(); 19 Thread.sleep(500); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 lock2.lockInterruptibly(); 24 System.out.println("lock == 1 完成任务"); 25 }else{ 26 try { 27 lock2.lockInterruptibly(); 28 Thread.sleep(500); 29 } catch (InterruptedException e) { 30 e.printStackTrace(); 31 } 32 lock1.lockInterruptibly(); 33 System.out.println("lock == 2 完成任务"); 34 } 35 }catch (InterruptedException e){ 36 e.printStackTrace(); 37 }finally { 38 if (lock1.isHeldByCurrentThread()){ 39 System.out.println(Thread.currentThread().getName() + "释放 lock1"); 40 lock1.unlock(); 41 } 42 if (lock2.isHeldByCurrentThread()){ 43 System.out.println(Thread.currentThread().getName() + "释放 lock2"); 44 lock2.unlock(); 45 } 46 47 System.out.println(Thread.currentThread().getName() + ":线程退出"); 48 } 49 } 50 51 public static void main(String[] args) throws InterruptedException { 52 ReentrantDeadLock deadLock = new ReentrantDeadLock(1); 53 ReentrantDeadLock deadLock1 = new ReentrantDeadLock(2); 54 55 Thread t1 = new Thread(deadLock,"t1"); 56 Thread t2 = new Thread(deadLock1,"t2"); 57 t1.start(); 58 t2.start(); 59 Thread.sleep(1000); 60 System.out.println("中断前"); 61 t2.interrupt();//中断t2线程 62 } 63 }
输出结果:
中断前 t2释放 lock2 t2:线程退出 lock == 1 完成任务 t1释放 lock1 t1释放 lock2 t1:线程退出
上面代码执行时,t1线程先占用lock1,再请求占用lock2;t2线程先占用lock2,再请求占用lock1。因此如果不加 t2.interrupt();这段代码,这段程序将会造成死锁。在上述代码中,采用了lockInterruptibly()方法请求锁资源,这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断。
从输出结果来看,在中断前,两个线程处于死锁状态,在 t2.interrupt();后,t2线程被中断,故放弃了对lock1的申请,同时释放了lock2,这样t1就可以获取到lock2,完成任务,完成后,释放所有锁,最后退出。
tryLock(long time,TimeUnit unit):
除了上述用lockInterruptibly()方法,通过外部通知解决死锁的方式外,还有一种方法,就是限时等待;通常来说,我们无法判断为什么一个线程迟迟拿不到锁,也许是因为死锁,也许是因为饥饿。但是如果给定一个等待时间,时间过后自动放弃,那么总的来说还是对系统有意义的。下面展示tryLock(long time,TimeUnit unit)的使用:
1 public class TimeLock implements Runnable { 2 3 public static ReentrantLock lock = new ReentrantLock(); 4 5 @Override 6 public void run() { 7 try { 8 if (lock.tryLock(5, TimeUnit.SECONDS)){ 9 System.out.println(Thread.currentThread().getName() + "获取锁成功!"); 10 Thread.sleep(6000); 11 }else{ 12 System.out.println(Thread.currentThread().getName() + "获取锁失败!"); 13 } 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 }finally { 17 if (lock.isHeldByCurrentThread()){ 18 lock.unlock(); 19 } 20 } 21 } 22 //测试 23 public static void main(String[] args){ 24 TimeLock timeLock = new TimeLock(); 25 Thread t1 = new Thread(timeLock,"t1"); 26 Thread t2 = new Thread(timeLock,"t2"); 27 28 t1.start(); 29 t2.start(); 30 } 31 }
输出:
t2获取锁成功!
t1获取锁失败!
由输出结果看出,在t2获取锁成功后,需要等待6S,但是设置了线程在获取锁请求最多等待5S,所以t1就获取失败了。
tryLock():
使用tryLock()去获取锁资源,如果当前锁未被其他线程占用,则会申请成功,并立即返回true,如果锁被其他线程占用,申请锁的线程也不会等待,而是立即返回false,这种方式不会引起线程等待,因此不会产生死锁。
修改上面第一个例子来演示这个方法:
1 public class TryLock implements Runnable{ 2 3 public static ReentrantLock lock1 = new ReentrantLock(); 4 public static ReentrantLock lock2 = new ReentrantLock(); 5 int lock; 6 7 public TryLock(int lock){ 8 this.lock = lock; 9 } 10 @Override 11 public void run() { 12 if (lock == 1){ 13 while (true){ 14 if (lock1.tryLock()){ 15 System.out.println(Thread.currentThread().getName() + ": 获取到了lock1"); 16 try { 17 try { 18 Thread.sleep(300); 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } 22 //获取lock2资源 23 if (lock2.tryLock()){ 24 try { 25 System.out.println(Thread.currentThread().getName() + ": Done!"); 26 System.out.println("lock == 1 完成!"); 27 return; 28 }finally { 29 lock2.unlock(); 30 } 31 } 32 }finally { 33 lock1.unlock(); 34 } 35 } 36 } 37 }else{ 38 while (true){ 39 if (lock2.tryLock()){ 40 System.out.println(Thread.currentThread().getName() + ": 获取到了lock2"); 41 try { 42 try { 43 Thread.sleep(100); 44 } catch (InterruptedException e) { 45 e.printStackTrace(); 46 } 47 //获取lock1资源 48 if (lock1.tryLock()){ 49 try { 50 System.out.println(Thread.currentThread().getName() + ": Done!"); 51 System.out.println("lock == 2 完成!"); 52 return; 53 }finally { 54 lock1.unlock(); 55 } 56 } 57 }finally { 58 lock2.unlock(); 59 } 60 } 61 } 62 } 63 } 64 //测试 65 public static void main(String[] args){ 66 TryLock tryLock = new TryLock(1); 67 TryLock tryLock1 = new TryLock(2); 68 69 Thread t1 = new Thread(tryLock,"t1"); 70 Thread t2 = new Thread(tryLock1,"t2"); 71 t1.start(); 72 t2.start(); 73 } 74 }
输出结果:
1 t1: 获取到了lock1 2 t2: 获取到了lock2 3 t2: 获取到了lock2 4 t2: 获取到了lock2 5 t1: 获取到了lock1 6 t2: 获取到了lock2 7 t2: 获取到了lock2 8 t2: 获取到了lock2 9 t1: 获取到了lock1 10 t2: 获取到了lock2 11 t2: 获取到了lock2 12 t2: 获取到了lock2 13 t1: 获取到了lock1 14 t2: 获取到了lock2 15 .......... 16 t2: 获取到了lock2 17 t1: 获取到了lock1 18 t2: Done! 19 lock == 2 完成! 20 t1: 获取到了lock1 21 t1: Done! 22 lock == 1 完成!
从结果可看出,t1,t2一直在不停的尝试获取锁,只要执行的时间足够长,线程总是会获取到需要的资源,完成相应的任务。
公平锁和非公平锁
在大多数的情况下,锁的申请都是非公平的。也就是说线程A首先申请了锁A,接着线程B也申请了锁A,那么当锁A可用时,是线程A获得锁还是线程B获得锁呢?这是不一定的。系统只是会从这个锁的等待队列中随机挑选一个,这样就不能保证获得锁的公平性,这就是非公平锁。而公平锁不是这样的,它会按照时间先后顺序,保证先到先得,后到后得。公平锁的一大特点就是:它不会产生饥饿。只要你排队,最终都会得到资源的。若使用synchronized关键字进行锁控制,那么产生的锁就是非公平锁。而ReentrantLock允许我们对其设置公平性。它有一个构造函数签名如下:
public ReentrantLock(boolean fair)
当参数fair为true时,表示锁就是公平锁。公平锁看起来很优美,但是要实现公平锁必然要求维护一个有序队列,因此公平锁的实现成本比较高,性能也相对非常低下,因此,ReentrantLock默认情况下,锁是非公平的。如果没有什么特别的要求,不要使用公平锁。
代码来展示一下,公平锁的特点:
1 public class FairLock implements Runnable{ 2 3 public static ReentrantLock fairlock = new ReentrantLock(true); 4 5 @Override 6 public void run() { 7 while (true){ 8 try { 9 fairlock.lock(); 10 System.out.println(Thread.currentThread().getName() + "获得锁!"); 11 }finally { 12 fairlock.unlock(); 13 } 14 } 15 } 16 //测试 17 public static void main(String[] args){ 18 FairLock fairLock = new FairLock(); 19 Thread t1 = new Thread(fairLock,"t1"); 20 Thread t2 = new Thread(fairLock,"t2"); 21 t1.start(); 22 t2.start(); 23 } 24 }
输出:
t1获得锁!
t2获得锁!
t1获得锁!
t2获得锁!
t1获得锁!
t2获得锁!
t1获得锁!
t2获得锁!
t1获得锁!
t2获得锁!
........
从输出就可以看出,两个线程是交替执行的。
将公平锁修改为非公平锁,输出:
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t2获得锁!
t2获得锁!
t2获得锁!
t2获得锁!
t2获得锁!
......
可以看出,根据系统随机调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的,但是没用公平性。
ReentrantLock是可重入锁
1 public class LockLock implements Runnable { 2 3 public static ReentrantLock lock = new ReentrantLock(); 4 5 @Override 6 public void run() { 7 try { 8 lock.lock(); 9 System.out.println("第一次!"); 10 lock.lock(); 11 System.out.println("第二次!"); 12 }finally { 13 lock.unlock(); 14 lock.unlock(); 15 } 16 } 17 //测试 18 public static void main(String[] args){ 19 LockLock lockLock = new LockLock(); 20 Thread thread = new Thread(lockLock); 21 thread.start(); 22 } 23 }
输出结果:
第一次!
第二次!
从结果,可以看出,一个线程连续获得两次锁,这种操作是允许的。所以ReentrantLock是可重入锁。
ReentrantLock的继承属性
1 public class Father { 2 3 private ReentrantLock lock = new ReentrantLock(); 4 5 public void subOpt() throws InterruptedException { 6 try { 7 lock.lock(); 8 System.out.println("Father 线程进入时间:" + System.currentTimeMillis()); 9 Thread.sleep(5000); 10 System.out.println("Father!" + Thread.currentThread().getName()); 11 }finally { 12 lock.unlock(); 13 } 14 } 15 } 16 17 class SonOverRide extends Father{ 18 @Override 19 public void subOpt() throws InterruptedException { 20 System.out.println("SonOverRide 线程进入时间:" + System.currentTimeMillis()); 21 Thread.sleep(3000); 22 System.out.println("SonOverRide!" + Thread.currentThread().getName()); 23 } 24 } 25 26 class Son extends Father{ 27 public void subOpt() throws InterruptedException { 28 super.subOpt(); 29 } 30 } 31 32 class Test{ 33 public static void main(String[] args){ 34 //测试重写父类中方法类 35 SonOverRide sonOverRide = new SonOverRide(); 36 for (int i= 0;i < 5;i++){ 37 new Thread(){ 38 @Override 39 public void run() { 40 try { 41 sonOverRide.subOpt(); 42 } catch (InterruptedException e) { 43 e.printStackTrace(); 44 } 45 } 46 }.start(); 47 } 48 //测试未重写父类方法的类 49 Son son = new Son(); 50 for (int i = 0;i < 5;i++){ 51 new Thread(){ 52 @Override 53 public void run() { 54 try { 55 son.subOpt(); 56 } catch (InterruptedException e) { 57 e.printStackTrace(); 58 } 59 } 60 }.start(); 61 } 62 } 63 }
输出结果:
SonOverRide 线程进入时间:1537518717790 SonOverRide 线程进入时间:1537518717790 SonOverRide 线程进入时间:1537518717790 SonOverRide 线程进入时间:1537518717790 Father 线程进入时间:1537518717791 SonOverRide 线程进入时间:1537518717791 SonOverRide!Thread-0 SonOverRide!Thread-4 SonOverRide!Thread-2 SonOverRide!Thread-1 SonOverRide!Thread-3 Father!Thread-5 Father 线程进入时间:1537518722791 Father!Thread-6 Father 线程进入时间:1537518727792 Father!Thread-9 Father 线程进入时间:1537518732792 Father!Thread-8 Father 线程进入时间:1537518737792 Father!Thread-7
观察线程进入时间,可以看出重写父类的方法并且没有加锁时,没有同步效果,线程进入时间几乎为同一时间;没有重写父类方法,有同步效果,上一个线程进入到下一个线程进入,间隔刚好5S。与synchronized关键字是一致的。