zoukankan      html  css  js  c++  java
  • 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
    努力了的才叫梦想,不努力的就是空想,努力并且坚持下去,毕竟这是我相信的力量
  • 相关阅读:
    postgreSQL入门命令
    JDBC连接数据库
    nano编辑器使用教程
    Linux上vi(vim)编辑器使用教程
    【编程思想】【设计模式】【行为模式Behavioral】状态模式State
    【编程思想】【设计模式】【行为模式Behavioral】Specification
    【编程思想】【设计模式】【行为模式Behavioral】registry
    【编程思想】【设计模式】【行为模式Behavioral】Publish_Subscribe
    【编程思想】【设计模式】【行为模式Behavioral】观察者模式Observer
    【编程思想】【设计模式】【行为模式Behavioral】备忘录模式Memento
  • 原文地址:https://www.cnblogs.com/Joe-Go/p/9687287.html
Copyright © 2011-2022 走看看