Java中的线程进阶:线程池

一、 自定义线程池

在这里插入图片描述

1. Blocking Queue

线程池中需要定义一个阻塞队列,用于存放生产者生产的,需要消费者执行的任务。

public class BlockingQueue<T>{

    //1.任务队列:存放任务对象
    private Deque<T> queue = new ArrayDeque<>();
    //2.锁:锁住任务队列的头部和尾部,避免队列头部一个任务被多个消费者抢走,同时,避免多个生产者向队列尾部存放任务时产生线程安全问题
    private ReentrantLock lock = new ReentrantLock();
    //3.生产者条件变量:当队列中没有任务时,消费者需要阻塞等待
    private Condition fullWaitSet = lock.newCondition();
    //4.消费者条件变量:队列中任务达到容量上限后,生产者需要阻塞等待
    private Condition emptyWaitSet = lock.newCondition();
    //5.容量:定义队列中可以存放的任务容量
    private int capcity;

    public BlockingQueue(){

    }

    public BlockingQueue(int capcity) {
        this.capcity = capcity;
    }

    //1.阻塞获取
    public T take(){
        lock.lock();
        try{
            while(queue.isEmpty()){
                try{
                    emptyWaitSet.await();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        }finally{
            lock.unlock();
        }
    }
    //2.阻塞添加
    public void put(T task){
        lock.lock();
        try{
            while(queue.size() == capcity){
                try{
                    fullWaitSet.await();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            queue.addLast(task);
            emptyWaitSet.signal();
        }finally{
            lock.unlock();
        }
    }
    //3.获取大小
    public int size(){
        lock.lock();
        try{
            return queue.size();
        }finally{
            lock.unlock();
        }
    }

    //4.带超时的阻塞获取
    public T poll(long timeout, TimeUnit unit){
        lock.lock();
        try{
            //将超时时间统一转换为ns
            long nanos = unit.toNanos(timeout);
            while(queue.isEmpty()){
                try{
                    //如果该线程自然醒来,却没有获取到任务,则返回null
                    if(nanos <= 0){
                        return null;
                    }
                    //返回的是剩余的时间,解决了虚假唤醒问题
                    //如果某个被唤醒的线程未达到睡眠时间就被唤醒,且还没有获取到任务,则继续沉睡
                    nanos = emptyWaitSet.awaitNanos(nanos);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            //线程被唤醒后获得了任务,或者自然醒来后获得任务
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        }finally{
            lock.unlock();
        }
    }

    //5.带超时的阻塞添加
    public boolean offer(T task, long timeout, TimeUnit unit){
        lock.lock();
        try{
            //将超时时间统一转换为ns
            long nanos = unit.toNanos(timeout);
            while(queue.size() == capcity){
                try{
                    if(nanos <= 0){
                        return false;
                    }
                    nanos = fullWaitSet.awaitNanos(nanos);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            queue.addLast(task);
            emptyWaitSet.signal();
            return true;
        }finally{
            lock.unlock();
        }
    }

    public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
        lock.lock();
        try {
            //判断队列是否已满
            if(queue.size() == capcity){
                rejectPolicy.reject(this, task);
            }else{//队列没满
                queue.addLast(task);
                emptyWaitSet.signal();
            }
        }finally {
            lock.unlock();
        }
    }
}

2. ThreadPool

public class ThreadPool {
    //任务队列
    private BlockingQueue<Runnable> taskQueue;

    //线程集合
    private HashSet<Worker> workers = new HashSet<>();

    //核心线程数
    private int coreSize;

    //获取任务的超时时间:当线程没有任务时可以存活的最长时间
    private long timeout;

    //时间单位
    private TimeUnit timeUnit;

    //拒绝策略
    private RejectPolicy rejectPolicy;

    public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity, RejectPolicy<Runnable> rejectPolicy) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(queueCapcity);
        this.rejectPolicy = rejectPolicy;
    }

    //执行任务
    public void execute(Runnable task){
        //当任务数没有超过coreSize时,直接将任务交给worker执行,否则将任务加入任务队列暂存起来
        synchronized (workers){
            if(workers.size() < coreSize){
                Worker worker = new Worker();
                workers.add(worker);
                worker.start();
            }else{
            	//尝试将任务放入阻塞队列
                taskQueue.tryPut(rejectPolicy, task); 
            }
        }
    }

    //用于包装线程Thread的类
    class Worker extends Thread{

        private Runnable task;

        public Worker(){

        }
        public Worker(Runnable task){
            this.task = task;
        }

        @Override
        public void run() {
            //执行任务
            //① 当task不为空,执行任务
            //② 当task执行完毕,从任务队列中获取新的任务并执行
//            while(task != null || (task = taskQueue.take()) != null){
            while(task != null || (task = taskQueue.poll(timeout, timeUnit)) != null){
                try{
                    task.run();
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    task = null;
                }
            }
            //当没有任务之后,从任务队列中移除本线程
            synchronized (workers){
                workers.remove(this);
            }
        }
    }
}

3. 拒绝策略

@FunctionalInterface
public interface RejectPolicy<T> {
	//拒绝策略其实有以下几种
    //1.任务队列满了,则让调用者死等
    //2.任务队列满了,则让调用者等待一段时间,在该时间内,若队列有空位则添加,超过该时间还未添加则放弃
    //3.任务队列满了,则让调用者直接放弃
    //4.任务队列满了,让调用者抛出异常
    //5.任务队列满了,让调用者自己执行
    void reject(BlockingQueue<T> queue, T task);
}

4. 测试

public class TestThreadPool {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(1, 1000, TimeUnit.MILLISECONDS, 1, (queue, task) -> {
            //1.任务队列满了,则让调用者死等
            //queue.put(task);
            //2.任务队列满了,则让调用者等待一段时间,在该时间内,若队列有空位则添加,超过该时间还未添加则放弃
            //queue.offer(task);
            //3.任务队列满了,则让调用者直接放弃
            //啥也不做
            //4.任务队列满了,让调用者抛出异常
            throw new RuntimeException("任务执行失败" + task);
            //5.任务队列满了,让调用者自己执行
            //task.run();
        });
        for(int i = 0; i < 4; i++){
            int temp = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000L);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            });
        }
    }
}

二、JDK中的线程池

在这里插入图片描述

1. 线程池状态

ThreadPoolExecutor使用int的高3位来表示线程池状态,低29位表示线程数量。这样的话,就可以将线程池状态与线程个数合二为一,所有的信息可以存储在一个原子变量ctl中,用一次CAS原子操作进行两个值的同时更改。

1.更改源码

// c 为旧值, ctlOf 返回结果为新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));
// rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) { return rs | wc; }

2.状态描述

状态名高3位接收新任务处理阻塞队列任务说明
RUNNING111YY-
SHUTDOWN000NY不会接收新任务,但会处理阻塞队列剩余任务
STOP001NN会中断正在执行的任务,并抛弃阻塞队列任务
TIDYING010--任务全执行完毕,活动线程为 0 即将进入终结
TERMINATED011--终结状态

注意:从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING(RUNNING是负数,其余的是正数)。

2. 构造方法

2.1 详解构造方法

public ThreadPoolExecutor(int corePoolSize,
						int maximumPoolSize,
						long keepAliveTime,
						TimeUnit unit,
						BlockingQueue<Runnable> workQueue,
						ThreadFactory threadFactory,
						RejectedExecutionHandler handler)
  • corePoolSize:核心线程数目(最多保留的线程数),核心线程在完成任务后,不会被销毁。
  • maximumPoolSize:最大线程数目,线程池中最多存放多少线程,是核心线程和救急线程的和。
  • keepAliveTime:生存时间,针对救急线程,救急线程在完成任务后,会等待一定的生存时间,该时间内没有执行任务,则被销毁。
  • unit:时间单位,针对救急线程。
  • workQueue:阻塞队列,当前的核心线程都已经在执行任务时,如果还有任务到来,则被存放在阻塞队列中。
  • threadFactory:线程工厂,可以在线程创建时为该线程起个好名字。
  • handler:拒绝策略,当核心线程都在运行任务,阻塞队列已满,救急线程也都在执行任务,则线程池无法再接受任务会采用拒绝策略,决定如何对待这些任务。

2.2 工作流程

在这里插入图片描述

  • 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
  • 当线程数达到corePoolSize时,并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue队列排队,直到有空闲的线程。
  • 如果队列选择了有界队列,那么任务超过了队列大小时,会创建maximumPoolSize - corePoolSize数目的救急线程来救急,救急线程会处理新加入的任务,而不是阻塞队列中先到的任务。
  • 如果线程到达maximumPoolSize仍然有新任务这时会执行拒绝策略。JDK中提供了四种拒绝策略。
  • 当高峰过去后,超过corePoolSize的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime和unit来控制。

2.3JDK中的四种拒绝策略

在这里插入图片描述

  • AbortPolicy让调用者抛出RejectedExecutionException异常,这是默认策略。
  • CallerRunsPolicy让调用者运行任务。
  • DiscardOldestPolicy放弃队列中最早的任务,本任务取而代之。
  • DiscardPolicy放弃本次任务。

3. 线程池的分类

3.1 固定大小的线程池newFixedThreadPool

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

该线程池的核心线程数就是最大线程数,不会创建应急线程,因此也就无需超时时间。它的阻塞队列是无界的,可以存放任意数量的任务。该线程适用于任务量已知,相对耗时的任务。

3.2 带缓冲线程池newCachedThreadPool

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

该线程的核心线程数是0,最大线程数是Integer.MAX_VALUE,也就是说创建的所有线程都是救急线程且可以无限被创建。救急线程的空闲生存时间是60s,也就是说60s之后救急线程会被回收。该线程的队列采用了SynchronousQueue,该队列没有容量,如果没有取任务的线程,无法向队列中存放任务。该线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕空闲1分钟后释放线程。它适合任务数比较密集,但每个任务执行时间较短的情况。

3.3 单线程线程池newSingleThreadExecutor

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

该线程的。线程数固定为1,任务数多于1时,会放入无界队列排队,从而使所有的任务串行执行。该线程和自己创建一个线程的区别是,如果自己创建的线程任务执行失败而终止,那么没有任何补救措施;但是该线程池还会新建一个线程,保证池的正常工作。该线程和newFixedThreadPool(1)的区别是,Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改,它对外暴露的是ThreadPoolExecutor对象,可以强转后调用setCorePoolSize等方法进行修改;但是Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改,因为FinalizableDelegatedExecutorService应用的是装饰器模式,所以该线程池只对外暴露了ExecutorService接口,因此不能调用ThreadPoolExecutor中特有的方法,也就不能修改。

3.4 任务调度线程池ScheduledExecutorService

在任务调度线程池功能加入之前,可以使用java.util.Timer来实现定时功能,Timer的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。ScheduledExecutorService则解决了Timer所存在的问题。

方法作用
schedule(函数式接口, delay, TimeUnit)在指定的delay事件后开始执行任务,TimeUnit规定了时间单位
scheduleAtFixedRate(函数式接口, initialDelay, period, TimeUnit)在指定延时后以一定的速率执行任务,在开始initialDelay后,每隔period时间执行一次任务,TimeUnit规定了时间单位,如果任务执行时间大于period则任务接连执行
scheduleWithFixedRate(函数式接口, initialDelay, delay, TimeUnit)在指定延时后以一定的速率执行任务,在开始initialDelay后,每次任务执行结束后,间隔delay时间后,执行一次任务,TimeUnit规定了时间单位

4. 线程池的方法

4.1 执行任务

方法作用
void execute(Runnable command)执行任务
Future<T> submit(Callable<T> task)提交任务,使用返回值Future获得任务执行结果
List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)提交tasks中的所有任务
List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)提交tasks中的所有任务,如果当前提交的这些任务在指定时间内没有执行完,则后续任务无法执行
T invokeAny(Collection<? extends Callable<T>> tasks)提交tasks中的所有任务,任意有一个任务执行完毕,则返回此任务执行结果,其他任务取消
T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)带超时时间的invokeAny方法

4.2 关闭线程池

方法作用
void shutdown()将线程池状态变为SHUTDOWN,该方法不会阻塞调用线程的执行,调用线程执行完shutdown()方法会继续执行之后的代码,该线程池内的任务会慢慢执行完毕
List<Runnable> shutdownNow()将线程池状态变为STOP

4.3 其他方法

方法作用
boolean isShutdown()不在RUNNING状态的线程池,此方法就返回true
boolean isTerminated()线程池状态是否是TERMINATED
boolean awaitTermination(long timeout, TimeUnit unit) throw InterruptedException调用shutdown后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待一定的时间,超过时间线程池还任务还没全部运行结束,则不再等待,继续运行

5. 线程池内的异常处理

如果在线程池内,不处理异常,则异常不会被发现,所以,需要我们人为对异常进行处理:
① 人为利用try-catch进行处理。
② 利用返回值Future接收,会将异常信息封装在Future对象中,此时如果调用Future对象,则会报出异常。

6. Tomcat线程池

在这里插入图片描述

  • LimitLatch用来限流,可以控制最大连接个数,类似JUC中的Semaphore。
  • Acceptor只负责接收新的socket连接。
  • Poller只负责监听socket channel是否有可读的I/O事件。
  • 一旦可读,封装一个任务对象(socketProcessor),提交给Executor线程池处理。
  • Executor线程池中的工作线程最终负责处理请求。
  • Tomcat线程池扩展了ThreadPoolExecutor,行为稍有不同。如果总线程数达到 maximumPoolSize,这时不会立刻抛出RejectedExecutionException异常,而是再次尝试将任务放入队列,如果还失败,才抛出RejectedExecutionException异常。

7. 高级线程池-Fork/Join

7.1 基本概念

  • Fork/Join是JDK1.7加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的cpu密集型运算。所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解。
  • Fork/Join在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率。
  • Fork/Join 默认会创建与 cpu 核心数大小相同的线程池。

7.2 基本使用

提交给Fork/Join线程池的任务需要继承RecursiveTask(有返回值)或RecursiveAction(没有返回值),例如下面定义了一个对 1~n 之间的整数求和的任务.
在这里插入图片描述

public static void main(String[] args) {
	ForkJoinPool pool = new ForkJoinPool(4);
	System.out.println(pool.invoke(new AddTask1(5)));
}
class AddTask1 extends RecursiveTask<Integer> {
	int n;
	public AddTask1(int n) {
		this.n = n;
	}
	@Override
	protected Integer compute() {
		// 如果 n 已经为 1,可以求得结果了
		if (n == 1) {
			return n;
		}
		// 将任务进行拆分(fork)
		AddTask1 t1 = new AddTask1(n - 1);
		t1.fork();
		// 合并(join)结果
		int result = n + t1.join();
		return result;
	}
}

7.3 优化使用

在这里插入图片描述

public static void main(String[] args) {
	ForkJoinPool pool = new ForkJoinPool(4);
	System.out.println(pool.invoke(new AddTask3(1, 10)));
}
class AddTask3 extends RecursiveTask<Integer> {
	int begin;
	int end;
	public AddTask3(int begin, int end) {
		this.begin = begin;
		this.end = end;
	}
	@Override
	protected Integer compute() {
		// 5, 5
		if (begin == end) {
			return begin;
		}
		// 4, 5
		if (end - begin == 1) {
			return end + begin;
		}
		
		// 1 5
		int mid = (end + begin) / 2; // 3
		AddTask3 t1 = new AddTask3(begin, mid); // 1,3
		t1.fork();
		AddTask3 t2 = new AddTask3(mid + 1, end); // 4,5
		t2.fork();
		int result = t1.join() + t2.join();
		return result;
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值