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

死锁

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

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 彩色Toast实现

    Android默认的Toast太丑了,我们来封装一个花里胡哨的Toast吧,就叫ColoredToast. Github:https://github.com/imcloudfloating/Desi ...

  2. [转] Vue生命周期

    Vue生命周期 这是Vue文档里关于实例生命周期的解释图 那么下面我们来进行测试一下 <section id="app-8"> {{data}} </sectio ...

  3. [译]在Linux上的提高MySQL/MariaDB安全性的12条建议

    MySQL 是世界上最流行的开源数据库系统,而MariaDB(MySQL的一个分支)是世界上发展最快的开源数据库系统.安装MySQL服务器之后,它的默认配置是不安全的,保护它是一般数据库管理中的基本任 ...

  4. Android Handler、Message、MessageQueue和Looper官方说明

    Handler官方说明 官方API文档:https://developer.android.google.cn/reference/android/os/Handler Handler允许您发送和处理 ...

  5. Wampserver或者帝国CMS安装后, 打开localhost显示IIS欢迎界面图片

    我们在安装集成环境Wampserver或者帝国CMS之后,有时会遇到一个问题, 打开localhost显示一张IIS欢迎界面图片,这个问题该如何解决呢,我在这里简单整理了一下解决方法 电脑win10系 ...

  6. [20181109]12cR2 的pre_page_sga参数

    [20181109]12cR2 的pre_page_sga参数.txt --//12CR2改变了参数pre_page_sga设置为True.设置为true有好处也有缺点.--//先看看官方的定义:ht ...

  7. shell编程—变量(三)

    在shell脚本中,变量分两种,系统变量和自定义变量. 系统默认变量是系统自带的一些变量,如path为路径变量 用户自定义变量为在编写吧脚本的时候自己定义的一些变量 变量名命名规则 首个字符必须为字母 ...

  8. mysql文件

    MySQL的每个数据库都对应存放在一个与数据库同名的文件夹中,MySQL数据库文件包括MySQL(server)所建数据库文件和MySQL(server)所用存储引擎创建的数据库文件. .frm文件: ...

  9. MyBatis动态SQL之一使用 if 标签和 choose标签

    bootstrap react https://segmentfault.com/a/1190000010383464 xml 中 < 转义 to thi tha <if test=&qu ...

  10. java 发邮件

    //先从oracle 下载mail.jarpackage test; import javax.mail.BodyPart; import javax.mail.Message; import jav ...