当开发者在应用中使用了并发来提升性能的同时,开发者也需要注意线程之间有可能会相互阻塞。当整个应用执行的速度比预期要慢的时候,也就是应用没有按照预期的执行时间执行完毕。在本章中,我们来需要仔细分析可能会影响应用多线程的活性问题。

死锁

死锁的概念在软件开发者中已经广为熟知了,甚至普通的计算机用户也会经常使用这个概念,尽管不是在正确的状况下使用。严格来说,死锁意味着两个或者更多线程在等待另一个线程释放其锁定的资源,而请求资源的线程本身也锁定了对方线程所请求的资源。如下:

Thread 1: locks resource A, waits for resource B
Thread 2: locks resource B, waits for resource A

为了更好的理解问题,参考一下如下的代码:

public class Deadlock implements Runnable {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
private final Random random = new Random(System.currentTimeMillis());
public static void main(String[] args) {
Thread myThread1 = new Thread(new Deadlock(), "thread-1");
Thread myThread2 = new Thread(new Deadlock(), "thread-2");
myThread1.start();
myThread2.start();
} public void run() {
for (int i = 0; i < 10000; i++) {
boolean b = random.nextBoolean();
if (b) {
System.out.println("[" + Thread.currentThread().getName() +
"] Trying to lock resource 1.");
synchronized (resource1) {
System.out.println("[" + Thread.currentThread().
getName() + "] Locked resource 1.");
System.out.println("[" + Thread.currentThread().
getName() + "] Trying to lock resource 2.");
synchronized (resource2) {
System.out.println("[" + Thread.
currentThread().getName() + "] Locked
resource 2.");
}
}
} else {
System.out.println("[" + Thread.currentThread().getName() +
"] Trying to lock resource 2.");
synchronized (resource2) {
System.out.println("[" + Thread.currentThread().
getName() + "] Locked resource 2.");
System.out.println("[" + Thread.currentThread().
getName() + "] Trying to lock resource 1.");
synchronized (resource1) {
System.out.println("[" + Thread.
currentThread().getName() + "] Locked
resource 1.");
}
}
}
}
}
}

从上面的代码中可以看出,两个线程分别启动,并且尝试锁定2个静态的资源。但对于死锁,我们需要两个线程的以不同顺序锁定资源,因此我们利用随机实例选择线程要首先锁定的资源。

如果布尔变量btrueresource1会锁定,然后尝试去获得resource2的锁。如果bfalse,线程会优先锁定resource2,然而尝试锁定resource1。程序不用一会儿就会碰到死锁问题,然后就会一直挂住,直到我们结束了JVM才会结束:

[thread-1] Trying to lock resource 1.
[thread-1] Locked resource 1.
[thread-1] Trying to lock resource 2.
[thread-1] Locked resource 2.
[thread-2] Trying to lock resource 1.
[thread-2] Locked resource 1.
[thread-1] Trying to lock resource 2.
[thread-1] Locked resource 2.
[thread-2] Trying to lock resource 2.
[thread-1] Trying to lock resource 1.

在上面的执行中,thread-1持有了resource2的锁,等待resource1的锁,而线程thread-2持有了resource1的锁,等待resource2的锁。

如果我们将b的值配置true或者false的话,是不会碰到死锁的,因为执行的顺序始终是一致的,那么thread-1thread-2请求锁的顺序始终是一致的。两个线程都会以同样的顺序请求锁,那么最多会暂时阻塞一个线程,最终都能够顺序执行。

大概来说,造成死锁需要如下的一些条件:

  • 互斥:必须存在一个资源在某个时刻,仅能由一个线程访问。
  • 资源持有:当锁定了一个资源的时候,线程仍然需要去获得另外一个资源的锁。
  • 没有抢占策略:当某个线程已经持有了资源一段时间的时候,没有能够强占线程锁定资源的机制。
  • 循环等待:在运行时必须存在两个或者更多的线程,相互请求对方锁定的资源。

尽管产生死锁的条件看起来较多,但是在多线程应用中存在死锁还是比较常见的。开发者可以通过打破死锁构成的必要条件来避免死锁的产生,参考如下:

  • 互斥: 这个需求通常来说是不可避免的,资源很多时候确实只能互斥访问的。但是并不是总是这样的。当使用DBMS系统的时候,可能使用类似乐观锁的方式来代替原来的悲观锁的机制(在更新数据的时候锁定表中的一行)。
  • 还有一种可行的方案,就是对资源持有进行处理,当获取了某一资源的锁之后,立刻获取其他所必须资源的锁,如果获取锁失败了,则释放掉之前所有的互斥资源。当然,这种方式并不是总是可以的,有可能锁定的资源之前是无法知道的,或者是废弃了的资源。
  • 如果锁不能立刻获取,防止出现死锁的一种方式就是给锁的获取配置上一个超时时间。在SDK类中的ReentrantLock就提供了类似超时的方法。
  • 从上面的代码中,我们可以发现,如果每个线程的锁定资源的顺序是相同的,是不会产生死锁的。而这个过程可以通过将所有请求锁的代码都抽象到一个方法,然后由线程调用来实现。这就可以有效的避免死锁。

在一个更高级的应用中,开发者或许需要考虑实现一个检测死锁的系统。在这个系统中,来实现一些基于线程的监控,当前程获取一个锁,并且尝试请求别的锁的时候,都记录日志。如果以线程和锁构成有向图,开发者是能够检测到2不同的线程持有资源并且同时请求另外的阻塞的资源的。如果开发者可以检测,并能够强制阻塞的线程释放掉已经获取的资源,就能够自动检测到死锁并且自动修复死锁问题。

饥饿

线程调度器会决定哪一个处于RUNNABLE状态的线程会的执行顺序。决定一般是基于线程的优先级的;因此,低优先级的线程会获得较少的CPU时间,而高优先级的线程会获得较多的CPU时间。当然,这种调度听起来较为合理,但是有的时候也会引起问题。如果总是执行高优先级的线程,那么低优先级的线程就会无法获得足够的时间来执行,处于一种饥饿状态。因此,建议开发者只在真的十分必要的时候才去配置线程的优先级。

一个很复杂的线程饥饿的例子就是finalize()方法。Java语言中的这一特性可以用来进行垃圾回收,但是当开发者查看一下finalizer线程的优先级,就会发现其运行的优先级不是最高的。因此,很有可能finalize()方法跟其他方法比起来会执行更久。

另一个执行时间的问题是,线程以何种顺序通过同步代码块是没有定义的。当很多并行线程需要通过封装的同步代码块时,会有的线程等待的时间要比其它线程的时间更久才能进入同步代码快。理论上,他们可能永远无法进入代码块。这个问题可以使用公平锁的方案来解决。公平锁在选择下个线程的时候会考虑到线程的等待时间。其中一个公平锁的实现就是java.util.concurrent.locks.ReentrantLock:

如果使用ReentrantLock的如下构造函数:

    /**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

传入true,那么ReentrantLock是一个公平锁,是会允许线程按挂起顺序来依次获取锁执行的。这样可以削减线程的饥饿,但是,并不能完全解决饥饿的问题,毕竟线程的调度是由操作系统调度的。所以,ReentrantLock类只考虑等待锁的线程,调度上是无法起作用的。举个例子,尽管使用了公平锁,但是操作系统会给低优先级的线程很短的执行时间。

Java线程和多线程(十五)——线程的活性的更多相关文章

  1. JAVA之旅(十五)——多线程的生产者和消费者,停止线程,守护线程,线程的优先级,setPriority设置优先级,yield临时停止

    JAVA之旅(十五)--多线程的生产者和消费者,停止线程,守护线程,线程的优先级,setPriority设置优先级,yield临时停止 我们接着多线程讲 一.生产者和消费者 什么是生产者和消费者?我们 ...

  2. 201871010104-陈园园 《面向对象程序设计(java)》第十五周学习总结

    201871010104-陈园园 <面向对象程序设计(java)>第十五周学习总结 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ ...

  3. 201871010128-杨丽霞《面向对象程序设计(java)》第十五周学习总结

    201871010128-杨丽霞<面向对象程序设计(java)>第十五周学习总结 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ ...

  4. Java 设计模式系列(十五)观察者模式(Observer)

    Java 设计模式系列(十五)观察者模式(Observer) Java 设计模式系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) Java ...

  5. Java 设计模式系列(十五)迭代器模式(Iterator)

    Java 设计模式系列(十五)迭代器模式(Iterator) 迭代器模式又叫游标(Cursor)模式,是对象的行为模式.迭代子模式可以顺序地访问一个聚集中的元素而不必暴露聚集的内部表象(interna ...

  6. 201271050130-滕江南-《面向对象程序设计(java)》第十五周学习总结

    201271050130-滕江南-<面向对象程序设计(java)>第十五周学习总结 博文正文开头格式:(2分) 项目 内容 这个作业属于哪个课程 https://www.cnblogs.c ...

  7. 201871010111-刘佳华《面向对象程序设计(java)》第十五周学习总结

    201871010111-刘佳华<面向对象程序设计(java)>第十五周学习总结 实验十三  Swing图形界面组件(二) 实验时间 2019-12-6 第一部分:理论知识总结 5> ...

  8. 201871010123-吴丽丽《面向对象程序设计(Java)》第十五周学习总结

    201871010123-吴丽丽<面向对象程序设计(Java)>第十五周学习总结 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ ...

  9. 201871010105-曹玉中《面向对象程序设计(java)》第十五周学习总结

    201871010105-曹玉中<面向对象程序设计(java)>第十五周学习总结 项目 内容 这个作业属于哪个过程 https://www.cnblogs.com/nwnu-daizh/ ...

  10. 201871010107-公海瑜《面向对象程序设计(java)》第十五周学习总结

    201871010107-公海瑜<面向对象程序设计(java)>第十五周学习总结             项目                            内容   这个作业属于 ...

随机推荐

  1. Codeup

    问题 I: 习题5-10 分数序列求和 时间限制: 1 Sec  内存限制: 12 MB提交: 611  解决: 537[提交][状态][讨论版][命题人:外部导入] 题目描述 有如下分数序列 求出次 ...

  2. WPF:完美自定义MeaagseBox 动画 反弹 背景模糊 扁平化

    不知道为什么,WPF的MeaageBox的风格还停留在Win 2000的风格... 很久前就想自己封装一个MessageBox出来,但是都只是简单的封装,不怎么具有通用性.这次终于搞完了. 使用方法和 ...

  3. 利用Selenium爬取淘宝商品信息

    一.  Selenium和PhantomJS介绍 Selenium是一个用于Web应用程序测试的工具,Selenium直接运行在浏览器中,就像真正的用户在操作一样.由于这个性质,Selenium也是一 ...

  4. DOIS 2018 — OneAPM 蓝海讯通以数据为中心的 AIOps 平台亮相

    AIOps 近两年开始在运维圈子里面火了起来.一夜间传统和新兴的运维管理软件供应商,IT 运维系统开发商,大数据厂商,人工智能算法提供商,还有 BAT,大家都在谈论这个话题.在短短的不到 1 年时间中 ...

  5. linux下objdump应用

    <a href="http://www.maomao365.com/?p=952" > linux命令objdump的用法  http://www.maomao365. ...

  6. 批量配置SSH互信脚本

    在大规模自动化部署时我们常常需要配置好服务器的SSH互信,以便自动化脚本可以免密登录远程服务器,常规的手动配置SSH互信步骤如下: 使用ssh-keygen生成本地ssh key(mha01),生成的 ...

  7. timeout 命令

    命令简介 运行指定的命令,如果在指定时间后仍在运行,则杀死该进程.用来控制程序运行的时间. 使用方法 1 2 3 timeout [选项] 数字[后缀] 命令 [参数]... 后缀 s 代表秒(默认值 ...

  8. 4.4Python数据处理篇之Matplotlib系列(四)---plt.bar()与plt.barh条形图

    目录 目录 前言 (一)竖值条形图 (二)水平条形图 1.使用bar()绘制: 2.使用barh()绘制: (三)复杂的条形图 1.并列条形图: 2.叠加条形图: 3.添加图例于数据标签的条形图: 目 ...

  9. java实现支付宝支付及退款(一)

    本篇博客主要做支付宝支付的准备工作(注册沙箱.natapp内网穿透等操作).具体代码实现操作请看下篇博客 一.登录沙箱 1.登录蚂蚁金服开发平台: https://open.alipay.com/pl ...

  10. 团队作业—预则立&&他山之石

    团队作业-预则立&&他山之石 Deadline: 2017.10.24 22:00pm 作业要求 一.确立团队选题,建立和初步熟悉团队git的协作方式.项目后续的代码.文档都要通过gi ...