Java多线程系列——深入重入锁ReentrantLock

简述

ReentrantLock 是一个可重入的互斥(/独占)锁,又称为“独占锁”。

ReentrantLock通过自定义队列同步器(AQS-AbstractQueuedSychronized,是实现锁的关键)来实现锁的获取与释放。

其可以完全替代 synchronized 关键字。JDK 5.0 早期版本,其性能远好于 synchronized,但 JDK 6.0 开始,JDK 对 synchronized 做了大量的优化,使得两者差距并不大。

“独占”,就是在同一时刻只能有一个线程获取到锁,而其它获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。

“可重入”,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。

该锁还支持获取锁时的公平和非公平性选择。“公平”是指“不同的线程获取锁的机制是公平的”,而“不公平”是指“不同的线程获取锁的机制是非公平的”。

简单实例

import java.util.concurrent.locks.ReentrantLock;
/*** Created by zhengbinMac on 2017/3/2.*/
public class ReenterLock implements Runnable{public static ReentrantLock lock = new ReentrantLock();public static int i = 0;public void run() {for (int j = 0;j<100000;j++) {lock.lock();
//            lock.lock();try {i++;}finally {lock.unlock();
//                lock.unlock();
            }}}public static void main(String[] args) throws InterruptedException {ReenterLock reenterLock = new ReenterLock();Thread t1 = new Thread(reenterLock);Thread t2 = new Thread(reenterLock);t1.start();t2.start();t1.join();t2.join();System.out.println(i);}
}

与 synchronized 相比,重入锁有着显示的操作过程,何时加锁,何时释放,都在程序员的控制中。

为什么称作是“重入”?这是因为这种锁是可以反复进入的。将上面代码中注释部分去除注释,也就是连续两次获得同一把锁,两次释放同一把锁,这是允许的。

注意,获得锁次数与释放锁次数要相同,如果释放锁次数多了,会抛出 java.lang.IllegalMonitorStateException 异常;如果释放次数少了,相当于线程还持有这个锁,其他线程就无法进入临界区。

引出第一个问题:为什么 ReentrantLock 锁能够支持一个线程对资源的重复加锁?

除了简单的加锁、解锁操作,重入锁还提供了一些更高级的功能,下面结合实例进行简单介绍:

中断响应(lockInterruptibly)

对于 synchronized 来说,如果一个线程在等待锁,那么结果只有两种情况,获得这把锁继续执行,或者线程就保持等待。

而使用重入锁,提供了另一种可能,这就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的需求。

下面的例子中,产生了死锁,但得益于锁中断,最终解决了这个死锁:

 1 import java.util.concurrent.locks.ReentrantLock;
 2 /**
 3  * Created by zhengbinMac on 2017/3/2.
 4  */
 5 public class IntLock implements Runnable{
 6     public static ReentrantLock lock1 = new ReentrantLock();
 7     public static ReentrantLock lock2 = new ReentrantLock();
 8     int lock;
 9     /**
10      * 控制加锁顺序,产生死锁
11      */
12     public IntLock(int lock) {
13         this.lock = lock;
14     }
15     public void run() {
16         try {
17             if (lock == 1) {
18                 lock1.lockInterruptibly(); // 如果当前线程未被 中断,则获取锁。
19                 try {
20                     Thread.sleep(500);
21                 } catch (InterruptedException e) {
22                     e.printStackTrace();
23                 }
24                 lock2.lockInterruptibly();
25                 System.out.println(Thread.currentThread().getName()+",执行完毕!");
26             } else {
27                 lock2.lockInterruptibly();
28                 try {
29                     Thread.sleep(500);
30                 } catch (InterruptedException e) {
31                     e.printStackTrace();
32                 }
33                 lock1.lockInterruptibly();
34                 System.out.println(Thread.currentThread().getName()+",执行完毕!");
35             }
36         } catch (InterruptedException e) {
37             e.printStackTrace();
38         } finally {
39             // 查询当前线程是否保持此锁。
40             if (lock1.isHeldByCurrentThread()) {
41                 lock1.unlock();
42             }
43             if (lock2.isHeldByCurrentThread()) {
44                 lock2.unlock();
45             }
46             System.out.println(Thread.currentThread().getName() + ",退出。");
47         }
48     }
49     public static void main(String[] args) throws InterruptedException {
50         IntLock intLock1 = new IntLock(1);
51         IntLock intLock2 = new IntLock(2);
52         Thread thread1 = new Thread(intLock1, "线程1");
53         Thread thread2 = new Thread(intLock2, "线程2");
54         thread1.start();
55         thread2.start();
56         Thread.sleep(1000);
57         thread2.interrupt(); // 中断线程2
58     }
59 }
View Code

上述例子中,线程 thread1 和 thread2 启动后,thread1 先占用 lock1,再占用 lock2;thread2 反之,先占 lock2,后占 lock1。这便形成 thread1 和 thread2 之间的相互等待。

代码 56 行,main 线程处于休眠(sleep)状态,两线程此时处于死锁的状态,代码 57 行 thread2 被中断(interrupt),故 thread2 会放弃对 lock1 的申请,同时释放已获得的 lock2。这个操作导致 thread1 顺利获得 lock2,从而继续执行下去。

执行代码,输出如下:

锁申请等待限时(tryLock)

除了等待外部通知(中断操作 interrupt )之外,限时等待也可以做到避免死锁。

通常,无法判断为什么一个线程迟迟拿不到锁。也许是因为产生了死锁,也许是产生了饥饿。但如果给定一个等待时间,让线程自动放弃,那么对系统来说是有意义的。可以使用 tryLock() 方法进行一次限时的等待。

 1 import java.util.concurrent.TimeUnit;
 2 import java.util.concurrent.locks.ReentrantLock;
 3 /**
 4  * Created by zhengbinMac on 2017/3/2.
 5  */
 6 public class TimeLock implements Runnable{
 7     public static ReentrantLock lock = new ReentrantLock();
 8     public void run() {
 9         try {
10             if (lock.tryLock(5, TimeUnit.SECONDS)) {
11                 Thread.sleep(6 * 1000);
12             }else {
13                 System.out.println(Thread.currentThread().getName()+" get Lock Failed");
14             }
15         } catch (InterruptedException e) {
16             e.printStackTrace();
17         }finally {
18             // 查询当前线程是否保持此锁。
19             if (lock.isHeldByCurrentThread()) {
20                 System.out.println(Thread.currentThread().getName()+" release lock");
21                 lock.unlock();
22             }
23         }
24     }
25     /**
26      * 在本例中,由于占用锁的线程会持有锁长达6秒,故另一个线程无法再5秒的等待时间内获得锁,因此请求锁会失败。
27      */
28     public static void main(String[] args) {
29         TimeLock timeLock = new TimeLock();
30         Thread t1 = new Thread(timeLock, "线程1");
31         Thread t2 = new Thread(timeLock, "线程2");
32         t1.start();
33         t2.start();
34     }
35 }
View Code

上述例子中,由于占用锁的线程会持有锁长达 6 秒,故另一个线程无法在 5 秒的等待时间内获得锁,因此,请求锁失败。

ReentrantLock.tryLock()方法也可以不带参数直接运行。这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁成功,立即返回 true。否则,申请失败,立即返回 false,当前线程不会进行等待。这种模式不会引起线程等待,因此也不会产生死锁。

公平锁

默认情况下,锁的申请都是非公平的。也就是说,如果线程 1 与线程 2,都申请获得锁 A,那么谁获得锁不是一定的,是由系统在等待队列中随机挑选的。这就好比,买票的人不排队,售票姐姐只能随机挑一个人卖给他,这显然是不公平的。而公平锁,它会按照时间的先后顺序,保证先到先得。公平锁的特点是:不会产生饥饿现象。

重入锁允许对其公平性进行设置。构造函数如下:

public ReentrantLock(boolean fair)

下面举例来说明,公平锁与非公平锁的不同:

 1 import java.util.concurrent.locks.ReentrantLock;
 2 /**
 3  * Created by zhengbinMac on 2017/3/2.
 4  */
 5 public class FairLock implements Runnable{
 6     public static ReentrantLock fairLock = new ReentrantLock(true);
 7 
 8     public void run() {
 9         while (true) {
10             try {
11                 fairLock.lock();
12                 System.out.println(Thread.currentThread().getName()+",获得锁!");
13             }finally {
14                 fairLock.unlock();
15             }
16         }
17     }
18     public static void main(String[] args) {
19         FairLock fairLock = new FairLock();
20         Thread t1 = new Thread(fairLock, "线程1");
21         Thread t2 = new Thread(fairLock, "线程2");
22         t1.start();t2.start();
23     }
24 }
View Code

修改重入锁是否公平,观察输出结果,如果公平,输出结果始终为两个线程交替的获得锁,如果是非公平,输出结果为一个线程占用锁很长时间,然后才会释放锁,另个线程才能执行。

引出第二个问题:为什么公平锁例子中出现,公平锁线程是不断切换的,而非公平锁出现同一线程连续获取锁的情况?

结合源码再看“重入”

何为重进入(重入)?

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

  • 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
  • 锁的最终释放。线程重复 n 次获取了锁,随后在第 n 次释放该锁后,其它线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于 0 时表示锁已经成功释放。

以非公平锁源码分析:

获取:

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) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

acquireQueued 方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程,来决定获取操作是否成功,如果获取锁的线程再次请求,则将同步状态值进行增加并返回 true,表示获取同步状态成功。
成功获取锁的线程再次获取锁,只是增加了同步状态值,也就是要求 ReentrantLock 在释放同步状态时减少同步状态值,释放锁源码如下:

public void unlock() {sync.release(1);
}
public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
}
protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;
}

如果锁被获取 n 次,那么前 (n-1) 次 tryRelease(int releases) 方法必须返回 false,只有同步状态完全释放了,才能返回 true。该方法将同步状态是否为 0 作为最终释放的条件,当同步状态为 0 时,将占有线程设置为 null,并返回 true,表示释放成功。

通过对获取与释放的分析,就可以解释,以上两个例子中出现的两个问题:为什么 ReentrantLock 锁能够支持一个线程对资源的重复加锁?为什么公平锁例子中出现,公平锁线程是不断切换的,而非公平锁出现同一线程连续获取锁的情况?

  • 为什么支持重复加锁?因为源码中用变量 c 来保存当前锁被获取了多少次,故在释放时,对 c 变量进行减操作,只有 c 变量为 0 时,才算锁的最终释放。所以可以 lock 多次,同时 unlock 也必须与 lock 同样的次数。
  • 为什么非公平锁出现同一线程连续获取锁的情况?tryAcquire 方法中增加了再次获取同步状态的处理逻辑。

小结

对上面ReentrantLock的几个重要方法整理如下:

  • lock():获得锁,如果锁被占用,进入等待。
  • lockInterruptibly():获得锁,但优先响应中断。
  • tryLock():尝试获得锁,如果成功,立即放回 true,反之失败返回 false。该方法不会进行等待,立即返回。
  • tryLock(long time, TimeUnit unit):在给定的时间内尝试获得锁。
  • unLock():释放锁。

对于其实现原理,下篇博文将详细分析,其主要包含三个要素:

  • 原子状态:原子状态有 CAS(compareAndSetState) 操作来存储当前锁的状态,判断锁是否有其他线程持有。
  • 等待队列:所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统才能够从等待队列中唤醒一个线程,继续工作。详见:队列同步器——AQS(待更新)
  • 阻塞原语 park() 和 unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。关于阻塞原语,详见:线程阻塞工具类——LockSupport(待更新)。

参考资料

[1] Java并发编程的艺术, 5.3 - 重入锁

[2] 实战Java高并发程序设计, 3.1.1 - synchronized的功能扩展:重入锁

转载于:https://www.cnblogs.com/zhengbin/p/6503412.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/269893.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端:nodejs版本管理神器nvm软件使用笔记

目录 1、nvm简介 2、nvm的作用 3、nvm下载安装 4、整理常用的nvm命令 使用vue框架开发的朋友可能会遇到首次运行公司项目环境的时候&#xff0c;会出现使用npm install命令安装依赖包的时候出现各种各样的问题&#xff0c;其中很重要的一个错误原因就是因为你的nodejs版本和当时…

NetCore实战:基于html生成pdf文件案例讲解

目录 一、WkHtmlToPdfDotNet介绍 二、安装 三、运行效果 一、WkHtmlToPdfDotNet介绍 WkHtmlToPdfDotNet是基于本地 wkhtmltopdf封装的.NET Core类库&#xff0c;主要通过webkit引擎实现html页面转换为pdf文件。并且支持在Windows、Docker、Linux、MacOSX运行。 主要功能实现在线…

struts2+spring3+hibernate3+mysql简单登录实现

1.导入相关的jar包 2.建立数据库 1 create table account( 2 id int(10), 3 user varchar(50), 4 paw varchar(50) 5 ); 6 insert into account values(1,admin,admin); 3.建立包结构 4.配置文件的配置及代码 4.1 数据库配置文件&#xff1a;db.properties 1 #jdbc 2 jdbc.driv…

Spring事务管理嵌套事务详解 : 同一个类中,一个方法调用另外一个有事务的方法

Spring 事务机制回顾 Spring事务一个被讹传很广说法是&#xff1a;一个事务方法不应该调用另一个事务方法&#xff0c;否则将产生两个事务. 结果造成开发人员在设计事务方法时束手束脚&#xff0c;生怕一不小心就踩到地雷。 其实这是不认识Spring事务传播机制而造成的误解&…

java多线程一览

线程概述:多线程的目的,不是提高程序的执行速度,而是提高程序的使用率(能抢到CPU的可能比较大). 因为线程是CPU调度的基本单位,所以,当一个程序的线程较多的时候就更容易抢到cpu的资源进程: 运行中的程序,是系统进行资源分配和调度的独立单位每个进程都有他自己的内存空间和系统…

人工智能:PyTorch深度学习框架介绍

目录 1、PyTorch 2、PyTorch常用的工具包 3、PyTorch特点 4、PyTorch不足之处 今天给大家讲解一下PyTorch深度学习框架的一些基础知识&#xff0c;希望对大家理解PyTorch有一定的帮助&#xff01; 1、PyTorch PyTorch是一个基于Torch的Python机器学习框架。它是由Facebook的人工…

Linux下安装配置MySQL

一、删除原来的MySQL 在安装前要先确定系统是否已经安装了其他版本的MySQL&#xff0c;如已安装其他版本的MySQL&#xff0c;需先删除后再安装新版本。 1. 执行yum命令&#xff0c;删除MySQL的lib库&#xff0c;服务文件 yum remove mysql mysql-server mysql-libs mysql-serve…

WebSocket 是什么原理?为什么可以实现持久连接?什么情况使用WebSocket

作者&#xff1a;Ovear链接&#xff1a;https://www.zhihu.com/question/20215561/answer/40316953来源&#xff1a;知乎著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。额。。最高票答案没答到点子上&#xff0c;最后怎么跑到Nodejs上去了。…

【Swift学习笔记-《PRODUCT》读书记录-实现自定义转场动画】

iOS默认的push动画是把即将展示的控制器从右边推过来。有时我们想实现类似PPT中的一些动画&#xff0c;这时候就需要自定义转场动画了。如下图我们想实现一个淡出并且放大的过场动画&#xff0c;在退出时是一个淡出缩小的动画。 首先需要自定义一个类DiaryAnimator.swift遵守 U…

【JZOJ3598】【CQOI2014】数三角形

Mission 对于100%的数据1<m,n<1000 Solution 鬼题&#xff0c;ansC3(n∗m)−Ans&#xff0c;其中Ans表示三点共线的数目&#xff1b; 枚举最长边的向量(x,y)&#xff0c;容易算出贡献及个数。 Code #include<iostream> #include<stdio.h> #include<algor…

NSTimer定时器进阶——详细介绍,循环引用分析与解决

引言 定时器&#xff1a;A timer waits until a certain time interval has elapsed and then fires, sending a specified message to a target object. 翻译如下&#xff1a;在固定的时间间隔被触发&#xff0c;然后给指定目标发送消息。总结为三要素吧&#xff1a;时间间隔、…

HTML - 超文本标记语言 (Hyper Text Markup Language)

HTML - 超文本标记语言 (Hyper Text Markup Language) HTML是建设网站/网页制作主要语言。 HTML是一种易于学习的标记语言。 HTML使用像 <p> 尖括号内标记标签来定义网页的内容&#xff1a; HTML 实例 <html><body><h1>My First Heading</h1><…

AOP切入同类调用方法不起作用,AopContext.currentProxy()帮你解决这个坑

原来在springAOP的用法中&#xff0c;只有代理的类才会被切入&#xff0c;我们在controller层调用service的方法的时候&#xff0c;是可以被切入的&#xff0c;但是如果我们在service层 A方法中&#xff0c;调用B方法&#xff0c;切点切的是B方法&#xff0c;那么这时候是不会切…