一、概述

  重入锁ReentrantLock,就是支持重进入的锁 ,它表示该锁能够支持一个线程对资源的重复加锁。支持公平性与非公平性选择,默认为非公平。

  以下梳理ReentrantLock。作为依赖于AbstractQueuedSynchronizer。 所以要理解ReentrantLock,先要理解AQS。013-AbstractQueuedSynchronizer-用于构建锁和同步容器的框架、独占锁与共享锁的获取与释放

aqs有多神奇,让ReentrantLock没有使用更“高级”的机器指令,也不依靠JDK编译时的特殊处理,就完成了代码的并发访问控制。

重进入是指任意线程在获取到锁之后能够再次获取该锁而不被锁所阻塞,需要解决两个问题:

1) 线程再次获取锁(锁需要识别获取锁的线程是否未当前占据锁的线程)

2)锁的最终释放(要求锁对于获取进行计数自增,锁释放技数自减。技数=0表示锁已经成功释放)

1.1、Lock定义以及说明

public interface Lock {
// 获取锁,若当前lock被其他线程获取;则此线程阻塞等待lock被释放
// 如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁
void lock(); // 获取锁,若当前锁不可用(被其他线程获取);
// 则阻塞线程,等待获取锁,则这个线程能够响应中断,即中断线程的等待状态
void lockInterruptibly() throws InterruptedException; // 来尝试获取锁,如果获取成功,则返回true;
// 如果获取失败(即锁已被其他线程获取),则返回false
// 也就是说,这个方法无论如何都会立即返回
boolean tryLock(); // 在拿不到锁时会等待一定的时间
// 等待过程中,可以被中断
// 超过时间,依然获取不到,则返回false;否则返回true
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 释放锁
void unlock(); // 返回一个绑定该lock的Condtion对象
// 在Condition#await()之前,锁会被该线程持有
// Condition#await() 会自动释放锁,在wait返回之后,会自动获取锁
Condition newCondition();
}

1.2、ReentrantLock

  可重入锁。jdk中ReentrantLock是唯一实现了Lock接口的类

  可重入的意思是一个线程拥有锁之后,可以再次获取锁,

  

基本使用:

  1. 创建锁对象 Lock lock = new ReentrantLock()
  2. 在希望保证线程同步的代码之前显示调用 lock.lock() 尝试获取锁,若被其他线程占用,则阻塞
  3. 执行完之后,一定得手动释放锁,否则会造成死锁 lock.unlock(); 一般来讲,把释放锁的逻辑,放在需要线程同步的代码包装外的finally块中

使用方式

private Lock lock = new ReentrantLock();
try {
lock.lock();
// .....
} finally {
lock.unlock();
}

  ReentrantLock会保证 do something在同一时间只有一个线程在执行这段代码,或者说,同一时刻只有一个线程的lock方法会返回。其余线程会被挂起,直到获取锁。从这里可以看出,其实ReentrantLock实现的就是一个独占锁的功能:有且只有一个线程获取到锁,其余线程全部挂起,直到该拥有锁的线程释放锁,被挂起的线程被唤醒重新开始竞争锁。

二、代码探究【主要逻辑即AQS的实现+子类实现的tryAcquire和tryRelease】  

  ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。如果在绝对时间上,先对锁进行获取的请求你一定先被满足,那么这个锁是公平的,反之,是不公平的。公平锁的获取,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock的公平与否,可以通过它的构造函数来决定。

  事实上,公平锁往往没有非公平锁的效率高,但是,并不是任何场景都是以TPS作为唯一指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求越能够得到优先满足。

实现重进入

  重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的首先需要解决以下两个问题:

  线程再次获取锁:所需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次获取成功;
  锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其它线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前线程被重复获取的次数,而被释放时,计数自减,当计数为0时表示锁已经成功释放。
  ReentrantLock是通过自定义同步器来实现锁的获取与释放,我们以非公平锁(默认)实现为例,对锁的获取和释放进行详解。

2.1、获取锁lock【持续等待】

详细参看:013-AbstractQueuedSynchronizer-用于构建锁和同步容器的框架、独占锁与共享锁的获取与释放 的独占锁获取

这里只做上文没有的补充

lock获取锁是一种阻塞是获取。该种方式获取锁不可中断,如果获取不到则一直休眠等待。

默认构造

public ReentrantLock() {
sync = new NonfairSync();
}

即内部同步组件为非公平锁,获取锁的代码为:

public void lock() {
sync.lock();
}

通过简介中的类图可以看到,Sync类是ReentrantLock自定义的同步组件,它是ReentrantLock里面的一个内部类,它继承自AQS,它有两个子类:公平锁FairSync和非公平锁NonfairSync。ReentrantLock的获取与释放锁操作都是委托给该同步组件来实现的。下面我们来看一看非公平锁的lock()方法:

final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

该程序首先会通过compareAndSetState(int, int)方法来尝试修改同步状态,如果修改成功则表示获取到了锁,然后调用setExclusiveOwnerThread(Thread)方法来设置获取到锁的线程,该方法是继承自AbstractOwnableSynchronizer类,AQS继承自AOS类,它的主要作用就是记录获取到独占锁的线程,AOS类的定义很简单:

public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 3737899427754241961L; protected AbstractOwnableSynchronizer() { } // The current owner of exclusive mode synchronization.
private transient Thread exclusiveOwnerThread; protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
} protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}

2.1.1、查看ReentrantLock的sync的实现非公平锁与公平锁

  可见,是Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer:

abstract static class Sync extends AbstractQueuedSynchronizer{

  锁的API是面向使用者的,它定义了与锁交互的公共行为,而每个锁需要完成特定的操作也是透过这些行为来完成的,但是实现是依托给同步器来完成,这样贯穿就容易理解代码。这是一个抽象类,Sync有两个子类:

static final class FairSync extends Sync {}
static final class NonfairSync extends Sync {}

  分别对应于非公平锁、公平锁,默认情况下为非公平锁。
    公平锁:每个线程抢占锁的顺序为先后调用lock方法的顺序依次获取锁。在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己
    非公平锁:每个线程抢占锁的顺序不定,谁运气好,谁就获取到锁,和调用lock方法的先后顺序无关。上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。其中synchronized是非公平锁。ReentrantLock可以选择创建,默认是非公平。

2.1.2、查看acquire

  如果获取锁失败的情况,就是 acquire(1),acquire的是调用AQS来实现的。代码如下:

    public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

2.1.3、查看tryAcquire

  AbstractQueuedSynchronizer中抽象了绝大多数Lock的功能,而只把tryAcquire方法延迟到子类中实现

  

2.2、其他方法

2.2.1、lockInterruptibly【中断】

  这个方法和lock方法的区别就是,lock会一直阻塞下去直到获取到锁,而lockInterruptibly则不一样,它可以响应中断而停止阻塞返回。ReentrantLock对其的实现是调用的Sync的父类AbstractQueuedSynchronizer#acquireInterruptibly方法:

//ReentrantLock#lockInterruptibly
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);//因为ReentrantLock是排它锁,故调用AQS的acquireInterruptibly方法
}
//AbstractQueuedSynchronizer#acquireInterruptibly
public final void acquireInterruptibly(int arg) throws InterruptedException{
  if (Thread.interrupted()) //线程是否被中断,中断则抛出中断异常,并停止阻塞
    throw new InterruptedException;
  if (!tryAcquire(arg)) //首先还是获取锁,具体参照上文
    doAcquireInterruptibly(arg);//独占模式下中断获取同步状态
}

  通过查看doAcquireInterruptibly的方法实现发现它和acquireQueued大同小异,前者抛出异常,后者返回boolean。【参看:013-AbstractQueuedSynchronizer-用于构建锁和同步容器的框架、独占锁与共享锁的获取与释放】独占锁的获取与释放

同lock区别,

示例一、Lock,lock()忽视interrupt(), 锁被主线程占有,子线程拿不到锁就一直阻塞

    @Test
public void testLock() throws Exception{
final Lock lock=new ReentrantLock();
lock.lock();
Thread.sleep(1000);
Thread t1 = new Thread(() -> {
lock.lock();
System.out.println(Thread.currentThread().getName() + " interrupted."); });
t1.start();
Thread.sleep(1000);
t1.interrupt();//lock()忽视interrupt(), 拿不到锁就 一直阻塞
Thread.sleep(10000);
}

无任何输出

示例二、lockInterruptibly()会响应打扰 并catch到InterruptedException

    @Test
public void testlockInterruptibly() throws Exception {
final Lock lock = new ReentrantLock();
lock.lock();
Thread.sleep(1000);
Thread t1 = new Thread(() -> {
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " interrupted.");
}
});
t1.start();
Thread.sleep(1000);
t1.interrupt();
Thread.sleep(10000);
}

输出:Thread-0 interrupted.

2.2.2、tryLock【立即返回】

此方法为非阻塞式的获取锁,不管有没有获取锁都返回一个boolean值。

public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}

  查看实现,它实际调用了Sync#nonfairTryAcquire非公平锁获取锁的方法,这个方法我们在上文lock()方法非公平锁获取锁的时候有提到,而且还特地强调了该方法不是在NonfairSync实现,而是在Sync中实现很有可能这个方法是一个公共方法,果然在非阻塞获取锁的时候调用的是此方法。

final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

  当获取锁时,只有当该锁资源没有被其他线程持有才可以获取到,并且返回true,同时设置持有count为1;
  当获取锁时,当前线程已持有该锁,那么锁可用时,返回true,同时设置持有count加1;
  当获取锁时,如果其他线程持有该锁,无可用锁资源,直接返回false,这时候线程不用阻塞等待,可以先去做其他事情;
  即使该锁是公平锁fairLock,使用tryLock()的方式获取锁也会是非公平的方式,只要获取锁时该锁可用那么就会直接获取并返回true。这种直接插入的特性在一些特定场景是很有用的。但是如果就是想使用公平的方式的话,可以试一试tryLock(0, TimeUnit.SECONDS),几乎跟公平锁没区别,只是会监测中断事件。

示例

    @Test
public void testtryLock() throws Exception {
final Lock lock = new ReentrantLock();
lock.lock();
Thread.sleep(1000);
Thread t1 = new Thread(() -> {
boolean b = lock.tryLock();
System.out.println(Thread.currentThread().getName() + " tryLock."+b); });
t1.start();
Thread.sleep(1000);
t1.interrupt();
Thread.sleep(10000);
}

2.2.3、tryLock(long timeout, TimeUnit unit) 【限时等待】

此方法是表示在超时时间内获取到同步状态则返回true,获取不到则返回false。由此可以联想到AQS的tryAcquireNanos(int arg, long nanosTimeOut)方法

//ReentrantLock#tryLock
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
  return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

Sync实际上调用了父类AQS的tryAcquireNanos方法

//AbstractQueuedSynchronizer#tryAcquireNanos
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();//可以看到前面和lockInterruptibly一样
  return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);//首先也会先尝试获取锁
}

2.2、解锁

解锁方式查看:013-AbstractQueuedSynchronizer-用于构建锁和同步容器的框架、独占锁与共享锁的获取与释放 的独占模式锁 释放

三、小结

  1. 创建锁对象 Lock lock = new ReentrantLock()
  2. 在希望保证线程同步的代码之前显示调用 lock.lock() 尝试获取锁,若被其他线程占用,则阻塞
  3. 执行完之后,一定得手动释放锁,否则会造成死锁 lock.unlock(); 一般来讲,把释放锁的逻辑,放在需要线程同步的代码包装外的finally块中
  4. lock() 和 unlock() 配套使用,不要出现一个比另一个用得多的情况
  5. 不要出现lock(),lock()连续调用的情况,即两者之间没有释放锁unlock()的显示调用
  6. Condition与Lock配套使用,通过 Lock#newConditin() 进行实例化
  7. Condition#await() 会释放lock,线程阻塞;直到线程中断or Condition#singal()被执行,唤醒阻塞线程,并重新获取lock
  8. ReentrantLock#lock的流程图大致如下

  

参看地址:

  https://my.oschina.net/u/566591/blog/1557978

  https://www.cnblogs.com/yulinfeng/p/6906597.html

  https://www.cnblogs.com/maypattis/p/6403682.html

016-并发编程-java.util.concurrent.locks之-Lock及ReentrantLock的更多相关文章

  1. 013-并发编程-java.util.concurrent.locks之-AbstractQueuedSynchronizer-用于构建锁和同步容器的框架、独占锁与共享锁的获取与释放

    一.概述 AbstractQueuedSynchronizer (简称AQS),位于java.util.concurrent.locks.AbstractQueuedSynchronizer包下, A ...

  2. 深入理解java:2.3. 并发编程 java.util.concurrent包

    JUC java.util.concurrent包, 这个包是从JDK1.5开始引入的,在此之前,这个包独立存在着,它是由Doug Lea开发的,名字叫backport-util-concurrent ...

  3. 018-并发编程-java.util.concurrent.locks之-ReentrantReadWriteLock可重入读写锁

    一.概述 ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程.写线程和写线程同时访问.相对 ...

  4. java中的 java.util.concurrent.locks.ReentrantLock类中的lockInterruptibly()方法介绍

    在java的 java.util.concurrent.locks包中,ReentrantLock类实现了lock接口,lock接口用于加锁和解锁限制,加锁后必须释放锁,其他的线程才能进入到里面执行, ...

  5. Java并发编程-并发工具包(java.util.concurrent)使用指南(全)

    1. java.util.concurrent - Java 并发工具包 Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包.这个包包含有一系列能够让 Ja ...

  6. Java并发—java.util.concurrent.locks包

    一.synchronized的缺陷 synchronized是java中的一个关键字,也就是说是Java语言内置的特性.那么为什么会出现Lock呢? 如果一个代码块被synchronized修饰了,当 ...

  7. Java 并发工具包 java.util.concurrent 用户指南

    1. java.util.concurrent - Java 并发工具包 Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包.这个包包含有一系列能够让 Ja ...

  8. Java_并发工具包 java.util.concurrent 用户指南(转)

    译序 本指南根据 Jakob Jenkov 最新博客翻译,请随时关注博客更新:http://tutorials.jenkov.com/java-util-concurrent/index.html.本 ...

  9. 12、java5锁java.util.concurrent.locks.Lock之ReentrantLock

    JDK文档描述: public interface LockLock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作.此实现允许更灵活的结构,可以具有差别很大的属性,可 ...

随机推荐

  1. Atitit s2018.5 s5 doc list on com pc.docx  v2

    Atitit s2018.5 s5  doc list on com pc.docx  Acc  112237553.docx Acc Acc  112237553.docx Acc baidu ne ...

  2. 【Java】类加载过程

    JVM把class文件加载到内存,并对数据进行校验.解析和初始化,最终形成JVM可以直接使用的Java类型的过程. 类加载的过程主要分为三个部分: 加载 链接 初始化 而链接又可以细分为三个小部分: ...

  3. SQL查看死锁+清理死锁

    ----查看sql死锁 CREATE procedure sp_who_lock    as      begin         declare @spid int         declare ...

  4. 字符集之在UTF-8中,一个汉字为什么需要三个字节?

    (一)在UTF-8中,一个汉字为什么需要三个字节? UNICODE是万能编码,包含了所有符号的编码,它规定了所有符号在计算机底层的二进制的表示顺序.有关Unicode为什么会出现就不叙述了,Unico ...

  5. TDD学习笔记【六】一Unit Test - Stub, Mock, Fake 简介

    这篇文章简介一下,如何通过 mock framework,来辅助我们更便利地模拟目标对象的依赖对象,而不必手工敲堆只为了这次测试而存在的辅助类型. 而模拟目标对象的部分,常见的有 stub objec ...

  6. Orders matters: seq2seq for set 实验

    论文提出了input的顺序影响seq2seq结果 有一些输入本身是无序的怎么去处理呢 作者提出LSTM controller的方式 替代输入的LSTM encode方式         作者实验这种方 ...

  7. Qt编写高仿苹果MAC电脑输入法(支持触摸滑动选词)

    最近有个朋友找我定制一个输入法,需要高仿一个苹果MAC电脑的输入法,MAC操作系统的审美无疑是相当棒的,于是乎直接拿以前的输入法高仿了一个,由于之前有做过输入法这块的开发,而且改进了四年,各种需求都遇 ...

  8. 在VS中为C/C++源代码文件生成对应的汇编代码文件(.asm)

    以VS2017为例 然后重新生成工程,在工程目录中就会有对应的汇编代码文件.

  9. 【CF434D】Nanami's Power Plant 最小割

    [CF434D]Nanami's Power Plant 题意:有n个二次函数$y=a_ix^2+b_ix+c_i$($a_i,b_i,c_i$是整数),第i个函数要求x的取值在$[l_i,r_i]$ ...

  10. python nose测试框架全面介绍十二 ----用例执行顺序打乱

    在实际执行自动化测试时,发现我们的用例在使用同一个资源的操作时,用例的执行顺序对测试结果有影响,在手工测试时是完全没法覆盖的. 但每一次都是按用例名字来执行,怎么打乱来执行的. 在网上看到一个有意思的 ...