安卓基础开发——ReentrantLock的简单使用

语言: CN / TW / HK

theme: scrolls-light

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 8 天,点击查看活动详情

前言

之前提到过有大佬给了建议,让我创建一个共享的ReentrantLock来解决快速多次点击检查权限时候(当然情景是更为复杂,有其他UI操作同时进行(切换播放区域),应该还有一个进度条DIalog也在展示占用屏幕)重复弹出弹窗。

但我对这个类不是很熟悉,之前只是看过介绍该知识点的文章,还没开发中应用操作过,所以我们先看看ReentrantLock的简单使用。

正篇

ReentrantLock概况

ReentrantLock(翻译过来叫可重入锁)是基于AQS进行的实现。

原理基础

其中AQS的全称为AbstractQueuedSynchronizer(直译为抽象队列同步器),是阻塞式锁和同步器工具的框架,用一个state来表示锁状态,通过子类CAS(保证原子性)去操作,内部维护了一个等待队列进行排队,使用条件变量来实现等待、唤醒,支持多个条件变量功能,我们的ReentrantLock只是其中的一种实现方式,是AQS排他模式(独占锁),此外还有使用AQS共享模式的ReentrantReadWriteLock中的读锁。

背景资料

在jdk1.6之前,我们使用锁实现同步使用的是synchronized关键字,但是synchronized的实现原理是调用操作系统函数来实现加锁/解锁,而一旦涉及操作系统的函数,那么代码执行的效率就会变低,所以,使用synchronized关键字来实现加/解锁就被称为重量级锁。\ 为了改善这一情况,Doug Lea就写了ReentrantLock锁,这种锁分情况在jvm层面和操作系统层面完成加锁/解锁的过程,因而代码执行效率显著提高,后来Sun公司在jdk1.6以后也改进了synchronized,使得synchronized的执行效率和ReentrantLock差不多,甚至更好,但是由于ReentrantLock可以直接代码操作加锁/解锁,可中断获取锁等特性,因此使用的比较多。

ReentrantLock使用

当有多个线程需要对同一个共享变量进行操作的时候就需要考虑上锁了,下面我们用一条样例代码说明:

线程1与线程2抢锁的样例代码如下: ```Java package com.example.weekreference;

import java.util.concurrent.locks.ReentrantLock;

public class LockTest {

public static void main(String[] args){
    final ReentrantLock lock = new ReentrantLock(true);
    Thread LockTest1 = new Thread("LockTest1"){
        @Override
        public void run() {
            lock.lock();

            lockTest();
            lock.unlock();
        }
    };
    Thread LockTest2 = new Thread("LockTest2"){
        @Override
        public void run() {
            lock.lock();
            lockTest();
            lock.unlock();
        }
    };
    LockTest1.start();
    LockTest2.start();
}

/**
 * 打印线程名,以及线程休眠2秒后打印日志
 */
public static void lockTest(){
    System.out.println(Thread.currentThread().getName());
    try {
        Thread.sleep(2000);
        System.out.println(" LockTest-------end");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

} ``` 我们可以右击方法名旁边的绿色三角形选择覆盖率测试运行(Run “...” with Coverage)

image.png 等待运行完成,我们能看到线程的打印

image.png 其中lock()方法就是加锁,unlock()方法就是释放锁。

image.png

image.png 如果我们把上面样例代码的ReentrantLock对象的lock方法和unlock方法去除,再次运行打印,我们就发现:

image.png 线程1和线程2都会正常运行,但线程休眠方法打印的日志会靠后,而加锁和解锁时,我们线程会依次等待,线程2会等线程1操作完释放锁后,才去运行。

而其中我们去实现ReentrantLock初始化时,我们传入了一个boolean值,这是用来选择是公平锁还是非公平锁的,默认是非公平锁,有关公平锁与非公平锁(Java中是抢锁时区分)简要意义如下: - 公平锁 - 按照请求锁的顺序来获取锁 - 非公平锁 - 是否能获取到锁与请求锁的顺序无关

image.png 下面再介绍几个常用的方法。

常用方法

  • isLocked() \ 是否持有锁

  • isFair() \ 是否是公平锁

  • hasQueuedThreads() \ 锁等待队列中是否有线程在等待锁

  • hasQueuedThread(Thread thread) \ 指定的线程是否存在于锁等待队列中

  • getQueueLength() \ 锁等待队列中的等待线程数

  • isHeldByCurrentThread() \ 当前线程是否持有此锁

  • getHoldCount() \ 当前线程持有此锁的数量(是数量,而不是次数。比如嵌套锁时,当前线程持有此锁的数量就可能会大于 1)

总结

这个部分内容我自己实践的也不够,后续有新的关于ReentrantLock方面的会及时补充到这篇文章,主要本次未解释自旋锁和阻塞锁,此外前言提到的共享状态,我通过查询资料得知AQS其实还有ReentrantReadWriteLock(读写锁,读锁是共享锁)和CountDownLatch(完全只用了AQS共享模式),本篇就讲的是独占锁(排他),而不是共享锁(读写锁): final ReentrantLock lock = new ReentrantLock(true); final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); final CountDownLatch countDownLatch = new CountDownLatch(1);

image.png 这几个后续再开新文章去记录学习过程。