【架构师面试-JUC并发编程-9】-线程池

wuchangjian2021-11-05 02:05:02编程学习

1:线程池介绍

使用线程池,可以复用线程及控制线程的总量。如果不使用线程池,每个任务都要 新开一个线程来处理,反复创建线程开销大,过多的线程会占用太多内存。

这样开销太大了,我们希望有固定数量的线程,来执行这1000个线程,这样就避 免了反复创建并销毁线程所带来的开销问题。

2:使用线程池的好处及场景

1:好处

消除线程创建带来的延迟,加快响应速度。

合理利用CPU和内存

统一管理资源

2:适用场合

服务器接受到大量请求时,使用线程池计数是很适合的,它可以大大减少线程的创 建和销毁的次数,提高服务器的工作效率。

在实际开发中,如果需要创建5个以上的线程,就可以用线程池来管理。

3:线程池参数

 

1:corePoolSize核心线程数

线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时再创建新线程去执行任务。

2:maxPoolSize最大线程数

线程池可能会在核心线程数的基础上,额外增加一些线程,但是这些新增加的线程数有一个上限,就是maxPoolSize

3:新的线程是由ThreadFactory创建

默认使用Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的NORM_PRIORITY优先级并且都不是守护线程。如果自己指定ThreadFactory,则可以改变线程名、线程组、优先级、是否是守护线程。

4:工作队列类型

直接交换:SynchronousQueue,这个队列没有容量,无法保存工作任务。

无界队列:LinkedBlockingQueue 无界队列

有界队列:ArrayBlockingQueue 有界队列

4:线程池原理

如果线程数小于corePoolSize,即使其它工作线程处于空闲状态,也会创建一个新线程来运行新任务。

如果线程数大于等于corePoolSize,但少于maximumPoolSize,将任务放入队列。

如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程来运行任务。

如果队列已满,并且线程数大于或等于maxPoolSize,则拒绝该任务。

 

 例子:核心池大小为5,最大池大小为10,队列为100

 因为线程中的请求最多会创建5个,然后任务将被添加到队列中,直到达到100。

 当队列已满时,将创建最新的线程maxPoolSize,最多到10个线程,如果再来任务,则拒绝。

增减线程的特点

通过设置corePoolSize和maximumPoolSize相同,可以创建固定大小的线程池。

线程池希望保持较少的线程数,并且只有在负载变得很大时才会增加。

通过设置maximumPoolSize为很高的值,例如Integer.MAX_VALUE,可以允许线 程 池容纳任意数量的并发任务。

只有在队列填满时才创建多于corePoolSize的线程,所以如果用的是无界队列 (LinkedBlockingQueue),则线程数就不会超过corePoolSize

5:自动创建线程池

1:newFixedThreadPool

手动创建更好,因为这样可以更明确线程池的运行规则,避免资源耗尽的风险。

threadpool/FixedThreadPoolTest.java

不管有多少任务,始终只有4个线程在执行。

package threadpool;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
/**
 * 描述:演示newFixedThreadPool
 */
public class FixedThreadPoolTest {
 
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());
        }
    }
}
 
class Task implements Runnable {
 
    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

 源码解读

 

第一个参数是核心线程数量,第二个参数是最大线程数量,它设置的与核心线程数量一样,不会有超过核心线程数量的线程出现,所以第三个参数存活时间设置为0,第四个参数是无界队列,有再多的任务也都会放在队列中,不会创建新的线程。

如果线程处理任务的速度慢,越来越多的任务就会放在无界队列中,会占用大量内存,这样就会导致内存溢出(OOM)的错误。

threadpool/FixedThreadPoolOOM.java

package threadpool;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
/**
 * 描述:     演示newFixedThreadPool出错的情况
 */
public class FixedThreadPoolOOM {
 
    private static ExecutorService executorService = Executors.newFixedThreadPool(1);
    public static void main(String[] args) {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            executorService.execute(new SubThread());
        }
    }
 
}
 
class SubThread implements Runnable {
    @Override
    public void run() {
        try {
            //处理越慢越好,演示内存溢出
            Thread.sleep(1000000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  测试时可以将内存设置小些:Run->Run..->Edit Configurations

 

 

2:newSingleThreadExecutor

只有一个线程

threadpool/SingleThreadExecutor.java

package threadpool;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class SingleThreadExecutor {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());
        }
    }
}

 

它和newFixedThreadPool的原理相同,只不过把线程数直接设置为1,当请求堆积时,也会占用大量内存。

3: newCachedThreadPool

可缓存线程池,它是无界线程池,并具有自动回收多余线程的功能。

threadpool/CachedThreadPool.java

package threadpool;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class CachedThreadPool {
 
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());
        }
    }
}

 源码解读

 SynchronousQueue是直接交换队列,容量为0,所以不能将任务放在队列中。任务过来后直接交给线程去执行。线程池的最大线程数量是整数的最大值,可以认为没有上限,有多个任务过来就创建多个线程去执行。一定时间后(默认60秒),会将多余的线程进行回收。

弊端:第二个参数maximumPoolSize设置为Integer.MAX_VALUE,可能会创建非常多的线程,导致OOM

4:newScheduledThreadPool

支持定时及周期性任务执行的线程池

threadpool/ScheduledThreadPoolTest.java

package threadpool;
 
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
 
public class ScheduledThreadPoolTest {
 
    public static void main(String[] args) {
        ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
        //threadPool.schedule(new Task(), 5, TimeUnit.SECONDS);
        //先延迟一秒运行,然后每隔3秒运行一次
        threadPool.scheduleAtFixedRate(new Task(), 1, 3, TimeUnit.SECONDS);
    }
}

6:手动创建线程池

1:正确创建线程池方法

根据不同的业务场景,自己设置线程池的参数,比如内存大小、线程名、任务被拒绝后如何记录日志等。

2:设置线程池数量

CPU密集型(比如加密、计算hash等):最佳线程数为CPU核心数的1-2倍。

耗时IO型(读写数据库、文件、网络读写):最佳线程数应大于CPU核心数很多倍,以JVM线程监控显示繁忙为依据,保证线程空闲时可以衔接上,充分利用CPU

线程数=CPU核心数*(1+平均等待时间/平均工作时间)

7:拒绝策略

1:拒绝时机

1) 当Executor关闭时,提交新任务会被拒绝。

2) 当Executor对最大线程和工作队列容量使用有限边界并且已经饱和时

2:AbortPolicy

直接抛出异常,说明任务没有提交成功

3:DiscardPolicy

线程池会默默的丢弃任务,不会发出通知

4:DiscardOldestPolicy

队列中存有很多任务,将队列中存在时间最久的任务给丢弃。

5:CallerRunsPolicy

当线程池无法处理任务时,那个线程提交任务由那个线程负责运行。好处在于避免丢弃任务和降低提交任务的速度,给线程池一个缓冲时间。

8:线程池满了,往线程池里提交任务会发生什么样的情况

如果你使用的LinkedBlockingQueue(阻塞队列),也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;

如果你使用的是有界队列比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。

线程池的饱和策略:

当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略

相关文章

JDT操作AST重构if块

实现效果1:   实现效果2: 代码ÿ...

从1亿美元到10亿美元,博世/大陆看到的自动驾驶量产技术趋势

从1亿美元到10亿美元,博世/大陆看到的自动驾驶量产技术趋势

从ADAS到自动驾驶,基于数据驱动的开发模式已经是明确趋势。但如何降低开发...

项目成本管理__计划价值_挣值_实际成本三者关系与应对措施

三参数关系分析措施  1  SV<0 CV<0 进度滞后 成本超支    ...

官方文档日记

1、oracle 11g plsql 开发手册:Database PL/S...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。