ReentrantLock

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关键字是一致的。

作者:Joe
努力了的才叫梦想,不努力的就是空想,努力并且坚持下去,毕竟这是我相信的力量
原文地址:https://www.cnblogs.com/Joe-Go/p/9687287.html