Java线程和多线程(十五)——线程的活性

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

死锁

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

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

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

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个静态的资源。但对于死锁,咱们须要两个线程的以不一样顺序锁定资源,所以咱们利用随机实例选择线程要首先锁定的资源。markdown

若是布尔变量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请求锁的顺序始终是一致的。两个线程都会以一样的顺序请求锁,那么最多会暂时阻塞一个线程,最终都可以顺序执行。dom

大概来讲,形成死锁须要以下的一些条件:函数

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

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

  • 互斥: 这个需求一般来讲是不可避免的,资源不少时候确实只能互斥访问的。可是并非老是这样的。当使用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类只考虑等待锁的线程,调度上是没法起做用的。举个例子,尽管使用了公平锁,可是操做系统会给低优先级的线程很短的执行时间。

相关文章
相关标签/搜索