第八章:线程池的使用——Java并发编程实战

时间:2022-03-24 18:03:34

一、在任务和执行策略之间隐性耦合

Executor框架将任务的提交和它的执行策略解耦开来。虽然Executor框架为制定和修改执行策略提供了相当大的灵活性,但并非所有的任务都能适用所有的执行策略。

  • 依赖性任务:依赖其他同步任务的结果,使其不得不顺序执行,影响活跃性
  • 使用线程封闭的任务:在单线程的Executor中执行,任务可以不是线程安全的,但是一旦提交到线程池时,就会失去线程安全
  • 对响应时间敏感的任务:在单个线程或含有少量线程的线程池中执行是不可接受的
  • 使用ThreadLocal的任务:ThreadLocal使每个线程都可以拥有某个变量的一个私有"版本",而线程池中的线程是重复使用的,即一次使用完后,会被重新放回线程池,可被重新分配使用。因此,ThreadLocal线程变量,如果保存的信息只是针对一次请求的,放回线程池之前需要清空这些Threadlocal变量的值(或者取得线程之后,首先清空这些Threadlocal变量的值)

只有任务都是同类型并且相互独立时,线程池的效率达到最佳

1、线程饥饿死锁——在线程池中所有正在执行任务的线程都由于等待其他仍处于工作队列中的任务而阻塞

  例1:在单线程池中,正在执行的任务阻塞等待队列中的某个任务执行完毕

  例2:线程池不够大时,通过栅栏机制协调多个任务时

  例3:由于其他资源的隐性限制,每个任务都需要使用有限的数据库连接资源,那么不管线程池多大,都会表现出和和连接资源相同的大小 

每当提交了一个有依赖性的Executor任务时,要清楚地知道可能会出现线程"饥饿"死锁,因此需要在代码或配置Executor地配置文件中记录线程池地大小限制或配置限制

2、运行时间较长的任务

  线程池的大小应该超过有较长执行时间的任务数量,否则可能造成线程池中线程均服务于长时间任务导致其它短时间任务也阻塞导致性能下降

缓解策略:限定任务等待资源的时间,如果等待超时,那么可以把任务标示为失败,然后中止任务或者将任务重新返回队列中以便随后执行。这样,无论任务的最终结果是否成功,这种方法都能确保任务总能继续执行下去,并将线程释放出来以执行一些能更快完成的任务。例如Thread.join、BlockingQueue.put、CountDownLatch.await以及Selector.select等

 

二、设置线程池的大小

线程池的理想大小取决于被提交任务的类型及所部署系统的特性

  • 线程池过大,那么大量的线程将在相对很少的CPU和内存资源上发生竞争,这不仅会导致更高的内存使用量,而且还可能耗尽资源
  • 如果线程池过小,那么将导致许多空闲的处理器无法执行工作,从而降低吞吐量

对于计算密集型的任务,在拥有Ncpu个处理器的系统上,当线程池的大小为Ncpu+1时,通常能实现最优的利用率;对于包含I/O操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大

N(threads)=N(cpu)*U(cpu)*(1+W/C)   N(cpu)=CPU的数量=Runtime.getRuntime().availableProcessors(); U(cpu)= 期望CPU的使用率,0<=U(cpu)<=1 ;W/C=等待时间与运行时间的比率

三、配置ThreadPoolExecutor

 

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

1、线程的创建与销毁

  • CorePoolSize: 线程池基本大小,在创建ThreadPoolExecutor初期,线程并不会立即启动,而是等到有任务提交时才会启动,除非调用prestartAllCoreThreads,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。
  • MaxmumPooSize: 线程池最大大小表示可同时活动的线程数量的上限。若某个线程的空闲时间超过了keepAliveTime, 则被标记为可回收的

newFixedThreadPool: CorePoolSize = MaxmumPoolSize

newCachedThreadPool: CorePoolSize=0,MaxmumPoolSize=Integer.MAX_VALUE,线程池可被无限扩展,需求降低时自动回收

2、管理队列任务

  • workQueue:用于保存超过线程池线程处理速率的Runnable任务的队列 (三种:*队列、有界队列和同步移交)

newFixedThreadPool和newSingleThreadPool在默认情况下将使用一个*的LinkedBlockingQueue,有更好的性能

使用有界队列有助于避免资源耗尽的情况发生,为了避免当队列填满后,在使用有界的工作队列时,队列的大小与线程池的大小必须一起调节,能防止过载

对于非常大的或者*的线程池,可以通过使用SynchronousQueue来避免任务排队,要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素,任务会直接移交给执行它的线程,否则将拒绝任务。newCachedThreadPool工厂方法中就使用了SynchronousQueue

使用优先队列PriorityBlockingQueue可以控制任务被执行的顺序

3、饱和策略

  • AbortPolicy(中止策略),默认的饱和策略。会抛出RejectedExecutionException异常(抛弃当前任务vs抛弃最旧任务)
  • 调用者运行:下一个任务在调用了execute方法的主线程中进行运行,主线程至少在一段时间内不能提交任何任务。到达的请求将被保存在TCP层的队列中而不是在应用程序的队列中,导致服务器在高负载下实现一种平缓的性能降低

其他:对执行策略进行修改,使用信号量,控制处于执行中的任务

public class BoundedExecutor {
    private final Executor exec;
    private final Semaphore semaphore;
    
    public BoundedExecutor(Executor exec, int bound) {
        this.exec = exec;
        this.semaphore = new Semaphore(bound);
    }
    
    public void submitTask(final Runnable command){
        try {
            semaphore.acquire(); //提交任务前请求信号量
            exec.execute(new Runnable() {
                @Override
                public void run() {
                    try{
                        command.run();
                    } finally{
                        semaphore.release(); //执行完释放信号
                    }
                }
            });
        } catch (InterruptedException e) {
            // handle exception
        }
    }
}

4、线程工厂

通过自定义线程工厂可以对其进行扩展加入新的功能实现

当应用需要利用安全策略来控制某些特殊代码库的访问权,可以利用PrivilegedThreadFactory来定制自己的线程工厂,以免出现安全性异常。将与创建privilegedThreadFactory的线程拥有相同的访问权限、AccessControlContext和contextClassLoader

第八章:线程池的使用——Java并发编程实战第八章:线程池的使用——Java并发编程实战
public class MyThreadFactory implements ThreadFactory {
    private final String poolName;
    
    public MyThreadFactory(String poolName) {
        super();
        this.poolName = poolName;
    }

    @Override
    public Thread newThread(Runnable r) {
        return new MyAppThread(r);
    }
}

public class MyAppThread extends Thread {
    public static final String DEFAULT_NAME="MyAppThread";
    private static volatile boolean debugLifecycle = false;
    private static final AtomicInteger created = new AtomicInteger();
    private static final AtomicInteger alive = new AtomicInteger();
    private static final Logger log = Logger.getAnonymousLogger();
    
    public MyAppThread(Runnable r) {
        this(r, DEFAULT_NAME);
    }

    public MyAppThread(Runnable r, String name) {
        super(r, name+ "-" + created.incrementAndGet());
        setUncaughtExceptionHandler( //设置未捕获的异常发生时的处理器
                new Thread.UncaughtExceptionHandler() {
                    @Override
                    public void uncaughtException(Thread t, Throwable e) {
                        log.log(Level.SEVERE, "UNCAUGHT in thread " + t.getName(), e);
                    }
                });
    }

    @Override
    public void run() {
        boolean debug = debugLifecycle;
        if (debug) 
            log.log(Level.FINE, "running thread " + getName());
        try {
            alive.incrementAndGet();
            super.run();
        } finally {
            alive.decrementAndGet();
            if (debug) 
                log.log(Level.FINE, "existing thread " + getName());
        }
    }
}
自定义线程工厂

 

5、在调用构造函数后在定制ThreadPoolExecutor

  • 可以在创建线程池后,再通过Setter方法设置其基本属性(将ExecutorService扩展为ThreadPoolExecutor)
  • 在Executors中包含一个unconfigurableExecutorService工厂方法,该方法对一个现有的ExecutorService进行包装,使其只暴露出ExecutorService的方法,因此不能对它进行配置

 

四、扩展ThreadPoolExecutor

ThreadPoolExecutor使用了模板方法模式,提供了beforeExecute、afterExecute和terminated扩展方法

  • 线程执行前调用beforeExecute(如果beforeExecute抛出了一个RuntimeException,那么任务将不会被执行)
  • 线程执行后调用afterExecute(抛出异常也会调用,如果任务在完成后带有一个Error,那么就不会调用afterExecute)
  • 在线程池完成关闭操作时调用terminated,也就是所有任务都已经完成并且所有工作者线程也已经关闭后
第八章:线程池的使用——Java并发编程实战第八章:线程池的使用——Java并发编程实战
public class TimingThreadPoolExecutor extends ThreadPoolExecutor {
    private final ThreadLocal<Long> startTime = new ThreadLocal<Long>();//任务执行开始时间
    private final Logger log = Logger.getAnonymousLogger();
    private final AtomicLong numTasks = new AtomicLong(); //统计任务数
    private final AtomicLong totalTime = new AtomicLong(); //线程池运行总时间

    public TimingThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
            long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        log.fine(String.format("Thread %s: start %s", t, r));
        startTime.set(System.nanoTime());
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        try{
            long endTime = System.nanoTime();
            long taskTime = endTime - startTime.get();
            numTasks.incrementAndGet();
            totalTime.addAndGet(taskTime);
            log.fine(String.format("Thread %s: end %s, time=%dns", t, r, taskTime));
        } finally{
            super.afterExecute(r, t);
        }
    }

    @Override
    protected void terminated() {
        try{
            //任务执行平均时间
            log.info(String.format("Terminated: average time=%dns", totalTime.get() / numTasks.get()));
        }finally{
            super.terminated();
        }
    }
}
增加日志和记时等功能的线程池

 

五、递归算法的并行化

  • 如果循环中的迭代操作都是独立的,并且不需要等待所有的迭代操作都完成再继续执行,那么就可以使用Executor将串行循环转化为并行循环
  • 如果需要提交一个任务集并等待它们完成,那么可以使用ExecutorService.invokeAll
  • 如果递归执行的任务中,在每个迭代操作中都不需要来自于后续递归迭代的结果,可以创建一个特定于遍历过程的Executor,并使用shutdown和awaitTermination等方法,等待上面并行运行的结果