java高并发(三.二)--线程复用:线程池

线程的生命周期

线程在创建后,通过start执行了run方法后,会被自动回收。

复用线程的原因

  • 线程频繁创建和关闭花费大量时间
  • 线程本身占用内存,创建过多线程可能导致OOM

1. 线程池

线程池中存放空闲线程,每当有任务执行,不再创建线程而是从线程池中获取,任务执行完后,将线程归还给线程池.
在这里插入图片描述

图片来源https://blog.csdn.net/Growing_stu/article/details/84144808

1) JDK对线程池的支持

ThreadPoolExecutor 类的构造函数

public ThreadPoolExecutor(int corePoolSize,//核心线程数
						int maximumPoo1Sizer//最大线程数
						long keepAliveTime,//非核心线程数保活时间
						TimeUnit unit,//时间单位
						BlockingQueue<Runnable> workQueue,//被提交但未执行任务的队列
						ThreadFactory threadFactory,//线程工厂
						RejectedExecutionHandler handler)//拒绝策略

线程池执行流程

  1. 当前任务数< corePoolSize,将使用已有的线程执行任务。
  2. 当前任务数>= corePoolSize,新提交任务将被放入workQueue中,等待线程池中任务调度执行
  3. 当workQueue已满,且corePoolSize<MaxmumPoolSize,新提交任务会创建新线程执行任务
  4. 当工作线程= maximumPoolSize时,且workQueue已满,新提交任务由RejectedExecutionHandler处理
    任务调度

2) 任务队列

  1. 直接提交队列(SynchronousQueue)

SynchronousQueue没有容量
每一个插入需要等待对应的删除操作,反之亦然
提交的任务不会保存在队列,而是准备直接提交给线程

  1. 有界的任务队列(ArrayBlockingQueue)

public ArrayBlockingQueue(int capacity)
当任务队列已满,则准备将任务提交给线程

  1. 无界的任务队列(LinkedBlockingQueue )

任务会直接进入任务队列直到线程池中有空闲的线程

  1. 优先任务队列(PriorityBlockingQueue)

public PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator
无界的任务队列,并会对入队的任务按照比较器排序

3) 拒绝策略

任务队列已满线程数=maximumPoolSize,再有新的任务到达,就要用到拒绝策略

JDK内置拒绝策略

  1. AbortPolicy 策略: 该策略策略会直接抛出异常, 阻止系统正常工作
  2. CallerRunsPolicy 策略: 只要线程池未关闭, 该策略直接在调用者线程中, 运行当前被丢弃的任务。
  3. DiscardOldestPolicy 策略: 读策略将丢弃最老的一个请求, 并尝试再次提交当前任务。
  4. DiscardPolicy 策略: 该策略默默地丢弃无法处理的任务, 不予任何处理。

自定义拒绝策略
拒绝策略都需要实现RejectedExecutionHandler接口,并实现**rejectedExecution()**方法

public class RejectThreadPoolDemo {
	public static class MyTask implements Runnable {
		@Override
		public void run() {
			System.out.println(System.currentTimeMillis() + ":Thread ID:"
					+ Thread.currentThread().getId());
			try {
				Thread.sleep(10000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	public static void main(String[] args) throws InterruptedException {
		MyTask task = new MyTask();
		ExecutorService es = new ThreadPoolExecutor(5, 5,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(5),
                Executors.defaultThreadFactory(),
                new RejectedExecutionHandler(){
					@Override
					public void rejectedExecution(Runnable r,
							ThreadPoolExecutor executor) {
						System.out.println(r.toString()+" is discard");
					}
		});
		for (int i = 0; i < Integer.MAX_VALUE; i++) {
			es.submit(task);
			Thread.sleep(10);
		}
	}
}
1571878291492:Thread ID:19
1571878291502:Thread ID:20
1571878291512:Thread ID:21
1571878291522:Thread ID:22
1571878291532:Thread ID:23
java.util.concurrent.FutureTask@33909752 is discard
java.util.concurrent.FutureTask@55f96302 is discard

代码分析

  1. 当任务队列已满,且线程数=maximumPoolSize,再有任务提交时,会执行拒绝策略
  2. LinkedBlockingQueue也可以指定任务队列大小,但一般使用ArrayBlockingQueue

4) 线程工厂

线程池中的线程由线程工厂创建.当线程池需要创建线程时,会执行ThreadFactory以下方法

Thread newThread(Runnable r);

自定义线程工厂:设置线程的名称,组,是否守护,优先级等信息

public class TFThreadPoolDemo {
	public static class MyTask implements Runnable {
		@Override
		public void run() {
			System.out.println(System.currentTimeMillis() + ":Thread Name:"
					+ Thread.currentThread().getName());
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	public static void main(String[] args) throws InterruptedException {
		MyTask task = new MyTask();
		ExecutorService es = new ThreadPoolExecutor(5, 5,
                0L, TimeUnit.MILLISECONDS,
                new SynchronousQueue<Runnable>(),
                new ThreadFactory(){
					@Override
					public Thread newThread(Runnable r) {
						Thread t= new Thread(r);
						t.setDaemon(true);
						t.setName(System.currentTimeMillis()+"");
						return t;
					}
				}
               );
		for (int i = 0; i < 5; i++) {
			es.submit(task);
		}
		Thread.sleep(2000);
	}
}
1571879131576:Thread Name:1571879131575
1571879131577:Thread Name:1571879131575
1571879131577:Thread Name:1571879131576
1571879131576:Thread Name:1571879131576
1571879131576:Thread Name:1571879131575

代码分析

  1. 通过自定义线程工厂,将创建线程的时间作为线程名,并将线程设置为守护线程
  2. 在线程执行时,打印当前时间和线程名中的时间,可以分析线程调度消耗的时间

Executors工厂方法
5. 固定大小的线程池

public static ExecutorService newFixedThreadPool(int nThreads)
核心线程数和最大线程数都为nThreads,任务队列为LinkedBloddngQueue.
public static ExecutorService newSingleThreadExecutor()
核心线程数和最大线程数都为1,任务队列为LinkedBloddngQueue.
public static ExecutorService newCachedThreadPool()
核心线程数为0,最大线程数都为max,任务队列为SynchronousQueue
  1. 计划任务线程池
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

计划任务调度方法

public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
											long initialDelay,
											long period,
											TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
											long initialDelay,
											long delay,
											TimeUnit unit);
  • ScheduledExecutorService不会立即安排任务,而是按照计划时间安排任务
  • schedule只对任务调度一次,其他2个方法对任务进行周期调度
    在这里插入图片描述
  • 周期地调度不会让任务堆叠执行.如果周期太短,任务会在上次任务结束后立即调度

2. 扩展线程池

ThreadPoolExecutor 是一个可以扩展的线程池

任务执行前运行
protected void beforeExecute(Thread t, Runnable r)
任务结束后运行
protected void afterExecute(Runnable r, Throwable t)
线程池退出时运行
protected void terminated()

源码分析
在ThreadPoolExecutor.Worker.runTask()中

try {
	beforeExecute(wt,task); //运行前
	Throwable thrown=null;
	try {
		task.run{); / /运行任务
	} catch (RuntimeException x) {
		thrown = x; throw x;
	} catch (Error x) {
		thrown = x; throw x;
	} catch (Throwable x) {
		thrown = x; throw new Error(x);
	} finally {
		afterExecute(task, thrown); //运行结束后
} finally {
	task = null;
	w.completedTasks++;
	w.unlock();
}
  1. beforeExecute会在任务执行前运行,包含工作线程工作任务两个参数
  2. afterExecute会在任务执行后运行,尽管任务抛出异常.包含工作任务异常信息两个参数

3. 优化线程池数量

线程池的大小对系统的性能有一定的影响。 过大或者过小的线程数量都无法发挥最优的系统性能
N c p u = C P U 的 数 量 Ncpu = CPU 的数量 Ncpu=CPU

获取cpu数量
Runtime.getRuntime().availableProcessors()

U c p u = 目 标 C P U 的 使 用 率 , 0 ≤ U c p u ≤ 1 Ucpu =目标 CPU 的使用率, 0≤Ucpu≤1 Ucpu=CPU使0Ucpu1
W / C = 等 待 时 间 与 计 算 时 间 的 比 率 W/C= 等待时间与计算时间的比率 W/C=
最优线程池大小
N t h r e a d s = N c p u × U c p u × ( 1 + W / C ) Nthreads = Ncpu×Ucpu ×(1 + W/C) Nthreads=Ncpu×Ucpu×(1+W/C)

4. 在线程池中寻找堆栈

线程池中的幽灵般错误

class DivTask implements Runnable {
    int a,b;
    public DivTask(int a,int b){
        this.a=a;
        this.b=b;
    }
    @Override
    public void run() {
        double re=a/b;
        System.out.println(re);
    }
}
public class TraceMain {
	public static void main(String[] args) {
		ThreadPoolExecutor pools=new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                0L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
		for(int i=0;i<5;i++){
			pools.submit(new DivTask(100,i));
		}
	}
}
100.0
25.0
33.0
50.0

代码分析

  1. 线程池执行了一个除法运算任务,但除以0并为抛出异常,而是直接跳过

获取异常信息

  1. 将submit()改为execute()

  2. 改造submit()方法

    Future retools.submit(new DivTask(100,i));
    re.get{);
    

异常信息

	Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
	at geym.conc.ch3.trace.DivTask.run(DivTask.java:11)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

错误信息包含了出现异常的任务,但是没有提示任务提交的位置

保存任务提交的堆栈信息

public class TraceThreadPoolExecutor extends ThreadPoolExecutor {
	public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
			long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
		super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
	}
	@Override
	public void execute(Runnable task) {
		super.execute(wrap(task, clientTrace(), Thread.currentThread()
				.getName()));
	}
	@Override
	public Future<?> submit(Runnable task) {
		return super.submit(wrap(task, clientTrace(), Thread.currentThread()
				.getName()));
	}
	private Exception clientTrace() {
		return new Exception("Client stack trace");
	}
	private Runnable wrap(final Runnable task, final Exception clientStack,
			String clientThreadName) {
		return new Runnable() {
			@Override
			public void run() {
				try {
					task.run();
				} catch (Exception e) {
					clientStack.printStackTrace();
					throw e;
				}
			}
		};
	}
}

代码解析

  1. TraceTraceThreadPoolExecutor中重写了execute和submit方法
  2. 提交的任务被wrap成新的Runnable,若抛出异常
     通过clientStack先打印任务提交位置的堆栈信息,再将任务异常抛出
     捕获任务异常,打印任务异常堆栈信息

打印任务提交的堆栈信息

public class TraceMain {
	public static void main(String[] args) {
		ThreadPoolExecutor pools=new TraceThreadPoolExecutor(0, Integer.MAX_VALUE,
                0L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
		for(int i=0;i<5;i++){
			pools.execute(new DivTask(100,i));
		}
	}
}

异常信息

java.lang.Exception: Client stack trace
	at geym.conc.ch3.trace.TraceThreadPoolExecutor.clientTrace(TraceThreadPoolExecutor.java:27)
	at geym.conc.ch3.trace.TraceThreadPoolExecutor.execute(TraceThreadPoolExecutor.java:16)
	at geym.conc.ch3.trace.TraceMain.main(TraceMain.java:14)
Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
	at geym.conc.ch3.trace.DivTask.run(DivTask.java:11)
	at geym.conc.ch3.trace.TraceThreadPoolExecutor$1.run(TraceThreadPoolExecutor.java:36)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

 堆栈信息包含了任务提交时的位置信息

分而治之:Fork/Join框架

在这里插入图片描述
ForkJoinPool原理一
ForkJoinPool原理二
任务窃取算法
ForkJoinTask

  • ForkJoinTask实现了Future接口,可通过get()获取线程的状态.
  • compute()方法在RecursiveAction子类没有返回值,RecursiveTack子类有返回值

任务分割
ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题
Fork/Join执行逻辑
线程互助
每个物理线程实际上是需要处理多个逻辑任务的。 每个线程必然需要拥有一个任务队列
当一个线程试图“ 帮助” 其他线程时, 总是从任务队列的底部开始获取数据, 而线程试图执行自己的任务时, 则是从相反的顶部开始获取数据。
互相帮助的线程
ForkJoinPool示例

public class CountTask extends RecursiveTask<Long>{
    private static final int THRESHOLD = 10000;
    private long start;
    private long end;
    public CountTask(long start,long end){
        this.start=start;
        this.end=end;
    }
    public Long compute(){
        long sum=0;
        boolean canCompute = (end-start)<THRESHOLD;
        if(canCompute){//如果任务规模小,直接进行
            for(long i=start;i<=end;i++){
                sum +=i;
            }
        }else{//如果任务规模大,分解成多个子任务
            //分成10个小任务
            long step=(end-start)/10;
            ArrayList<CountTask> subTasks=new ArrayList<CountTask>();
            long pos=start;
            for(int i=0;i<10;i++){
                long lastOne=pos+step;
                if(lastOne>end)lastOne=end;
                CountTask subTask=new CountTask(pos,lastOne);//创建子任务
                pos+=step+1;
                subTasks.add(subTask);//记录子任务
                subTask.fork();//提交子任务列表
            }
            for(CountTask  t:subTasks){
                sum+=t.join();//获取子任务返回值
            }
        }
        return sum;//返回计算值
    }
    public static void main(String[]args){
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        CountTask task = new CountTask(0,2000L);//创建任务
        ForkJoinTask<Long> result = forkJoinPool.submit(task);//提交任务
        try{
            long res = result.get();
            System.out.println("sum="+res);
        }catch(InterruptedException e){
            e.printStackTrace();
        }catch(ExecutionException e){
            e.printStackTrace();
        }
    }
}

fork()

public final ForkJoinTask<V> fork() 
  • 将任务提交到工程线程的工作队列
  • 线程的工作队列为双端队列
     自身的任务:LIFO(队尾添加,队尾取用)
     偷窃的任务:FIFO(队头添加,队尾取用)

join()

public final V join()
  • 检查调用 join() 的线程是否是 ForkJoinThread 线程。
      如果不是,则阻塞当前线程,等待任务完成。
      如果是,则不阻塞。

  • 查看任务的完成状态,如果已经完成,直接返回结果。

  • 如果任务尚未完成,查看每个工作线程的工作队列
     若工作队列不为空,完成自身任务
     若工作队列为空,实行任务窃取算法
    直到所有工作都完成.

代码解析

  1. forkJoinPool.submit(task)会自动执行task中compute()
  2. compute()将大任务分解成小任务,大任务的返回值依赖于小任务的返回值
  3. compute()的返回值由task.join()获取

Guava 中对线程池的扩展

Guava并非JDK内置的,其中的DirectExecutor对线程池进行了扩展.

特殊的 DirectExecutor 线程池

DirectExecutor 没有真的创建或者使用额外线程, 而是将任务在当前线程中直接执行.

广义线程池
任何一个可以运行 Runnable 实例的模块都可以被视为线程池, 即便它没有真正创建线程
DirectExecutor示例

public class MoreExecutorsDemo {
    public static void main(String[] args) {
        Executor exceutor = MoreExecutors.directExecutor();
        exceutor.execute(() -> System.out.println("I am running in " + Thread.currentThread().getName()));
    }
}
输出:
I am running in main

代码解析

  1. DirectExecutor将Runnable示例在当前线程执行
  2. 可以使用狭义线程池代替DirectExecutor.实现异步和同步在代码上的统一

Daemon 线程池

将普通线程池转换为Daemon线程池

public static ExecutorService getExitingExecutorService(ThreadPoolExecutor executor)

代码演示

public class MoreExecutorsDemo2 {
    public static void main(String[] args) {
        ThreadPoolExecutor exceutor = (ThreadPoolExecutor)Executors.newFixedThreadPool(2);
        MoreExecutors.getExitingExecutorService(exceutor);
        exceutor.execute(() -> System.out.println("I am running in " + Thread.currentThread().getName()));
    }
}
输出:
I am running in pool-1-thread-1(程序结束)

代码分析

  1. getExitingExecutorService()将线程池转换为Daemon线程池.不会阻止程序退出
  2. 普通的线程池必须调用shutdown才能正常退出.

问题

  • future相关类图
  • submit吞异常,而execute不会
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值