线程池创建的4种方式与参数详解

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程池创建的4种方式与参数详解相关的知识,希望对你有一定的参考价值。

参考技术A Executors 是 Executor 的工具类,提供了4种创建不同线程池的方式,来满足业务的需求。底层是调ThreadPoolExecutor类构造方法。

newFixedThreadPool:创建的是定长的线程池,可以控制线程最大并发数,超出的线程会在线程队列中等待,使用的是无界队列,核心线程数和最大线程数一样,当线程池中的线程没有任务时候立刻销毁,使用默认线程工厂。

newSingleThreadExecutor:创建的是单线程化的线程池,只会用唯一一个工作线程执行任务,可以指定按照是否是先入先出,还是优先级来执行任务。同样使用无界队列,核心线程数和最大线程数都是1个,同样keepAliveTime为0,可选择是否使用默认线程工厂。

newCachedThreadPool:设定一个可缓存的线程池,当线程池长度超过处理的需要,可以灵活回收空闲线程,如果没有可以回收的才新建线程。没有核心线程数,当线程没有任务60s之后就会回收空闲线程,使用有界队列。同样可以选择是否使用默认线程工厂。

newScheduledThreadPool:支持线程定时操作和周期性操作。

Executors工具类创建线程池底层是调ThreadPoolExecutor构造方法。
1、ThreadPoolExecutor4个创建线程池的构造方法:

2、参数详解
corePoolSize:核心线程数量。当线程数少于corePoolSize的时候,直接创建新的线程,尽管其他线程是空闲的。当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。

maximunPoolSize:线程池最大线程数。只有在缓冲队列满了之后才会申请超过核心线程数的线程。当线程数量大于最大线程数且阻塞队列满了这时候就会执行一些策略来响应该线程。

workQueue:阻塞队列。存储等待执行的任务,会对线程池的运行产生很大的影响。当提交一个新的任务到线程池的时候,线程池会根据当前线程数量来选择不同的处理方式。

keepAliveTime:允许线程的空闲时间。当超过了核心线程数之外的线程在空闲时间到达之后会被销毁。

unit:keepAliveTime的时间单位。

threadFactory:线程工厂。用来创建线程,当使用默认的线程工厂创建线程的时候,会使得线程具有相同优先级,并且设置了守护性,同时也设置线程名称。

handler:拒绝策略。当workQueue满了,并且没有空闲的线程数,即线程达到最大线程数。就会有四种不同策略来处理

待补充。

参考资料
https://www.jianshu.com/p/272e36149e05

线程池面试汇总

文章目录

1.进程Process与线程Thread

进程:就是正在执行的程序,线程是程序执行的一条路径,一个进程之中可以包含多个线程。通俗的讲,我们可以把打开微信开做一个进程,在微信里和一个好友进行视频聊天就是开启了一个线程。

线程:一个进程里可以有多条线程,至少有一条线程,我认为可以理解为当你开启进程时默认开启一条线程,一条线程一定会在一条进程里,就好像你不打开微信就没有办法和微信里的好友进行聊天。

2.创建多线程4种方式

方式一:实现类继承Thread类
步骤:
①实现类去继承Thread类;
②实现类重写Thread类中的run()方法;
③测试类创建Thread子类实例;
④开启线程,调用Thread类中的start()方法。

方式二:实现Runnable接口
步骤:
①实现Runnable接口;
②重写run方法;
③实例化实现类;
④将实现类以参数传递给Thread对象;
⑤开启线程。

方式三: 实现Callable接口,实例化FutureTask类(jdk.1.5出现)
步骤:
①实现Runnable接口;
②重写run方法;
③实例化实现类;
④实现类以参数的形式传递到FutureTask对象;
⑤FutureTask对象以参数的形式传递到Thread对象中;
⑥此步骤可有可无,FutureTask对象调用get()方法,获取到call方法的返回值;

重写的Call()方法特性:

1.call()方法有返回值;②call()方法会抛异常;③call()方法支持泛型

实现Callable接口 Java测试代码:

package TreadTest;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*
线程创建方式三:
实现Callable接口
 */
class thread4 implements Callable 

    public Object call() throws Exception 
        int sum = 0;
        for (int i = 0; i < 5; i++) 
            sum += i;
        
        return sum;
    


public class ThreadTest3 
    public static void main(String[] args) 
        thread4 t4 = new thread4();
        
        FutureTask<Integer> task = new FutureTask<Integer>(t4);
        new Thread(task).start();

        try 
            Integer integer = task.get();//调用get()方法要抛异常,返回call方法的返回值
            System.out.println("sum=" + integer);
         catch (InterruptedException e) 
            e.printStackTrace();
         catch (ExecutionException e) 
            e.printStackTrace();
        
    


执行结果:(会返回call()方法的返回值)

sum=10

Callable其实就是一接口,跟Runnable差不多,只不多有返回值,它需要有一容器来包装它就是FutureTask类。

方式四: 线程池
Executor框架介绍:在jdk1.5加入Executor框架,Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。

背景

经常创建和销毁,使用量特别大的资源时,比如并发情况下的线程,对性能影响很大。

思路

提前创建多个线程,放入线程池,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用

好处

1、提高响应速度(减少了创建新线程的时间)
2、降低资源消耗(重复利用线程池中线程,不需要每次创建)
3、便于线程管理

使用:

	JKD5.0起提供了线程池相关API:ExecutorServiceExecutors
	ExecutorService:真正的线程池接口。常见子类ThreadPoolExceptor
	void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
	<T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
	void shutdown():关闭连接池
	Executors:工具类,线程池的工厂类,用户创建并返回不同类型的线程池

package com.bruce.demo9;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//测试线程池
public class TestPool 
    public static void main(String[] args) 
        //创建服务,创建线程池
        //newFixedThreadPool  参数:线程池大小
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        //执行
        executorService.execute(new MyThread(1));
        executorService.execute(new MyThread(2));
        executorService.execute(new MyThread(3));
        executorService.execute(new MyThread(4));
        //关闭连接
        executorService.shutdown();
    


class MyThread implements Runnable

    int id;

    public MyThread(int id) 
        this.id = id;
    

    @Override
    public void run()
        System.out.println(Thread.currentThread().getName()+"执行任务"+id);
    



3.继承Thread类和实现Runnable接口比较

相同点

①都可以创建多个线程;
②都需要重写Thread类中的run()方法;
③都需要通过start()方法进行开启;
④run方法就是void类型的,没有返回值;

差别

①关键字不一样,继承是extends Thread,实现implements Runnable;
②通过实现Runnable接口创建的线程要比继承Thread这种方式灵活得多。其实查看源码,我们可以知道,Thread类其实也是继承于Runnable接口。由于Java是单继承,可多实现的特性,当实现类要继承于其他类时,就不能再继承Thread类了,所有Java就因此又多了两种创建方式一是实现runnable接口(方法二),二是实现Callable接口(方式三)。
③由于多个线程共享一个Runnable对象,所以Runnable适合多个相同的线程去处理共享资源。这就涉及到线程同步了。

4.使用线程池比手动创建线程好在哪里

1、减少线程生命周期带来的开销。如:线程是提前创建好的,可以直接使用,避免创建线程的消耗。
2、合理的利用内存和CPU。如:避免线程创建较多造成的内存溢出,避免线程创建较少造成CPU的浪费。
3、可以统一管理资源。如:统一管理任务队列,可以统一开始或结束任务。

/** 
*  例子: 用固定线程数的线程池执行10000个任务 
*/ 
public class ThreadPoolDemo  
    
    public static void main(String[] args)  
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10000; i++)  
            service.execute(new Task());
         
        System.out.println(Thread.currentThread().getName());
     

    static class Task implements Runnable  
        public void run()  
            System.out.println("Thread Name: " + Thread.currentThread().getName());
         
     


5.线程池的各参数的含义

corePoolSize:核心线程数(“长工”),常驻线程的数量。随着任务增多,线程池从0开始增加。

maxPoolSize:最大线程数,创建线程的最大容量。是核心线程数与非核心线程数之和。

keepAliveTime+时间单位:空闲线程存活时间。当非核心线程(“临时工”)空闲时,过了存活时间该线程就会被- 回收 。

ThreadFactory:创建线程的工厂。

workQueue:存放任务的队列。任务队列满了,会创建非核心线程,直至达到最大线程数。

Handler:任务拒绝策略。当线程数达到最大,并且队列被塞满时,会拒绝任务。

6.五种拒绝策略

AbortPolicy:抛出RejectedExecutionException 的 RuntimeException异常,可根据业务做重试或做放弃提交等处理。

DiscardPolicy:直接丢弃任务不做任何提示,存在数据丢失风险。

DiscardOldestPolicy: 丢弃任务头节点,通常是存活时间最长的任务。给新提交的任务让路,这样也存在一定的数据丢失风险。

CallerRunsPolicy: 谁提交任务,谁来处理。将任务交给提交任务的线程执行(一般是主线程)。

自定义拒绝策略,写一个类实现RejectedExecutionHandler接口

7.有哪些常见的线程池?

  • FixedThreadPool: 固定线程池。核心线程数由构造参数传入,最大线程数=核心线程数。

  • CachedThreadPool: 缓存线程池。核心线程数为0,最大线程数为 2^31-1 。 队列的容量为0 ( SynchronousQueue )。

  • ScheduledThreadPool: 定时线程池。可延迟x秒,可延迟x秒,按y周期执行(起始点有开始或结束)。

  • SingleThreadPool:单一线程池。和FixedThreadPool差不多,区别在于只有一个核心线程数。

  • SingleThreadScheduledPool: 单一定时线程池。和ScheduledThreadPool差不多,区别在于内部只有一个线程。

8.线程池内部结构

1.线程池管理器:负责线程创建、销毁、添加任务等;

2.工作线程: 线程池创建的正在工作的线程;

3.任务队列BlockingQueue ):线程满了之后,可以放到任务队列中,起到一定的缓冲;

4.任务:要求实现统一的接口,方便处理和执行;

9.常见线程池的阻塞队列

  • LinkedBlockingQueue:容量大小为 Integer.MAX_Value,无界队列。对应线程池有 FixedThreaPoolSingleThreadPool

  • SynchronousQueue:容量大小为0。对应线程池有CachedThreadPool(可理解线程数无限扩展);

  • DelayedWorkQueue: 延迟工作队列。队列中的任务不是按照任务存放的先后顺序放的,而是按照延迟时间的先后存放的。对应线程池有ScheduledThreadPoolSingleThreadScheduledPool

10.为什么不应该自动创建线程池

为什么不应该自动创建线程池,所谓的自动创建线程池就是直接调用 Executors 的各种方法来生成前面了解过的常见的线程池,例如 Executors.newFixedThreadPool()。但这样做是有一定风险的,接下来我们就来逐一分析自动创建线程池可能带来哪些问题。

public static ExecutorService newFixedThreadPool(int nThreads)  

    return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());



FixedThreadPool、SingleThreadPool:使用的是无界队列(LinkedBlockingQueue),当任务堆积很多时,会占用大量内存,最终导致OOM。

ChachedTheadPool:可以无限创建线程(Integer.MAX_VALUE),任务过多时会导致创建线程达到操作系统上线或者发生OOM

ScheduledThreadPool、SingleThreadScheduledPool:使用的是DelayedWorkQueue队列,实质上也是一种无界队列,会导致OOM。

你可以看到,这几种自动创建的线程池都存在风险,相比较而言,我们自己手动创建会更好,因为我们可以更加明确线程池的运行规则,不仅可以选择适合自己的线程数量,更可以在必要的时候拒绝新任务的提交,避免资源耗尽的风险。

11.合适的线程数量是多少?CPU 核心数和线程数的关系

CPU密集型任务
占用CPU比较多的任务(加密、解密、计算等),最佳线程数为CPU核心数的 1~2倍。

耗时IO型任务
IO耗时比较多的任务(数据库、文件读写、网络传输等),占用CPU较少。《Java并发编程实战》推荐:最佳线程数= CPU核心数*(1+线程平均等待时间/线程平均工作时间)。

线程平均工作时间越长,应创建较少的线程。线程平均等待时间长,应创建较多的线程。

12.如何根据实际需要,定制自己的线程池

核心线程数:平均工作时间比例多 ,定义较少的线程数;平均等待时间比例高,创建较多的线程数。如一个任务CPU密集和IO耗时混搭,最大线程数应为核心线程数的几倍,应对突发情况。

阻塞队列: 相对于无界队列,可使用 ArrayBlockingQueue,可以设置固定容量,防止资源耗尽,同时会产生数据丢失。

另外,队列容量大小和最大线程数应做一个平衡。队列容量大,最大线程数小时,可减少上下文切换,但是减少吞吐量。队列容量小,最大线程数大时,可提高效率,但是增多上下文切换。

线程工厂: 我们可以使用默认的 defaultThreadFactory, 也可以使用 ThreadFactoryBuilder创建 线程工厂,并自定义线程名。

ThreadFactoryBuilder factoryBuilder = new ThreadFacoryBuillder();
ThreadFactory threadFactory = builder.setNameFormat("rpc-pool-%d").build();

这样,线程名会为 rpc-pool-1rpc-pool-2

拒绝策略:除了4种常规拒绝策略,还可以自定义拒绝策略,做日志打印, 暂存任务、重新执行等操作 。实现方式,继承 RejecedExecutionHandler 接口,重写 rejectedExecution () 方法。

private static class CustomRejectionHandler implements RejectedExecutionHandler  
    
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor)  
        //打印日志、暂存任务、重新执行等拒绝策略
     


13.如何正确关闭线程池?shutdown 和 shutdownNow 的区别?

  • shutdown():调用此方法,线程池不会马上关闭,会等线程运行完 ,并且阻塞的任务运行完再关闭。

  • isShutdown():判断线程池是否被标记关闭。调用了shutdown方法后,此方法会返回true。

  • isTerminated():判断线程池中是否已关闭并且阻塞的任务都已执行完 。

  • awaitTermination():判断线程池终结状态。等待周期内,线程池终结会返回true。超过等待时间,线程池未终结会返回false。等待周期内,线程被中断会抛出InterruptedException异常。

  • shutdownNow():表示立即关闭线程池。会向所有线程发送中断信号,并停止线程。将等待中的任务转移到list中,以后可做补救措施。

14.使用队列有什么需要注意的吗

使用有界队列时,需要注意线程池满了后,被拒绝的任务如何处理。
使用无界队列时,需要注意如果任务的提交速度大于线程池的处理速度,可能会导致内存溢出。

15.线程只能在任务到达时才启动吗

默认情况下,即使是核心线程也只能在新任务到达时才创建和启动。但是我们可以使用 prestartCoreThread(启动一个核心线程)或 prestartAllCoreThreads(启动全部核心线程)方法来提前启动核心线程。

public class Demo10 
    public static void main(String[] args) 
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(1));
        poolExecutor.prestartCoreThread();
        System.out.println(poolExecutor);
    

16.线程如何回收

线程池回收线程只会发生在当前线程池中线程数量大于corePoolSize参数的时候;当线程池中线程数量小于等于corePoolSize参数的时候,回收过程就会停止。

allowCoreThreadTimeOut设置项可以要求线程池:将包括“核心线程”在内的,没有任务分配的任何线程,在等待keepAliveTime时间后全部进行回收:

public class Demo10 
    public static void main(String[] args) throws Exception 
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1));
        poolExecutor.prestartAllCoreThreads();
        poolExecutor.allowCoreThreadTimeOut(true);
        System.out.println(poolExecutor);
        Thread.sleep(4000);
        System.out.println(poolExecutor);
    

17.线程池状态

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;//线程池容量
 
    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;
 
    // Packing and unpacking ctl
    private static int runStateOf(int c)  	 return c & ~COUNT_MASK; 
    private static int workerCountOf(int c)   return c & COUNT_MASK; 
    private static int ctlOf(int rs, int wc)  return rs | wc; 

18.总结

以上是关于线程池创建的4种方式与参数详解的主要内容,如果未能解决你的问题,请参考以下文章

线程池(详解):三大方法七大参数四种拒绝策略

SpringBoot 整合线程池及各参数详解

线程池ThreadPoolExecutorExecutors参数详解与源代码分析

Java线程池七个参数详解

线程池参数详解

Java线程池构造参数详解