Java中ReentrantLock的使用


一、基本概念和使用

可重入锁: 也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。

在JAVA中ReentrantLock 和synchronized 都是可重入锁;

重入锁ReentrantLock 相对来说是synchronized、Object.wait()和Object.notify()方法的替代品(或者说是增强版),在JDK5.0的早期版本,重入锁的性能远远好于synchronized,但从JDK6.0开始,JDK在synchronized上做了大量的优化,使得两者的性能差距并不大。但ReentrantLock也有一些synchronized没法实现的特性。

ReentrantLock 在Java也是一个基础的锁,ReentrantLock 实现Lock接口提供一系列的基础函数,开发人员可以灵活的是应用函数满足各种复杂多变应用场景;

1.Lock接口:

Java中的ReentrantLock 也是实现了Java中锁的核心接口Lock,在Lock接口定义了标准函数,但是具体实现是在实体类中[类似List和ArrayList、LinkedList关系];

//获取锁,获取不到lock就不罢休,不可被打断,即使当前线程被中断,线程也一直阻塞,直到拿到锁, 比较无赖的做法。
    void lock();

   /**
   *获取锁,可中断,如果获取锁之前当前线程被interrupt了,
   *获取锁之后会抛出InterruptedException,并且停止当前线程;
   *优先响应中断
   */
    void lockInterruptibly() throws InterruptedException;

    //立即返回结果;尝试获得锁,如果获得锁立即返回ture,失败立即返回false
    boolean tryLock();

    //尝试拿锁,可设置超时时间,超时返回false,即过时不候
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

   //释放锁
    void unlock();

    //返回当前线程的Condition ,可多次调用
    Condition newCondition();
2.ReentrantLock私有public方法

ReentrantLock中除了实现Lock中定义的一些标准函数外,同时提供其他的用于管理锁的public方法:

//传入boolean值,true时create一个公平锁,false为非公平锁
    ReentrantLock(boolean fair) 

    //查看有多少线程等待锁
    int getQueueLength()

    //是否有线程等待抢锁
    boolean hasQueuedThreads()

    //是否有指定线程等待抢锁
    boolean hasQueuedThread(Thread thread)

    //当前线程是否抢到锁。返回0代表没有
    int getHoldCount()

    //查询此锁是否由任何线程持有
     boolean isLocked()

     //是否为公平锁
    boolean isFair()
3.ReentrantLock中Condition的使用

ReentrantLock中另一个重要的应用就是Condition,Condition是Lock上的一个条件,可以多次newCondition()获得多个条件,Condition可用于线程间通信,通过Condition能够更加精细的控制多线程的休眠与唤醒,而且在粒度和性能上都优于Object的通信方法(wait、notify 和 notifyAll);
还时先看一下Condition 接口的源码吧:

public interface Condition {
    /**
    *Condition线程进入阻塞状态,调用signal()或者signalAll()再次唤醒,
    *允许中断如果在阻塞时锁持有线程中断,会抛出异常;
    *重要一点是:在当前持有Lock的线程中,当外部调用会await()后,ReentrantLock就允许其他线程来抢夺锁当前锁,
    *注意:通过创建Condition对象来使线程wait,必须先执行lock.lock方法获得锁
    */
    void await() throws InterruptedException;

    //Condition线程进入阻塞状态,调用signal()或者signalAll()再次唤醒,不允许中断,如果在阻塞时锁持有线程中断,继续等待唤醒
    void awaitUninterruptibly();

    //设置阻塞时间,超时继续,超时时间单位为纳秒,其他同await();返回时间大于零,表示是被唤醒,等待时间并且可以作为等待时间期望值,小于零表示超时
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    //类似awaitNanos(long nanosTimeout);返回值:被唤醒true,超时false
    boolean await(long time, TimeUnit unit) throws InterruptedException;

   //类似await(long time, TimeUnit unit) 
    boolean awaitUntil(Date deadline) throws InterruptedException;

   //唤醒指定线程
    void signal();

    //唤醒全部线程
    void signalAll();
}

ReentrantLock.Condition的线程通信
ReentrantLock.Condition是在 粒度性能 上都优于Object的notify()、wait()、notifyAll()线程通信的方式。

Condition中通信方法相对Object的通信在粒度上是粒度更细化,表现在一个Lock对象上引入多个Condition监视器、通信方法中除了和Object对应的三个基本函数外,更是新增了线程中断、阻塞超时的函数;
Condition中通信方法相对Object的通信在性能上更高效,性能的优化表现在ReentrantLock比较synchronized的优化 ;

基本示例代码:
多线程通信/同步的一个经典的应用属于生产者消费者模式,关于通过ReentrantLock的newCondition()是实现生产者消费者模式可以直接参考: 生产者消费者模式 ;

ReentrantLock.Condition线程通信注意点:
  • 1.使用ReentrantLock.Condition的signal()、await()、signalAll()方法使用之前必须要先进行lock()操作[记得unlock()],类似使用Object的notify()、wait()、notifyAll()之前必须要对Object对象进行synchronized操作;否则就会抛IllegalMonitorStateException;
  • 2.注意在使用ReentrantLock.Condition中使用signal()、await()、signalAll()方法 ,不能和Objectnotify()、wait()、notifyAll()方法混用,否则抛出IllegalMonitorStateException`** ;
4.公平锁与非公平锁

公平锁: 是指多个线程竞争同一资源时[等待同一个锁时],获取资源的顺序是按照申请锁的先后顺序的;公平锁保障了多线程下各线程获取锁的顺序,先到的线程优先获取锁,有点像早年买火车票一样排队早的人先买到火车票;
基本特点: 线程执行会严格按照顺序执行 ,等待锁的线程不会饿死,但 整体效率相对比较低

非公平锁: 是指多个线程竞争同一资源时,获取资源的顺序是不确定的,一般是抢占式的;非公平锁相对公平锁是增加了获取资源的不确定性,但是整体效率得以提升;
基本特点: 整体效率高,线程等待时间片具有不确定性

公平锁与非公平锁的测试demo:

重入锁ReentrantLock实现公平锁和非公平锁很简单的,因为ReentrantLock构造函数中可以直接传入一个boolean值fair,对公平性进行设置。当fair为true时,表示此锁是公平的,当fair为false时,表示此锁是非公平的锁;
来个简单的demo;

public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        ReentrantLock fairLock = new ReentrantLock(true);
        ReentrantLock unFairLock = new ReentrantLock();
        for (int i = 0; i < 10; i++) {
            threadPool.submit(new TestThread(fairLock,i," fairLock"));
            threadPool.submit(new TestThread(unFairLock, i, "unFairLock"));
        }
    }

    static class TestThread implements Runnable {
        Lock lock;
        int indext;
        String tag;

        public TestThread(Lock lock, int index, String tag) {
            this.lock = lock;
            this.indext = index;
            this.tag = tag;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getId() + " 线程 START  " + tag);
            meath();
        }

        private void meath() {
            lock.lock();
            try {
                if((indext&0x1)==1){
                    Thread.sleep(200);
                }else{
                    Thread.sleep(500);
                }
                System.out.println(Thread.currentThread().getId() + " 线程 获得: Lock  ---》" + tag + "  Index:" + indext);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }

    }

二、ReentrantLock与synchronized简单对比

ReentrantLock是JDK1.5之后引入的,synchronized作为关键字在ReentrantLock引入后进行的大量修改性能不断提升;

1.可重入性

ReentrantLock和synchronized都具有可重入性,写代码synchronized更简单,ReentrantLock需要将lock()和unlock()进行一一对应否则有死锁的风险;

2.锁的实现方式

Synchronized作为Java关键字是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。

3.公平性

ReentrantLock提供了公平锁和非公平锁两种API,开发人员完全可以根据应用场景选择锁的公平性;
synchronized是作为Java关键字是依赖于JVM实现,Java团队应该是优先考虑性能问题,因此synchronized是非公平锁。

  • 小插曲

之前看了很多博文有些人说synchronized是公平锁有人说是非公平锁,总之,看到让人的苦笑不得,于是自己测试一下[JDK1.8]测试代码如下,结果很明显synchronized就是一中非公平锁。

public synchronizedTest() {
        for(int i=0;i<20;i++){
            int finalI = i;
            new Thread(() ->
                test(finalI)
            ).start();
        }
    }
    synchronized  private void test(int index) {
            System.out.println("--------------- > Task :" + index);
    }
}
4.二者性能和粒度

Java一直被诟病的就是性能问题,所以这是一个很重要的问题。
在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞;

至于二者的细粒度差别就更明显了,Synchronized只是关键字,而ReentrantLock则提供较为多样的实现方式和更多的功能;

5.编程灵活度和难度

根据上面的介绍估计这个问题已近很明确了;
很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。其中ReenTrantLock使用不当死锁问题更是让人头痛不已。
灵活度:很明显ReentrantLock优于synchronized;
难度:也很明显ReentrantLock难于synchronized;

  • ReenTrantLock实现的原理:

在网上看到相关的源码分析,本来这块应该是本文的核心,但是感觉比较复杂就不一一详解了,简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。
什么情况下使用ReenTrantLock:
答案是,如果你需要实现ReenTrantLock的三个独有功能时

三、ReentrantLock实际开发中的应用场景

1.公平锁,线程排序执行,防饿死应用场景;

公平锁原则必须按照锁申请时间上先到先得的原则分配机制场景;

1). 实现逻辑 上(包括:软件中函数计算、业务先后流程;硬件中操作实现中顺序逻辑)的顺序排队机制的场景;
软件场景:用户交互View中对用户输入结果分析类,分析过程后面算法依赖上一步结果的场景, 例如:推荐算法实现 [根据性别、年龄筛选]、 阻塞队列的实现
硬件场景:需要先分析确认用户操作类型硬件版本或者厂家,然后发出操作指令;例如:自动售货机;

2).现实 生活中 时间排序的 公平原则例如:客服分配,必须是先到先服务,不能出现饿死现象
公平锁实现见上文:公平锁与非公平锁的测试demo:
逻辑代码实现那就没法子实现了;
阻塞队列的实现就是时间上的公平原则

示例代码:没有!!!

2.非公平锁,效率的体现者;

实际开发中最常用的的场景就是非公平锁,ReentrantLock无参构造默认就时候非公平锁;
适应场景除了上面公平锁中提到的其他都是非公平锁的使用场景;

示例代码:没有!!!

3.ReentrantLock.Condition线程通信

ReentrantLock.Condition线程通信是最长见的面试题,这里以最简单例子:两个 线程之间交替打印 26英文字母和阿拉伯数字为demo:

private void alternateTask() {
        ReentrantLock lock = new ReentrantLock();
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();
        Thread thread1 = new Thread(() -> {
            try {
                lock.lock();
                for (int i = 65; i < 91; i++) {
                    System.out.println("----------thread1------- " + (char) i);
                    condition2.signal();
                    condition1.await();
                }
                condition2.signal();
            } catch (Exception e) {
            } finally {
                lock.unlock();
            }
        });
        Thread thread2 = new Thread(() -> {
            try {
                lock.lock();
                for (int i = 0; i < 26; i++) {
                    System.out.println("----------thread2------- " + i);
                    condition1.signal();
                    condition2.await();
                }
                condition1.signal();
            } catch (Exception e) {
            } finally {
                lock.unlock();
            }
        });
        thread1.start();
        thread2.start();
    }
4.同步功能的使用

实现线程同步锁synchronized 功能【单例为例】

private Singleton() {
    }

    private static Singleton instance;
    private static Lock lock = new ReentrantLock();

    public static Singleton getInstance() {
        lock.lock();
        try {
            if (instance == null) {
                instance = new Singleton();
            }
        } finally {
            lock.unlock();
        }
        return instance;
    }
6.中断杀器应用

ReentrantLock中lockInterruptibly()和lock()最大的区别就是中断相应问题:
lock()是支持中断相应的阻塞试的获取方式,因此即使主动中断了锁的持有者,但是它不能立即unlock(),仍然要机械版执行完所有操作才会释放锁。
lockInterruptibly()是 优先响应中断的,这样有个优势就是可以通过tryLock()、tryLock(timeout, TimeUnit.SECONDS)方法,中断优先级低的Task,及时释放资源给优先级更高的Task,甚至看到网上有人说可以做防止死锁的优化;

实例代码:

ReentrantLock lock = new ReentrantLock();
        try {
            lock.lockInterruptibly();
            if (lock.tryLock(timeout, TimeUnit.SECONDS)) {
                //TODO
            }else{
                //超时直接中断优先级低的Task
                Thread.currentThread().interrupt();
                lock.lock();
                //TODO
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
7.非重要任务Lock使用

优先级较低的操作让步给优先级更高的操作,提示代码效率/用户体验;
忽略重复触发
1).用在定时任务时,如果任务执行时间可能超过下次计划执行时间,确保该有状态任务只有一个正在执行,忽略重复触发。
2).用在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)。
以上两种情况多用于进行非重要任务防止重复执行,(如:清除无用临时文件,检查某些资源的可用性,数据备份操作等)
tryLock()功能:如果已经获得锁立即返回fale,起到防止重复而忽略的效果

ReentrantLock lock = new ReentrantLock();
      //防止重复执行,执行耗时操作,例如用户重复点击
       if (lock.tryLock()) {
           try {
            //TO DO
           } finally {
             lock.unlock();
           }
       }

超时放弃
定时操作的例如:错误日志、定时过期缓存清理的操作,遇到优先级更高的操作占用资源时,暂时放弃本次操作下次再处理,可以起到让出CPU,提升用户体验;

ReentrantLock lock = new ReentrantLock();
        try {
            if (lock.tryLock(timeout, TimeUnit.SECONDS)) {
                //TO DO
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

四、最后

1.本文最为学习ReentrantLock的学习笔记,使用文中应该有诸多纰漏错误之处,望诸位高手及时指正,菜鸡感激不尽;
2.如一种所述本篇文章属于学习笔记,所以其中有很多都是间了以下文章,在此一并感谢;

ReentrantLock使用场景和实例 ; 生产者消费者模式 ; ReenTrantLock可重入锁(和synchronized的区别)总结 ; ReentrantLock的条件锁Condition用法;


原文链接:https://blog.csdn.net/black_bird_cn/article/details/81913671