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

死锁

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

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. Python_基于Python同Linux进行交互式操作实现通过堡垒机访问目标机

    基于Python同Linux进行交互式操作实现通过堡垒机访问目标机   by:授客 QQ:1033553122 欢迎加入全国软件测试交流群:7156436 实现功能 1 测试环境 1 代码实践 2 注 ...

  2. Android为TV端助力 fragment 的用法以及与activity的交互和保存数据的方法,包括屏幕切换(转载)!

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/37992017 1.管理Fragment回退栈 类似与Android系统为Acti ...

  3. springboot 学习之路 4(日志输出)

    目录:[持续更新.....] spring 部分常用注解 spring boot 学习之路1(简单入门) spring boot 学习之路2(注解介绍) spring boot 学习之路3( 集成my ...

  4. Django 2.0 URL新版配置介绍

    实例 先看一个例子: from django.urls import path from . import views urlpatterns = [ path('articles/2003/', v ...

  5. java传输文件的简单方法

    假设现在已经打包了一个文件(1233444333),要将这个文件传输给另一方: package file; import java.io.*; public class F_PasswordUnPas ...

  6. DBA思考系列——学会拒绝不合理的需求

    DBA思考系列--学会拒绝不合理的需求   一直以来,个性都比较随意,一般很少拒绝开发人员的一些需求(有点老好人的感觉). 这点一直被老大诟病,也一直在反省!最近又有一件事情,让我觉得:应该学会拒绝不 ...

  7. FileInputStream、FileReader、FileWriter和File

    FileInputStream提供了对文件的字节读取 用于读取诸如图像数据之类的原始字节流       如:FileInputStream fis=new FileInputStream(new Fi ...

  8. Linux没有最小只有更小----迷你Linux版本大集合(转)

    [自从去年到现在已经收集了上百种版本的Linux和Unix,至于Unix就不想说了,没有Linux的功底是很难驾驭Unix的,我在这里只把小于360M的Linux以及一些非Linux但是很像Linux ...

  9. 大杀器:VS2017 查看或调试liunx代码(转载)

    From:https://blog.csdn.net/mumufan05/article/details/80094637 上一篇简单介绍了vs2017新建一个linux的工程,本编将介绍一下如何管理 ...

  10. 智能合约 solidity 开发的环境基本搭建

    以太坊Dapp开发快速入门 以太坊为开源社区,虽然设计东西都很优秀,但是组件十分的杂乱,因此下面首先简单介绍下以太坊的一些常用组件以及各种工具介绍 Geth Geth是由以太坊基金会提供的官方客户端软 ...