java 死锁和饥饿_[Java并发编程实战] 线程池的使用之饥饿死锁的发生

一屋不扫何以扫天下?———《后汉书·陈蕃传》

这句话的意思是,从一点一滴的小事开始积累,才能做成一番大事业。

Executor框架核心之一就是利用线程池,所以接下来这几篇,详细介绍线程池相关的高级选项以及注意事项。

任务间隐性耦合的说明

虽说 Exectuor 将任务和执行策略解耦,但是实际上言过其实了。假如任务之间存在某种相互依赖关系,其中一个任务必须依赖另外一个的执行,这就又产生某种程度上的耦合。像这些类型的任务,我们需要注意,需要明确地指定一个执行策略。比如下面这些任务都是需要注意的:

依赖性任务:任务之间相互依赖,隐形地给执行策略带来了约束,这样要求我们必须仔细的管理执行策略避免活跃度问题。比如,一个任务需等待另外一个耗时任务或者相互等待了,会产生什么样的结果?

采用线程限制的任务

对响应时间敏感的任务:将一个耗时的任务提交到单线程化的 Executor 中,或者将多个耗时的任务提交到只包含少量线程的线程池中,会削弱由 Executor 管理的服务的响应性。

使用 ThreadLocal 的任务:ThreadLocal 让每个线程可以保留一份变量的私有版本。Executor的是实现是:在需求不高时回收空闲线程,需求增加时添加新的线程,如果任务抛出异常,就会用一个全新的线程取代出错的那个。所以,只有当 ThreadLocal 值的生命周期被限制在当前任务中时,在池的某线程中使用 ThreadLocal 才有意义。在线程池中,不应该使用 ThreadLocal 传递任务间的数值。

所以结论就是,当任务是同类的,独立的时候,线程池才会发挥出最佳的作用。

另外:

如果将耗时的任务和短时任务混合在一起,除非线程池很大,否则会出现线程池拥堵,拖长服务时间,最差的情况是所有线程任务都在执行耗时任务。

如果提交的任务依赖于其他任务,除非线程池是无限的,否则会有产生死锁的风险。

线程饥饿死锁概念

在线程池中如果一个任务依赖于其他任务的执行,就可能产生死锁。在一个大的线程池中,如果所有线程执行的任务都阻塞在线程池中,等待着仍然处于同一队列中的其他任务,那么这就会发生线程饥饿死锁。换句话说,只要池任务开始了无限期阻塞,其目的是等待一些资源或条件,此时只有另一个池任务的执行才能使那些条件成立。除非能保证线程池足够大,否则会发生线程饥饿死锁。

下面举个线程饥饿死锁的,创建只有一个线程的线程池,用于串行执行任务。创建两个任务 Task1 和 Task2,其中 Task1 从队列中取出元素, Task2 向队列添加元素。其中,队列为阻塞队列,当队列为空时,Task1 将会一直阻塞等待 Task2 执行,但是此时只有一个线程只能执行一个任务,所以这个 Task1 将会永远阻塞,Task2 将永远无法执行。这就是任务之间相互依赖的饥饿死锁。

代码清单如下:

import java.util.concurrent.ArrayBlockingQueue;

import java.util.concurrent.BlockingQueue;

import java.util.concurrent.Callable;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class ThreadDeadLockTest{

//创建一个阻塞队列

private static BlockingQueue Q = new ArrayBlockingQueue(10);

//线程池线程数量

private static final int THREAD_SIZE = 1;

@SuppressWarnings("unchecked")

public static void main(String[] args) {

//创建一个固定线程的线程池

ExecutorService service = Executors.newFixedThreadPool(THREAD_SIZE);

service.submit(new Task1());

service.submit(new Task2(1));

service.shutdown();

}

//任务1取出阻塞队列的值并打印

static class Task1 implements Callable {

@Override

public Object call() throws Exception {

System.out.println("Task1 is running");

//取出阻塞队列的值,如果没有则会阻塞

int value = (int) Q.take();

System.out.println("Task1 finished, value = " + value);

return null;

}

}

//任务2,往阻塞队列增加元素

static class Task2 implements Callable {

private int val;

public Task2(int value) {

val = value;

}

@Override

public Object call() throws Exception {

System.out.println("Task2 put value = " + val);

//往阻塞队列增加元素

Q.put(1);

return null;

}

}

}

执行结果:

edaeaaf914ae59e45441566ae1783327.png

从这里看出,Task1 一直在运行并且没有结束,Task2 永远无法执行。这个例子就简单的说明了饥饿死锁发生的情况。

上面也说过,除非线程池足够大,才能避免饥饿死锁的发生。所以,我们把上面的代码的线程数量改为2:

THREAD_SIZE = 2;

执行结果如下:

f7700f9c1f707d1ee278e54c2e7590d9.png

运行结果正常,不会发生饥饿死锁啦,因为线程池足够大。

本文完结。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值