记录一次学习总结线程池原理与其API

一次简单的多线程面试

不积跬步无以至千里,不积小流无以成江海!

问题1. 用多线程的目的是什么?
-> 充分利用CPU资源,并发做多件事。

问题2. 单核CPU机器上适不适合用多线程?
->适合,如果是单线程,线程中需要等待IO时,此时CPU就空闲出来了。

问题3.线程什么时候会让出CPU
-> 阻塞时 wait await 等待IO
->Sleep
->yield
结束了

问题4. 用自己的话说一说线程是什么?
-> 一条代码执行流,完成有一组代码的执行,这一组代码称之为一个任务。

问题5. CPU是做什么的工作的?
-> 执行代码
在这里插入图片描述

问题6. 线程是不是越多越好?
(1) 如上图,造卡车(线程)要不要时间?一次性使用,用完了得销毁,销毁要不要时间?
—线程在JAVA中是一个对象。每一个java对象线程都需要一个操作系统线程得支持。线程得创建、销毁需要时间。如果创建时间+销毁时间>执行任务时间 就会很不核算。
(2) 做很多得卡车(线程),得需要空间来存放,会不会造成内存紧张?
—java对象占用堆内存,操作系统线程占用系统内存,根据jvm规范,一个线程默认最大栈大小1M,这个栈空间是需要从系统内存分配的,线程过多,会消耗很多内存。同时,操作系统需要频繁切换线程上下文(大家都想被运行),影响性能。

问题6 .该如何正确使用多线程?
-> 多线程目的:充分利用CPU资源
-> 线程得本质:将代码送给CPU处理
-> 用合适数量得卡车(线程)不断得运送代码即可
-> 着合适数量得线程就构成一个线程池
-> 有任务要执行,就放入池中,池中得一个线程将把任务运送到CPU处理

通过以上问题引出线程池原理

线程池工作原理
在这里插入图片描述
(1) 接受任务,放入仓库
(2)工作线程从仓库取任务,执行。
(3)当没有任务时,线程阻塞,当有任务时唤醒线程执行

问题7. 如何确定合适数量得线程?
(1) 如果是计算型任务?
—cpu数量得1-2倍
(2) 如果是IO型任务?
—则需要多一些线程,要根据具体得IO阻塞时长进行考量决定。如Tomcat中默认最大得线程数为:200,也可以根据需要在一个最小数量和最大数量间自动增减线程数

JAVA线程池API

这里是引用
其中最常用的是ThreadPoolExecutor, 有如下四种标准实现

// 第一种
public ThreadPooleExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)

// 第二种
public ThreadPooleExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)

// 第三种
public ThreadPooleExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)

// 第四种
public ThreadPooleExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
// 参数解释
/*第1个参数:corePoolSize 表示常驻核心线程数。如果等于0,则执行完任务之后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完,核心线程也不会被销毁。这个值非常关键,设置过大会浪费资源,过小会导致线程频繁的创建或销毁。
第2个参数: maximumPoolSize 表示线程池能够同时执行的最大线程数,这个值必须大于1,如果执行的线程数大于此值,则需要借助第5个参数的帮助,缓存在队列中。如果maximumPoolSize与corePoolSize相等,即是固定大小的线程池。
第3个参数:keepAliveTime 表示线程池中的线程空闲时间,当空闲时间达到keepAliveTime值时,线程会被销毁,直至剩下corePoolSize个线程为止,避免浪费内存和句柄资源。在默认情况下,当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用。但是当ThreadPoolExecutor的allowCoreThreadTimeOut变量值设为true时,核心线程超时后也会被收回。
第4个参数:TimeUnit 表示时间单位。keepAliveTime 的时间单位通常时TimeUnit.SECONDS.
第5个参数:workQueue 表示缓存队列。当请求的线程数大于maximumPoolSize时,线程进入BlockingQueue阻塞队列。
第6个参数:threadFactory 表示线程工厂。他用来生产一组相同任务的线程。线程池的命名是通过给这个factory增加组名前缀名来实现的。在虚拟机栈分析时就可以知道线程任务是有哪个线程工厂来产生的。
第7个参数:handler 表示执行拒绝策略的对象。当超过第5个参数workQueue的任务缓存区上限时,就可以通过该策略处理对象,这是一种简单的限流保护。

RejectedExecutionHandler 常用的拒绝策略如下:

(1)ThreadPoolExecutor.AbortPolicy策略,是默认的策略,处理程序遭到拒绝将抛出运行时 RejectedExecutionException。
(2)ThreadPoolExecutor.CallerRunsPolicy策略 ,调用者的线程会执行该任务,如果执行器已关闭,则丢弃.
(3)ThreadPoolExecutor.DiscardPolicy策略,不能执行的任务将被丢弃.
(4)ThreadPoolExecutor.DiscardOldestPolicy策略,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程).

这里再强调一点,不能使用Executors创建线程池。在阿里巴巴开发手册中明确指出不允许使用Executors创建线程池,而实用ThreadPoolExecutor的方式创建,这样做的目的是为了避免耗尽资源

Executors的核心方法有如下5个:

  1. Executors.newWorkStealingPool:创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争,此构造方法中把CPU数量设置为默认的并行度:
public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }
  1. Executors.newCacheThreadPool: maximumPoolSize最大可至Integer.MAX_VALUE,是高度可伸缩的线城池,会抛出OOM异常。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }
  1. Executors.newScheduledThreadPool: 线程最大数可至最大可至Integer.MAX_VALUE,是高度可伸缩的线城池,会抛出OOM异常。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
// 
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
  1. Executors.newSingleThreadExecutor: 最大可至Integer.MAX_VALUE,会抛出OOM异常
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
// 队列没有指明长度,可导致OOM异常
public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
  1. Executors.newFixedThreadPool: 最大可至Integer.MAX_VALUE,会抛出OOM异常
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
// 队列没有指明长度,可导致OOM异常
public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

综上所述:使用Executors创建线程池可能导致OOM异常。

下面以一个案例总结线城池中所有参数用法

  1. Task.java 任务类
public class Task implements Runnable{
	private final AtomicLong count = new AtomicLong(0L);

	@Override
	public void run() {
		// TODO Auto-generated method stub
		System.out.println("线程名称:"+Thread.currentThread().getName());
		if(count.getAndIncrement() > 100) {
			System.out.println("测试闹着玩:"+2/0);
		}
	}

}
  1. MyThreadFactory 给线程组起名称
public class MyThreadFactory implements ThreadFactory {
	
	private final String namePrefix;
	
	// poolNumber 原子变量用来记录当前线程池的编号是应用级别的,所有线程池公用一个, 比如创建第一个线程池时候线程池编号为1,创建第二个线程池时候线程池的编号为2,
	private final AtomicInteger poolNumber = new AtomicInteger(1);
	
	// threadNumber 是线程池级别的,每个线程池有一个该变量用来记录该线程池中线程的编号,
	private final AtomicInteger threadNumber = new AtomicInteger(1);
	
	//定义线程组名称
	public MyThreadFactory(String threadGroupName) {
		namePrefix = threadGroupName + poolNumber.getAndIncrement()+ "-worker-";
		
	}

	@Override
	public Thread newThread(Runnable task) {
		// TODO Auto-generated method stub
		String name = namePrefix + threadNumber.getAndIncrement();
		Thread thread = new Thread(null, task, name, 0);
		System.out.println("线程名:"+thread.getName());
		return thread;
	}
	

}
  1. MyRejectHandler 拒绝策略
public class MyRejectHandler implements RejectedExecutionHandler{

	@Override
	public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) {
		// TODO Auto-generated method stub
		System.out.println("task rejected." + executor.toString());
	}

}
  1. MyThreadPool 线程池
public class MyThreadPool {
	
	public static void main(String[] args) {
		// 设置固定长度的缓存队列2,为了出发rejectHandler
		BlockingQueue queue = new LinkedBlockingQueue(2);
		
		// 两个线程任务
		MyThreadFactory thread1 = new MyThreadFactory("线程任务一池");
		
		// 拒绝策略
		MyRejectHandler handler = new MyRejectHandler();
		
		// 核心线程数1,最大线程数2,为保证出发rejectHandler
		ThreadPoolExecutor poolExecutor1 = 
				new ThreadPoolExecutor(1, 2, 60, TimeUnit.SECONDS, queue, thread1, handler);
		
		// 创建5个线程任务
		Task task = new Task();
		for(int i = 0; i<5; i++) {
			poolExecutor1.execute(task);
		}
	}

}

结果:

线程名:线程任务一池1-worker-1
线程名:线程任务一池1-worker-2
线程名称:线程任务一池1-worker-1
线程名称:线程任务一池1-worker-1
线程名称:线程任务一池1-worker-2
线程名称:线程任务一池1-worker-1
线程名:线程任务一池1-worker-3
task rejected.java.util.concurrent.ThreadPoolExecutor@5c647e05[Running, pool size = 2, active threads = 0, queued tasks = 0, completed tasks = 4]
Exception in thread "线程任务一池1-worker-1" java.lang.ArithmeticException: / by zero
	at com.zxf.executors.Task.run(Task.java:13)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
	at java.lang.Thread.run(Unknown Source)

说明:在这里插入图片描述
这样一来,哪个线程出了问题就一目了然,也方便于排查!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值