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

死锁

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

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. Android 四大组件之broadcast的理解

    Android广播的两种类型: 1.静态广播 2.动态广播 静态注册广播: Manifeast中的代码块: <receiver android:name=".broadcast.MyS ...

  2. [TensorFlow]Tensor维度理解

    http://wossoneri.github.io/2017/11/15/[Tensorflow]The-dimension-of-Tensor/ Tensor维度理解 Tensor在Tensorf ...

  3. elasticsearch及head插件安装与配置

    1. 环境软件版本说明 系统:ubuntu14.04.1 JDK:1.8 elasticsearch:5.5.2 node:9.11.1 elasticsearch:5.X 2. 环境软件下载说明 1 ...

  4. JS 文本框格式化

    页面: <script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script> & ...

  5. TURN Server Windows 安装程序

    有了OfficeSIP TURN Server 安装包,记录一下. http://www.onlinedown.net/soft/94746.htm 开源代码(C#)和应用地址:https://sou ...

  6. JSON Web Tokens简单学习

    JWT用于加密生成安全认证的令牌,存储登录验证成功的部分用户信息 一.安装JWT 二.加密 解密 代码 /*存储在加密字符串的信息*/ var payload = new Dictionary< ...

  7. python中封装、继承、多态

    又看到这个玩意,顺手写下来 面向对象三大特征: 封装:本质是将事物相关的属性和方法封装在一个类里面,我们调用类创建实例的时候,不用关心类内部的代码细节 继承:子类需要复用父类里面的属性或者方法,当然子 ...

  8. Vue学习之路6-条件渲染

    条件指令 所谓条件指令是指满足某个条件时执行哪部分代码,不满足条件时执行哪部分条件代码.vue条件指令有v-if,v-else-if,v-else三个,v-if条件渲染用来指示元素是否移除或者插入,根 ...

  9. 【算法】LeetCode算法题-Remove Element

    这是悦乐书的第150次更新,第152篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第9题(顺位题号是27).给定整数数组nums和值val,删除nums中所有的val值, ...

  10. 17.基于scrapy-redis两种形式的分布式爬虫

    redis分布式部署 1.scrapy框架是否可以自己实现分布式? - 不可以.原因有二. 其一:因为多台机器上部署的scrapy会各自拥有各自的调度器,这样就使得多台机器无法分配start_urls ...