一、概述

  重入锁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. 【翻译】Apache Shiro10分钟教程

    本文为翻译文,原文地址:http://shiro.apache.org/10-minute-tutorial.html 介绍 欢迎来到Apache Shiro的10分钟教程! 通过这个教程,你会明白一 ...

  2. Windows下pip安装及更新出现“UnicodeEncodeError: 'ascii' codec can't encode character u'\u258c' in position 8: ordinal not in range(128)”问题解决办法

    Windows下pip安装及更新出现“UnicodeEncodeError: 'ascii' codec can't encode character u'\u258c' in position 8: ...

  3. java生成兑换码礼包码--工具类

    import java.util.HashSet; import java.util.Random; import java.util.Set; public class GenSerial { pr ...

  4. C#编程高并发的几种处理方法

    并发(英文Concurrency),其实是一个很泛的概念,字面意思就是“同时做多件事”,不过方式有所不同.在.NET的世界里面,处理高并发大致有以下几种方法: 1,异步编程 异步编程就是使用futur ...

  5. AOP 技术原理——代理模式全面总结

    前言 非常重要的一个设计模式,也很常见,很多框架都有它的影子.定义就不多说了.两点: 1.为其它对象提供一个代理服务,间接控制对这个对象的访问,联想 Spring 事务机制,在合适的方法上加个 tra ...

  6. Java开发面试题整理(2019春招)

    一.Java基础部分 1. HashMap和Hashtable各有什么特点,它们有什么区别?(必背题,超级重要) HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们 ...

  7. 写文件的工具类,输出有格式的文件(txt、json/csv)

    import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io. ...

  8. exists oracle 的用法

    CREATE TABLE `A` ( `id` ) NOT NULL AUTO_INCREMENT, `name` ) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGIN ...

  9. PHP 数组转XML 格式

    function buildXml( $data, $wrap= 'xml' ){ $str = "<{$wrap}>"; if( is_array( $data ) ...

  10. Axure RP 9 Beta 开放下载(更新激活密钥和汉化包)

    2018年9月9号,7月9号来厦门入职,已经两个月了.这两个月的生活状态真心不好,一方面工作很忙(刚工作是这样?),虽然工资还可以,但总感觉性价比很低,自已对这份工作不够热爱也许.另一方面,来到新城市 ...