并发编程之线程池详解

juc线程池最重要的接口是ExecutorService,ThreadPoolExecutor类是线程池的具体实现

一、线程池状态

ThreadPoolExecutor中使用int的高3位来表示线程池的状态,低29位表示线程数量

状态名高3位接受新任务处理阻塞队列任务说明
Running111YY 
ShutDown000NY不会接受新任务,但是会处理阻塞队列的剩余任务
stop001NN会中断正在执行的任务,并抛弃阻塞队列的任务
Tidying010  任务全部执行完成,活动线程数为0
Terminated011  终结状态

Terminated>Tidying>stop>ShutDown>Running

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))

线程池使用一个32位整型来表示两个状态,使用一次CAS操作即可修改当前两个状态,如果此种设计,定义两个状态,使用两次CAS操作,有可能导致数据不一致。

二、线程池参数

public ThreadPoolExecutor(int corePoolSize,//核心线程数
                          int maximumPoolSize,//最大线程数
                          long keepAliveTime,//存活时间(针对救急线程,救急线程=最大线程数-核心线程数)
                          TimeUnit unit,//存活时间的单位(针对救急线程)
                          BlockingQueue<Runnable> workQueue,//阻塞队列
                          ThreadFactory threadFactory,//产生线程的工厂
                          RejectedExecutionHandler handler//当核心线程繁忙,阻塞队列满了,救急线程也繁忙,此时执行拒绝策略
) 

1、线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务

2、当线程数达到corePoolSize并没有线程空闲,这是再加入任务,新加的任务会被加入workQueue队列中排队,直到有空闲的线程

3、如果队列选择了有界队列,那么任务超过了队列大小时,会创建MaxPoolSize-corePoolSize数量的救急线程来执行任务

4、如果线程达到最大线程数,仍然有新任务加入,此时执行拒绝策略(jdk提供了4种,第三方框架也有提供)

(1)AbortPolicy(调用者抛出RejectedExecutionException异常,这是默认策略)

(2)CallerRunsPolicy(调用者运行任务)

(3)DiscardPolicy(放弃本次任务)

(4)DiscardOldestPolicy(放弃队列中最早的任务,用当前任务替代)

(5)Netty的实现是创建一个新的线程来执行任务

5、当高峰过去,超过corePoolSize的救急线程如果过了指定的一段时间(keepAliveTime和TimeUnit 指定)没有任务执行,将会被回收。

三、常用的工厂线程池

1、newFixedThreadPool(适用于任务量已知,相对耗时的任务)

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

特点

(1)核心线程数和最大线程数一致,即没有救急线程

(2)阻塞队列是无界的,可以放任意数量的任务

2、newCachedThreadPool(适用于任务数密集,但每个任务执行时间较短)

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

特点

(1)核心线程数是0,最大线程数是Integer.MAX_VALUE,救急线程的空闲生存时间是60s,即全部都是救急线程(60s后无任务执行就回收)

(2)阻塞队列采用SynchronousQueue,它没有容量,没有线程来取的话,放入任务的线程是放不进去的处于阻塞状态(类似生活中一手交钱,一手交货)

3、newSingleThreadExecutor(适用于多个任务排队执行)

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

特点

(1)核心线程数为1,最大线程数也为1,即没有救急线程

四、提交任务

1、execute(执行任务没有返回值)

void execute(Runnable command);

2、submit(提交任务,用Future获取任务执行结果)

<T> Future<T> submit(Callable<T> task);

3、invokeAll(提交tasks所有任务)

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
    throws InterruptedException;

4、invokeAll(提交所有任务,且带超时时间)

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                              long timeout, TimeUnit unit)
    throws InterruptedException;

5、invokeAny(提交tasks所有任务,tasks任意一个任务执行完成,返回此任务的执行结果,其他任务取消)

<T> T invokeAny(Collection<? extends Callable<T>> tasks)
    throws InterruptedException, ExecutionException;

6、invokeAny(提交tasks所有任务,tasks任意一个任务执行完成,返回此任务的执行结果,其他任务取消,带超时时间)

<T> T invokeAny(Collection<? extends Callable<T>> tasks,
                long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException;

五、关闭线程池

ThreadPoolExecutor类的关闭方法

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        //修改线程状态
        advanceRunState(SHUTDOWN);
        //打断空闲线程
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    //尝试终结(没有运行的线程可以立即终结,还有运行的线程不会等待)
    tryTerminate();
}
public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        //修改线程池状态
        advanceRunState(STOP);
        //打断所有线程
        interruptWorkers();
        //获取队列中剩余任务
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    //尝试终结
    tryTerminate();
    return tasks;
}

六、工作线程

有限的工作线程轮流异步处理无限的任务,典型实现就是线程池,同时也体现了设计模式里的享元模式。

1、饥饿

固定大小线程池会有饥饿现象

举例

两个工人是同一个线程池中两个线程,这两个线程的要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作

(1)客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待

(2)后厨做菜:做菜

场景1:来了一位客人。工人A处理点餐任务,工人B做菜,相安无事

场景2:同时来了两位客人,此时工人A和工人B都处理点餐了,此时没人做菜,导致饥饿现象

public class Test {

    private static List<String> menu= Arrays.asList(new String[]{"地三鲜","辣子鸡丁","烤鸡翅"});
    private static Random random=new Random();

    public static void main(String[] args)  throws Exception{


        ExecutorService pool = Executors.newFixedThreadPool(2);

        pool.execute(()->{
            System.out.println("处理点餐");
            Future<String> future = pool.submit(() -> {
                System.out.println("做菜");
                return cooking();
            });
            try {
                String s = future.get();
                System.out.println("上菜,"+s);
            }catch (Exception e){
                e.printStackTrace();
            }
        });

        pool.execute(()->{
            System.out.println("处理点餐");
            Future<String> future = pool.submit(() -> {
                System.out.println("做菜");
                return cooking();
            });
            try {
                String s = future.get();
                System.out.println("上菜");
            }catch (Exception e){
                e.printStackTrace();
            }
        });

    }

    private static String cooking(){
        return menu.get(random.nextInt(menu.size()));
    }

}

运行结果

此时线程池处于饥饿状态了

解决饥饿现象,不能通过增加线程数来解决,最好的解决方式是针对不同的任务类型使用不同的线程池,这样就可以避免饥饿,并提升效率

2、选择合适的线程数

线程数过小,不能充分利用资源,过大会导致线程上下文的切换,占用更多的内存

(1)cpu密集型运算

采用cpu核数+1实现最优的cpu利用率,+1的操作是保证当线程由于页缺失故障或者其他原因导致暂停时,额外的这个线程就可以使用,保证cpu时钟周期不被浪费

(2)IO密集型运算

cpu不总是处于繁忙状态,当执行业务计算时,需要使用cpu资源,但是执行IO操作,远程RPC调用时,数据操作时,cpu时空闲的,此时可以使用多线程提高利用率

线程数=cpu核数*期望cpu利用率*总时间(cpu计算时间+等待时间)/cpu计算时间

七、任务调度线程池

1、Timer

java.util.Timer可以实现定时功能

优点,简单易用,当然缺点很严重

有如下缺点

(1)所有任务都是同一个线程来调度,因此所有任务都是串行的,同一时间只能有一个任务在执行

(2)前一个任务延迟,后面所有任务都延迟

(3)前一个任务由于异常导致失败,后面任务都不执行

2、ScheduledExecutorService

ScheduledExecutorService解决了Timer的缺点问题

(1)scheduleAtFixedRate

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,//初始延迟时间
                                              long period,//两个任务的间隔
                                              TimeUnit unit);//单位

假设period=1,unit为秒,如果每个任务执行完成都很快,那么每个任务的执行都是近似间隔1秒。

如果前一个任务执行需要2秒,当前一个任务运行结束后,第二个任务会立刻运行。,

(2)scheduleWithFixedDelay

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                 long initialDelay,//初始延迟时间
                                                 long delay,//两个任务的间隔
                                                 TimeUnit unit);//单位

假设delay=1,unit为秒,如果前一个任务执行需要2秒,当前一个任务运行结束后,第二个任务会等待1秒才会运行,这也是和scheduleAtFixedRate的差异

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值