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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java线程和多线程(十五)——线程的活性相关的知识,希望对你有一定的参考价值。

当开发人员在应用中使用了并发来提升性能的同一时候。开发人员也须要注意线程之间有可能会相互堵塞。

当整个应用运行的速度比预期要慢的时候,也就是应用没有依照预期的运行时间运行完成。在本章中。我们来须要细致分析可能会影响应用多线程的活性问题。

死锁

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

例如以下:

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线程和多线程(十五)——线程的活性的主要内容,如果未能解决你的问题,请参考以下文章

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

并发和多线程(十五)--AbstractQueuedSynchronizer共享锁和Condition条件队列

多个请求是多线程吗

python中的多线程和多进程编程

Java进阶泛型和多线程

什么是多线程,多进程?