轮询锁使用时遇到的问题与解决方案!

作者 | 王磊

来源 | Java中文社群(ID:javacn666)

转载请联系授权(微信ID:GG_Stone

当我们遇到死锁之后,除了可以手动重启程序解决之外,还可以考虑是使用顺序锁和轮询锁,这部分的内容可以参考我的上一篇文章,这里就不再赘述了。然而,轮询锁在使用的过程中,如果使用不当会带来新的严重问题,所以本篇我们就来了解一下这些问题,以及相应的解决方案。

问题演示

当我们没有使用轮询锁之前,可能会出现这样的问题:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class DeadLockByReentrantLock {public static void main(String[] args) {Lock lockA = new ReentrantLock(); // 创建锁 ALock lockB = new ReentrantLock(); // 创建锁 B// 创建线程 1Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {lockA.lock(); // 加锁System.out.println("线程 1:获取到锁 A!");try {Thread.sleep(1000);System.out.println("线程 1:等待获取 B...");lockB.lock(); // 加锁try {System.out.println("线程 1:获取到锁 B!");} finally {lockA.unlock(); // 释放锁}} catch (InterruptedException e) {e.printStackTrace();} finally {lockA.unlock(); // 释放锁}}});t1.start(); // 运行线程// 创建线程 2Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {lockB.lock(); // 加锁System.out.println("线程 2:获取到锁 B!");try {Thread.sleep(1000);System.out.println("线程 2:等待获取 A...");lockA.lock(); // 加锁try {System.out.println("线程 2:获取到锁 A!");} finally {lockA.unlock(); // 释放锁}} catch (InterruptedException e) {e.printStackTrace();} finally {lockB.unlock(); // 释放锁}}});t2.start(); // 运行线程}
}

以上代码的执行结果如下:

img

从上述结果可以看出,此时程序中出现了线程相互等待,并尝试获取对方(锁)资源的情况,这就是典型的死锁问题了。

简易版轮询锁

当出现死锁问题之后,我们就可以使用轮询锁来解决它了,它的实现思路是通过轮询的方式来获取多个锁,如果中途有任意一个锁获取失败,则执行回退操作,释放当前线程拥有的所有锁,等待下一次重新执行,这样就可以避免多个线程同时拥有并霸占锁资源了,从而直接解决了死锁的问题,简易版的轮询锁实现如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class SolveDeadLockExample2 {public static void main(String[] args) {Lock lockA = new ReentrantLock(); // 创建锁 ALock lockB = new ReentrantLock(); // 创建锁 B// 创建线程 1(使用轮询锁)Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {// 调用轮询锁pollingLock(lockA, lockB);}});t1.start(); // 运行线程// 创建线程 2Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {lockB.lock(); // 加锁System.out.println("线程 2:获取到锁 B!");try {Thread.sleep(1000);System.out.println("线程 2:等待获取 A...");lockA.lock(); // 加锁try {System.out.println("线程 2:获取到锁 A!");} finally {lockA.unlock(); // 释放锁}} catch (InterruptedException e) {e.printStackTrace();} finally {lockB.unlock(); // 释放锁}}});t2.start(); // 运行线程}/*** 轮询锁*/private static void pollingLock(Lock lockA, Lock lockB) {// 轮询锁while (true) {if (lockA.tryLock()) { // 尝试获取锁System.out.println("线程 1:获取到锁 A!");try {Thread.sleep(1000);System.out.println("线程 1:等待获取 B...");if (lockB.tryLock()) { // 尝试获取锁try {System.out.println("线程 1:获取到锁 B!");} finally {lockB.unlock(); // 释放锁System.out.println("线程 1:释放锁 B.");break;}}} catch (InterruptedException e) {e.printStackTrace();} finally {lockA.unlock(); // 释放锁System.out.println("线程 1:释放锁 A.");}}// 等待一秒再继续执行try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

以上代码的执行结果如下:

img

从上述结果可以看出,当我们在程序中使用轮询锁之后就不会出现死锁的问题了,但以上轮询锁也并不是完美无缺的,下面我们来看看这个轮询锁会有什么样的问题?

问题1:死循环

以上简易版的轮询锁,如果遇到有一个线程一直霸占或者长时间霸占锁资源的情况,就会导致这个轮询锁进入死循环的状态,它会尝试一直获取锁资源,这样就会造成新的问题,带来不必要的性能开销,具体示例如下。

反例

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class SolveDeadLockExample {public static void main(String[] args) {Lock lockA = new ReentrantLock(); // 创建锁 ALock lockB = new ReentrantLock(); // 创建锁 B// 创建线程 1(使用轮询锁)Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {// 调用轮询锁pollingLock(lockA, lockB);}});t1.start(); // 运行线程// 创建线程 2Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {lockB.lock(); // 加锁System.out.println("线程 2:获取到锁 B!");try {Thread.sleep(1000);System.out.println("线程 2:等待获取 A...");lockA.lock(); // 加锁try {System.out.println("线程 2:获取到锁 A!");} finally {lockA.unlock(); // 释放锁}} catch (InterruptedException e) {e.printStackTrace();} finally {// 如果此处代码未执行,线程 2 一直未释放锁资源// lockB.unlock(); }}});t2.start(); // 运行线程}/*** 轮询锁*/public static void pollingLock(Lock lockA, Lock lockB) {while (true) {if (lockA.tryLock()) { // 尝试获取锁System.out.println("线程 1:获取到锁 A!");try {Thread.sleep(1000);System.out.println("线程 1:等待获取 B...");if (lockB.tryLock()) { // 尝试获取锁try {System.out.println("线程 1:获取到锁 B!");} finally {lockB.unlock(); // 释放锁System.out.println("线程 1:释放锁 B.");break;}}} catch (InterruptedException e) {e.printStackTrace();} finally {lockA.unlock(); // 释放锁System.out.println("线程 1:释放锁 A.");}}// 等待一秒再继续执行try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

以上代码的执行结果如下:

img

从上述结果可以看出,线程 1 轮询锁进入了死循环的状态。

优化版

针对以上死循环的情况,我们可以改进的思路有以下两种:

  1. 添加最大次数限制:如果经过了 n 次尝试获取锁之后,还未获取到锁,则认为获取锁失败,执行失败策略之后终止轮询(失败策略可以是记录日志或其他操作);

  2. 添加最大时长限制:如果经过了 n 秒尝试获取锁之后,还未获取到锁,则认为获取锁失败,执行失败策略之后终止轮询。

以上策略任选其一就可以解决死循环的问题,出于实现成本的考虑,我们可以采用轮询最大次数的方式来改进轮询锁,具体实现代码如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class SolveDeadLockExample {public static void main(String[] args) {Lock lockA = new ReentrantLock(); // 创建锁 ALock lockB = new ReentrantLock(); // 创建锁 B// 创建线程 1(使用轮询锁)Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {// 调用轮询锁pollingLock(lockA, lockB, 3);}});t1.start(); // 运行线程// 创建线程 2Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {lockB.lock(); // 加锁System.out.println("线程 2:获取到锁 B!");try {Thread.sleep(1000);System.out.println("线程 2:等待获取 A...");lockA.lock(); // 加锁try {System.out.println("线程 2:获取到锁 A!");} finally {lockA.unlock(); // 释放锁}} catch (InterruptedException e) {e.printStackTrace();} finally {// 线程 2 忘记释放锁资源// lockB.unlock(); // 释放锁}}});t2.start(); // 运行线程}/*** 轮询锁** maxCount:最大轮询次数*/public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {// 轮询次数计数器int count = 0;while (true) {if (lockA.tryLock()) { // 尝试获取锁System.out.println("线程 1:获取到锁 A!");try {Thread.sleep(1000);System.out.println("线程 1:等待获取 B...");if (lockB.tryLock()) { // 尝试获取锁try {System.out.println("线程 1:获取到锁 B!");} finally {lockB.unlock(); // 释放锁System.out.println("线程 1:释放锁 B.");break;}}} catch (InterruptedException e) {e.printStackTrace();} finally {lockA.unlock(); // 释放锁System.out.println("线程 1:释放锁 A.");}}// 判断是否已经超过最大次数限制if (count++ > maxCount) {// 终止循环System.out.println("轮询锁获取失败,记录日志或执行其他失败策略");return;}// 等待一秒再继续尝试获取锁try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

以上代码的执行结果如下:

img

从以上结果可以看出,当我们改进之后,轮询锁就不会出现死循环的问题了,它会尝试一定次数之后终止执行。

问题2:线程饿死

我们以上的轮询锁的轮询等待时间是固定时间,如下代码所示:

// 等待 1s 再尝试获取(轮询)锁
try {Thread.sleep(1000);
} catch (InterruptedException e) {e.printStackTrace();
}

这样在特殊情况下会造成线程饿死的问题,也就是轮询锁一直获取不到锁的问题,比如以下示例。

反例

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class SolveDeadLockExample {public static void main(String[] args) {Lock lockA = new ReentrantLock(); // 创建锁 ALock lockB = new ReentrantLock(); // 创建锁 B// 创建线程 1(使用轮询锁)Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {// 调用轮询锁pollingLock(lockA, lockB, 3);}});t1.start(); // 运行线程// 创建线程 2Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {while (true) {lockB.lock(); // 加锁System.out.println("线程 2:获取到锁 B!");try {System.out.println("线程 2:等待获取 A...");lockA.lock(); // 加锁try {System.out.println("线程 2:获取到锁 A!");} finally {lockA.unlock(); // 释放锁}} finally {lockB.unlock(); // 释放锁}// 等待一秒之后继续执行try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}});t2.start(); // 运行线程}/*** 轮询锁*/public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {// 循环次数计数器int count = 0;while (true) {if (lockA.tryLock()) { // 尝试获取锁System.out.println("线程 1:获取到锁 A!");try {Thread.sleep(100); // 等待 0.1s(获取锁需要的时间)System.out.println("线程 1:等待获取 B...");if (lockB.tryLock()) { // 尝试获取锁try {System.out.println("线程 1:获取到锁 B!");} finally {lockB.unlock(); // 释放锁System.out.println("线程 1:释放锁 B.");break;}}} catch (InterruptedException e) {e.printStackTrace();} finally {lockA.unlock(); // 释放锁System.out.println("线程 1:释放锁 A.");}}// 判断是否已经超过最大次数限制if (count++ > maxCount) {// 终止循环System.out.println("轮询锁获取失败,记录日志或执行其他失败策略");return;}// 等待一秒再继续尝试获取锁try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

以上代码的执行结果如下:

img

从上述结果可以看出,线程 1(轮询锁)一直未成功获取到锁,造成这种结果的原因是:线程 1 每次轮询的等待时间为固定的 1s,而线程 2 也是相同的频率,每 1s 获取一次锁,这样就会导致线程 2 会一直先成功获取到锁,而线程 1 则会一直处于“饿死”的情况,执行流程如下图所示:

img

优化版

接下来,我们可以将轮询锁的固定等待时间,改进为固定时间 + 随机时间的方式,这样就可以避免因为获取锁的频率一致,而造成轮询锁“饿死”的问题了,具体实现代码如下:

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class SolveDeadLockExample {private static Random rdm = new Random();public static void main(String[] args) {Lock lockA = new ReentrantLock(); // 创建锁 ALock lockB = new ReentrantLock(); // 创建锁 B// 创建线程 1(使用轮询锁)Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {// 调用轮询锁pollingLock(lockA, lockB, 3);}});t1.start(); // 运行线程// 创建线程 2Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {while (true) {lockB.lock(); // 加锁System.out.println("线程 2:获取到锁 B!");try {System.out.println("线程 2:等待获取 A...");lockA.lock(); // 加锁try {System.out.println("线程 2:获取到锁 A!");} finally {lockA.unlock(); // 释放锁}} finally {lockB.unlock(); // 释放锁}// 等待一秒之后继续执行try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}});t2.start(); // 运行线程}/*** 轮询锁*/public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {// 循环次数计数器int count = 0;while (true) {if (lockA.tryLock()) { // 尝试获取锁System.out.println("线程 1:获取到锁 A!");try {Thread.sleep(100); // 等待 0.1s(获取锁需要的时间)System.out.println("线程 1:等待获取 B...");if (lockB.tryLock()) { // 尝试获取锁try {System.out.println("线程 1:获取到锁 B!");} finally {lockB.unlock(); // 释放锁System.out.println("线程 1:释放锁 B.");break;}}} catch (InterruptedException e) {e.printStackTrace();} finally {lockA.unlock(); // 释放锁System.out.println("线程 1:释放锁 A.");}}// 判断是否已经超过最大次数限制if (count++ > maxCount) {// 终止循环System.out.println("轮询锁获取失败,记录日志或执行其他失败策略");return;}// 等待一定时间(固定时间 + 随机时间)之后再继续尝试获取锁try {Thread.sleep(300 + rdm.nextInt(8) * 100); // 固定时间 + 随机时间} catch (InterruptedException e) {e.printStackTrace();}}}
}

以上代码的执行结果如下:

img

从上述结果可以看出,线程 1(轮询锁)加入随机等待时间之后就不会出现线程饿死的问题了。

总结

本文我们介绍了轮询锁的用途,用于解决死锁问题,但简易版的轮询锁在某些情况下会造成死循环和线程饿死的问题,因此我们对轮询锁进行了优化,给轮询锁加入了最大轮询次数,以及随机轮询等待时间,这样就可以解决因为引入轮询锁而造成的新问题了,这样就可以愉快的使用它来解决死锁的问题了。

参考 & 鸣谢

《Java并发编程实战》

---END---


原创并发文章推荐

1.线程的故事:我的3位母亲成就了优秀的我!

2.线程池的7种创建方式,强烈推荐你用它...

3.轻量级锁一定比重量级锁快吗?

4.这样终止线程,竟然会导致服务宕机?

5.漫画:如何证明sleep不释放锁,而wait释放锁?

6.池化技术到达有多牛?看了这个对比吓我一跳!

7.求求你,别再用wait和notify了!

8.Semaphore自白:限流器用我就对了!

9.CountDownLatch:别浪,等人齐再团!

10.CyclicBarrier:人齐了,老司机就发车了!

11.Java中用户线程和守护线程区别这么大?

12.ThreadLocal不好用?那是你没用对!

13.ThreadLocal内存溢出代码演示和原因分析!

14.SimpleDateFormat线程不安全的5种解决方案!

15.synchronized 加锁 this 和 class 的区别!

16.synchronized 优化手段之锁膨胀机制!

17.synchronized 中的 4 个优化,你知道几个?

18.ReentrantLock 中的 4 个坑!

19.图解:为什么非公平锁的性能更高?

20.死锁的 4 种排查工具 !

21.死锁终结者:顺序锁和轮询锁!


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

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

相关文章

DHCP 日志分析

DHCP 日志分析 DHCP(Dynamic Host Configuration Protocol,动态主机配置协议)是一种有效的IP 地址分配手段,现已经被广泛地应用在各种局域网管理。它能动态地向网络中每台计算机分配唯一的IP 地址,并提供安全、可靠、简…

Nginx For Windows Socket 端口转发

Nginx For Windows Socket 端口转发一、需求说明二、配置文件一、需求说明 使用Nginx进行端口转发 Socket 端口通信。 监控本地服务器的 3001 端口,转发到 10.73.60.48:3001 服务器上的 Socket 端口服务。 二、配置文件 完整配置文件如下,测试可用 #…

16 条 yyds 的代码规范

作者 | 涛姐涛哥链接 | cnblogs.com/taojietaoge/p/11575376.html背景:如何更规范化编写Java 代码的重要性想必毋需多言,其中最重要的几点当属提高代码性能、使代码远离Bug、令代码更优雅。一、MyBatis 不要为了多个查询条件而写 1 1当遇到多个查询条件…

linkedhashset_Java LinkedHashSet clear()方法与示例

linkedhashsetLinkedHashSet类的clear()方法 (LinkedHashSet Class clear() method) clear() method is available in java.util package. clear()方法在java.util包中可用。 clear() method is used to clear all of the existing objects in this LinkedHashSet. clear()方法…

C# 导出word文档及批量导出word文档(3)

在初始化WordHelper时,要获取模板的相对路径。获取文档的相对路径多个地方要用到,比如批量导出时要先保存文件到指定路径下,再压缩打包下载,所以专门写了个关于获取文档的相对路径的类。 1 #region 获取文档的相对路径2 pub…

Nginx For Windows 路由配置

Nginx For Windows 路由配置一、路由配置说明二、需求说明三、配置文件一、路由配置说明 使用Nginx进行路由配置。 使用过 SpringCloud 网关的同学都知道,网关可以使用同一IP地址、同一端口号、不同的服务ID,转发不同服务API信息,不会出现跨…

再见收费的 XShell,我改用国产良心工具!

使用或维护Linux系统的都知道,我们日常对服务器的操作,一般都会借助SSH工具远程登录到服务器之后进行操作。常用的SSH工具有不少,比如:Xshell、Putty、SSH Secure Shell Client、secureCRT等等。我使用过其中两种secureCRT和Xshel…

Java IdentityHashMap put()方法与示例

IdentityHashMap类的put()方法 (IdentityHashMap Class put() method) put() method is available in java.util package. put()方法在java.util包中可用。 put() method is used to set the given value element (val_ele) with the given key element (key_ele) when no valu…

全球六大国际域名解析量统计报告(6月25日)

IDC评述网(idcps.com)06月29日报道:根据DailyChanges公布的实时数据显示,在2015年6月25日,全球六大国际域名解析量总量持续攀升至153,246,819个,环比6月16日,净增46,078个,涨幅增大3…

Windows 创建符号链接

一、场景分析 1.环境变量 在Windows系统配置 环境变量 的时候,经常会遇到以下 路径 情况: C:\Program Files C:\Program Files (x86)\Common Files2.异常情况 这种路径中,存在空格字符,在一些程序调用时,可能出现异…

1.3w字,一文详解死锁!

作者 | 王磊来源 | Java中文社群(ID:javacn666)转载请联系授权(微信ID:GG_Stone死锁(Dead Lock)指的是两个或两个以上的运算单元(进程、线程或协程),都在等待…

Java Dictionary elements()方法与示例

字典类的elements()方法 (Dictionary Class elements() method) elements() method is available in java.util package. elements()方法在java.util包中可用。 elements() method is used to get the elements of an Enumeration in this dictionary. elements()方法用于获取此…

PHP与ThinkPHP读写文件

2019独角兽企业重金招聘Python工程师标准>>> 使用php将数据写入到指定的文件 $str"<?php return".var_export($phiz,true)."?>"; file_put_contents(./Data/phiz.php); 使用php读取指定的文件 …

软件版本号设置规则及示例

一、版本号设置规则 ABC客户端 版本说明书 [版本命名规则] 如&#xff1a;1.0.0.0 位数名称描述情景第一位主版本重大修改&#xff0c;重写或里程碑[里程碑] [重大更新]第二位次版本显著增强、功能变更较多[新增功能] [删减功能]第三位生成号功能变更&#xff0c;局部修正较多…

【图解】透彻Java线程状态转换

大家好&#xff0c;我是阿星&#xff0c;好久不见&#xff0c;欢迎来到Java并发编程系列番外篇线程状态转换&#xff0c;内容通俗易懂&#xff0c;请放心食用。线程状态先来个开场四连问Java线程状态有几个&#xff1f;Java线程状态是如何转换&#xff1f;Java线程状态转换什么…

Java Byte类的hashCode()方法及示例

短类hashCode()方法 (Short class hashCode() method) hashCode() method is available in java.lang package. hashCode()方法在java.lang包中可用。 hashCode() method is used to return hashcode of the Byte object. hashCode()方法用于返回Byte对象的哈希码。 hashCode()…

CentOS7安装Hadoop2.7完整流程

2019独角兽企业重金招聘Python工程师标准>>> 1、环境&#xff0c;3台CentOS7&#xff0c;64位&#xff0c;Hadoop2.7需要64位Linux&#xff0c;CentOS7 Minimal的ISO文件只有600M&#xff0c;操作系统十几分钟就可以安装完成&#xff0c; Master 192.168.0.182 Slav…

获取外网IP地址API

1、获取外网IP地址 地址&#xff1a; http://pv.sohu.com/cityjson?ieutf-8 返回结果&#xff1a; var returnCitySN {"cip": "39.***.***.***", "cid": "370000", "cname": "山东省"};2、获取IP地址详细信…

如果不这样用,Nacos也有安全问题!

前言配置管理作为软件开发中重要的一环&#xff0c;肩负着连接 代码和环境的职责&#xff0c;能很好的分离开发人员和维护人员的关注点。Nacos 的配置管理功能就很好地满足了云原生应用对于配置管理的需求&#xff1a;既能做到配置和代码分离&#xff0c;也能做到配置的动态…

java中intvalue_Java Byte类intValue()方法的示例

java中intvalue字节类intValue()方法 (Byte class intValue() method) intValue() method is available in java.lang package. intValue()方法在java.lang包中可用。 intValue() method is used to return the value denoted by this Byte object converted to type int (by c…