ThreadPool

写在前边

一直对线程池的理解就是下边的代码。之后看到别人发的文章,线程池的坑,才发现原来线程池也有这么多的坑。所以就想着好好的学习一下线程池。

ExecutorService executorService = Executors.newFixedThreadPool(10);

自定义简单线程池

线程池的大概结构,可以看到主要有两部分线程池队列

image-20230528163216446

阻塞队列

因为在线程池中要用到队列,所以先写队列的代码

基本属性:阻塞队列、锁、队列的容量、队列满的时候的状态量、队列空的时候的状态量

// 1.定义阻塞队列
private Deque<T> queue = new ArrayDeque<>();

// 2.因为是高并发,所以要有锁
private ReentrantLock lock = new ReentrantLock();

// 3.定义一个容量的大小
private int capacity;

// 4.定义一个队列满的时候的状态量,表示,不能向队列中放元素了
private Condition fullWaitState = lock.newCondition();

// 5.定义一个队列空的状态量,表示,线程不能再消费了
private Condition emptyWaitState = lock.newCondition();

构造方法

public BlockingQueue(int queueCapacity) {
    this.capacity = queueCapacity;
}

三个方法:获取任务、放入任务、队列的最大容量。

放入任务(死等)

因为阻塞队列不是线程安全的,所以在放入任务的时候要加锁。

队列可能是满了,所以要判断当前的队列容量。如果已经满了,那么就要阻塞这个线程。如果队列没有满,那么就调用对列的相关方法将任务放入队列中。

在一个新的任务放入队列之前,有可能有其他线程因为队列空了,陷入了等待状态,所以在放入任务之后,要唤醒从队列中拿任务的线程。

最后释放锁。

public void put(T element){
    lock.lock();
    try {
        // 是不是满了
        while (queue.size() == capacity) {
            // 满了,阻塞
            try {
                fullWaitState.await();
                log.info("正在等待添加到任务队列...,{}", element);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 不满了,就能放进去了
        queue.addLast(element);
        log.info("放入队列,任务:{}", element);
        // 如果现在又因为队列是空的而阻塞的线程,就唤醒
        emptyWaitState.signal();
    } finally {
        lock.unlock();
    }
}

但是这种方式有不好的一点,如果执行一个任务的时间特别长,并且队列已经满了,那么,因为fullWaitState.await();这个代码,线程会死等直到线程执行完毕。

放入任务(带超时时间阻塞添加)

代码和上边的一段代码基本一样。nacos = fullWaitState.awaitNanos(nacos);会返回当前还需要等待的时间。

public boolean poll(T element, long timeout, TimeUnit unit) {
    lock.lock();
    long nacos = unit.toNanos(timeout);
    try {
        // 是不是满了
        while (queue.size() == capacity) {
            // 满了,阻塞
            try {
                if (nacos <= 0) {
                    return false;
                }
                nacos = fullWaitState.awaitNanos(nacos);
                log.info("正在等待添加到任务队列...,{}", element);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 不满了,就能放进去了
        queue.addLast(element);
        log.info("放入队列,任务:{}", element);
        // 如果现在又因为队列是空的而阻塞的线程,就唤醒
        emptyWaitState.signal();
        return true;
    } finally {
        lock.unlock();
    }
}

获取任务(阻塞获取)

可能有多个线程同时获取任务,加锁。

队列可能是空的,空的话,还取什么任务呀,要判断。

是空的,就等待,阻塞等待。

不是空的,就从队列的头拿到元素。

在获得任务的时候或者之前,已经有任务因为队列满了,所以线程被阻塞了,所以要唤醒那写阻塞的线程,让他们向队列里放任务。

最后,释放锁。

public T take() {
    // 加锁取
    lock.lock();
    try {
        // 队列不能是空
        while (queue.isEmpty()) {
            // 阻塞
            try {
                emptyWaitState.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 不空,就能拿元素
        T t = queue.removeFirst();
        // 有位置了,可以往里放元素了
        fullWaitState.signal();
        return t;
    } finally {
        lock.unlock();
    }
}

获取任务(带超时时间)

如果获取时间已经大于了最大能等的时间,返回空,表示没有拿到任务。

// 带超时时间的获取
public T poll(long timeout, TimeUnit unit) {
    // 加锁取
    lock.lock();
    // 将时间转化成为统一的时间
    long nanos = unit.toNanos(timeout);
    try {
        // 队列不能是空
        while (queue.isEmpty()) {
            // 阻塞
            try {
                if (nanos <= 0) {
                    return null;
                }
                // 返回值是剩下的等待时间
                nanos = emptyWaitState.awaitNanos(nanos);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 不空,就能拿元素
        T t = queue.removeFirst();
        // 有位置了,可以往里放元素了
        fullWaitState.signal();
        return t;
    } finally {
        lock.unlock();
    }
}

获取队列最大容量

public int size() {
    return capacity;
}

线程池

线程池基本属性:阻塞队列线程集合核心线程数超时时间拒绝策略

拒绝策略:就是当线程池中的所有线程都被占用且阻塞队列也满了之后,线程池怎么对待后续想要添加进来的任务。这里将拒绝策略下放给了用户,让用户自定义

// 要有阻塞队列
private BlockingQueue<Runnable> taskQueue;
// 线程集合
private HashSet<Worker> workers = new HashSet();

// 核心线程数
private int coreSize;

// 线程的超时时间
private long timeout;

// 时间单位
private TimeUnit timeUnit;

// 拒绝策略
private RejectPolicy rejectPolicy;

拒绝策略

函数式接口

@FunctionalInterface
interface RejectPolicy<T> {
    void reject(BlockingQueue<T> queue, T task);
}

构造方法

taskQueue创建线程池的时候就要有,不能让用户自己传。

线程集合也要自己定义

其他的都让用户自己传

// 构造方法,线程数,超时时间是自定义的
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapacity, RejectPolicy<Runnable> rejectPolicy) {
    this.coreSize = coreSize;
    this.timeout = timeout;
    this.timeUnit = timeUnit;
    // 要有一个容量
    this.taskQueue = new BlockingQueue<>(queueCapacity);
    this.rejectPolicy = rejectPolicy;
}

线程集合

内部类。

要让Woker继承Thread这样才能创建线程

线程要执行任务,所以要有Runnable的实例化对象,且通过构造函数传进来。

重写run方法,这样,让Worker的实例化对象调用start()方法的时候,就会自动执行类里边的run方法。

只有传进来的任务不是空的话,才执行代码task != null

线程池时可以复用的,所以,不能因为传进来的task是空的就终止线程了

还要看阻塞队列中是否还有任务。(task = taskQueue.poll(timeout, timeUnit)) != null)

如果有的话,就执行任务。

如果没有,就移除这个worker即可。

class Worker extends Thread {
    private Runnable task;

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

    @Override
    public void run() {
        // 因为是线程池,所以不能线程结束了,就立即终止线程
        while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
            // 执行任务
            try {
                log.info("正在执行...,{}", task);
                task.run();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                task = null;
            }
        }
        // 退出集合,说明这个线程已经执行完所有的方法了,就要移除掉
        synchronized (workers) {
            log.info("worker被移除,{}", this);
            workers.remove(this);
        }
    }
}

线程池执行方法

线程池开始执行实际上是在内部生成Woker对象来执行的。

加锁,避免创建过多的线程。

判断,如果当前线程集合中的线程小于核心线程数的话。就可以为当前任务分配新的线程Worker worker = new Worker(task);

将线程放入线程集合中workers.add(worker);

执行worker.start();

public void execute(Runnable task) {
    // Workers是线程不安全的,所以要加锁
    // 传进来task,交给一个线程来执行
    // 要判断能不能放进到works
    synchronized (workers) {
        if (workers.size() < coreSize) {
            // 说明和兴线程数还是用空余的,可以执行
            Worker worker = new Worker(task);
            log.info("Worker对象:{},任务:{}", worker, task);
            // 放入线程集合中
            workers.add(worker);
            // 执行任务
            worker.start();
        } else {
            // 说明核心线程已经用完了,将任务放到阻塞队列中
            // taskQueue.put(task);
            // 1 队列满,死等
            // 2 带超时等待
            // 3 让调用者放弃任务
            // 4 让调用者抛出异常
            // 5 让调用者自己执行任务
            taskQueue.tryPut(task, rejectPolicy);
        }
    }
}

调用执行策略

如果核心线程都在忙碌的情况下,就将任务放入阻塞队列中。

加锁

判断阻塞队列的大小是否已经超过设定的最大值了

没有的话,就将任务放入队列中。

已经超过的话,就要执行决绝策略了。

public void tryPut(T task, RejectPolicy<T> rejectPolicy) {
    lock.lock();
    try {
        if (queue.size() < capacity) {
            queue.addLast(task);
            log.info("放入队列,任务:{}", task);
            // 如果现在又因为队列是空的而阻塞的线程,就唤醒
            emptyWaitState.signal();
        } else {
            rejectPolicy.reject(this, task);
        }
    } finally {
        lock.unlock();
    }
}

拒绝策略的可能策略

// 1 队列满,死等
// taskQueue.put(task);
// 2 带超时等待
// taskQueue.poll(task, 1500, TimeUnit.MILLISECONDS);
// 3 让调用者放弃任务
// log.debug("放弃,{}", task);
// 4 让调用者抛出异常
// throw new RuntimeException("任务执行失败");
// 5 让调用者自己执行任务
// task.run();

入口函数

public static void main(String[] args) {
    ThreadPool threadPool = new ThreadPool(1, 1000, TimeUnit.MILLISECONDS, 1, (taskQueue, task)->{

        // 1 队列满,死等
        // taskQueue.put(task);
        // 2 带超时等待
        // taskQueue.poll(task, 1500, TimeUnit.MILLISECONDS);
        // 3 让调用者放弃任务
        // log.debug("放弃,{}", task);
        // 4 让调用者抛出异常
        // throw new RuntimeException("任务执行失败");
        // 5 让调用者自己执行任务
        // task.run(); 


    });

    for (int i = 0; i < 3; i++) {
        int j = i;
        threadPool.execute(() -> {
            try {
                TimeUnit.SECONDS.sleep(10000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("llllllllll===,{}", j);
        });
    }
}

测试:

死等

一个线程,阻塞队列容量是1,所以,一个任务放入了Woker中,另外一个放入了阻塞队列中。最后一个任务就只能死等。

image-20230528172735872

超时等待更改TimeUnit.SECONDS.sleep(1);可以等1.5秒,线程最多执行的时间是1秒,所以,都可以执行完

image-20230528173219880

主动放弃,什么也不执行。更改TimeUnit.*SECONDS*.sleep(2);会执行完0,1。2不会输出

image-20230528173518787

让调用者抛出异常

image-20230528173725079

调用者自己执行任务,因为队列满了之后就要执行决绝策略,就会直接执行代码。

image-20230528173905268

完整代码

/**
 * @author HGL
 * @time 2023-05-27
 */
@Slf4j
public class ThreadPoolTest01 {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(1, 1000, TimeUnit.MILLISECONDS, 1, (taskQueue, task)->{

            // 1 队列满,死等
            // taskQueue.put(task);
            // 2 带超时等待
            // taskQueue.poll(task, 1500, TimeUnit.MILLISECONDS);
            // 3 让调用者放弃任务
            // log.info("放弃,{}", task);
            // 4 让调用者抛出异常
            // throw new RuntimeException("任务执行失败");
            // 5 让调用者自己执行任务
            task.run();


        });

        for (int i = 0; i < 3; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("llllllllll===,{}", j);
            });
        }
    }
}


@FunctionalInterface
interface RejectPolicy<T> {
    void reject(BlockingQueue<T> queue, T task);
}

@Slf4j(topic = "threadpool")
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 queueCapacity, RejectPolicy<Runnable> rejectPolicy) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        // 要有一个容量
        this.taskQueue = new BlockingQueue<>(queueCapacity);
        this.rejectPolicy = rejectPolicy;
    }


    // 执行方法
    public void execute(Runnable task) {
        // Workers是线程不安全的,所以要加锁
        // 传进来task,交给一个线程来执行
        // 要判断能不能放进到works
        synchronized (workers) {
            if (workers.size() < coreSize) {
                // 说明和兴线程数还是用空余的,可以执行
                Worker worker = new Worker(task);
                log.info("Worker对象:{},任务:{}", worker, task);
                // 放入线程集合中
                workers.add(worker);
                // 执行任务
                worker.start();
            } else {
                // 说明核心线程已经用完了,将任务放到阻塞队列中
                // taskQueue.put(task);
                // 1 队列满,死等
                // 2 带超时等待
                // 3 让调用者放弃任务
                // 4 让调用者抛出异常
                // 5 让调用者自己执行任务
                taskQueue.tryPut(task, rejectPolicy);
            }
        }
    }

    class Worker extends Thread {
        private Runnable task;

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

        @Override
        public void run() {
            // 因为是线程池,所以不能线程结束了,就立即终止线程
            while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
                // 执行任务
                try {
                    log.info("正在执行...,{}", task);
                    task.run();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    task = null;
                }
            }
            // 退出集合,说明这个线程已经执行完所有的方法了,就要移除掉
            synchronized (workers) {
                log.info("worker被移除,{}", this);
                workers.remove(this);
            }
        }
    }
}

@Slf4j
class BlockingQueue<T> {

    public BlockingQueue(int queueCapacity) {
        this.capacity = queueCapacity;
    }

    // 1.定义阻塞队列
    private Deque<T> queue = new ArrayDeque<>();

    // 2.因为是高并发,所以要有锁
    private ReentrantLock lock = new ReentrantLock();

    // 3.定义一个容量的大小
    private int capacity;
    // 4.定义一个队列满的时候的状态量,表示,不能向队列中放元素了
    private Condition fullWaitState = lock.newCondition();

    // 5.定义一个队列空的状态量,表示,线程不能再消费了
    private Condition emptyWaitState = lock.newCondition();


    // 带超时时间的获取
    public T poll(long timeout, TimeUnit unit) {
        // 加锁取
        lock.lock();
        // 将时间转化成为统一的时间
        long nanos = unit.toNanos(timeout);
        try {
            // 队列不能是空
            while (queue.isEmpty()) {
                // 阻塞
                try {
                    if (nanos <= 0) {
                        return null;
                    }
                    // 返回值是剩下的等待时间
                    nanos = emptyWaitState.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 不空,就能拿元素
            T t = queue.removeFirst();
            // 有位置了,可以往里放元素了
            fullWaitState.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    // 从队列中取出的方法,阻塞获取
    public T take() {
        // 加锁取
        lock.lock();
        try {
            // 队列不能是空
            while (queue.isEmpty()) {
                // 阻塞
                try {
                    emptyWaitState.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 不空,就能拿元素
            T t = queue.removeFirst();
            // 有位置了,可以往里放元素了
            fullWaitState.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    // 相对列中放入元素的方法
    public void put(T element){
        lock.lock();
        try {
            // 是不是满了
            while (queue.size() == capacity) {
                // 满了,阻塞
                try {
                    fullWaitState.await();
                    log.info("正在等待添加到任务队列...,{}", element);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 不满了,就能放进去了
            queue.addLast(element);
            log.info("放入队列,任务:{}", element);
            // 如果现在又因为队列是空的而阻塞的线程,就唤醒
            emptyWaitState.signal();
        } finally {
            lock.unlock();
        }
    }

    // 带超时时间阻塞添加
    public boolean poll(T element, long timeout, TimeUnit unit) {
        lock.lock();
        long nacos = unit.toNanos(timeout);
        try {
            // 是不是满了
            while (queue.size() == capacity) {
                // 满了,阻塞
                try {
                    if (nacos <= 0) {
                        return false;
                    }
                    nacos = fullWaitState.awaitNanos(nacos);
                    log.info("正在等待添加到任务队列...,{}", element);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 不满了,就能放进去了
            queue.addLast(element);
            log.info("放入队列,任务:{}", element);
            // 如果现在又因为队列是空的而阻塞的线程,就唤醒
            emptyWaitState.signal();
            return true;
        } finally {
            lock.unlock();
        }
    }

    // 当前队列容量
    public int size() {

        return capacity;
    }


    public void tryPut(T task, RejectPolicy<T> rejectPolicy) {
        lock.lock();
        try {
            if (queue.size() < capacity) {
                queue.addLast(task);
                log.info("放入队列,任务:{}", task);
                // 如果现在又因为队列是空的而阻塞的线程,就唤醒
                emptyWaitState.signal();
            } else {
                rejectPolicy.reject(this, task);
            }
        } finally {
            lock.unlock();
        }
    }
}

ThreadPoolExecutor

image-20230528192102998

线程池状态

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

image-20230528192142738

shutdown只是不再接收新的任务了,但是还是会把当前的任务以及阻塞队列中的任务执行完的

构造方法

在阿里巴巴手册中,明确了不能使用Executors这个工具类来定义线程池。所以就要必要搞清楚线程池的构造方法每个参数的含义

public ThreadPoolExecutor(int corePoolSize,   // 核心线程数
                              int maximumPoolSize,   // 最大线程数,核心线程数+应急线程数=最大线程数
                              long keepAliveTime,    // 应急线程存活的时间
                              TimeUnit unit,		 // 时间单位
                              BlockingQueue<Runnable> workQueue,    // 阻塞队列
                              ThreadFactory threadFactory,      // 线程工厂,主要就是给线程起名字的
                              RejectedExecutionHandler handler)  // 拒绝策略

keepAliveTime和unit是相对于应急线程说的,对于核心线程是没有存活时间这个概念的

线程池中任务执行的具体流程

  1. 初始时,线程池中没有任务,当一个任务进入线程池后,线程池会创建一个线程来执行任务。

  2. 当线程池中的线程数量到达了corePoolSize,这个时候再加入到线程池中的任务就会被放在阻塞队列中排队,直到有核心线程空闲出来。

  3. 如果在阻塞队列满之前都没有核心线程空闲,那么线程池就会创建maximumPoolSize - corePoolSize 的应急线程来处理新加入进来的任务

  4. 如果线程池中的线程已经到达了maximumPoolSize,仍然有新的任务进来的话,线程池就会执行拒绝策略。jdk默认提供了四种拒绝策略:

    1. AbortPolicy :让调用者抛出异常:RejectedExecutionException,也是默认的拒绝策略
    2. CallerRunsPolicy:让调用者自己运行任务
    3. DiscardPolicy:放弃本次任务
    4. DiscardOldestPolicy:放弃队列中最早的任务,本任务取而代之
  5. 当任务的高峰过去之后,也就是核心线程有空余的或者阻塞队列不满,应急线程这时候就无事可做。等到了keepAliveTime和unit约定的时间后,应急线程会释放。

根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池

newFixedThreadPool

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

可以看到这种线程池的特点有:

  1. 没有应急线程
  2. 阻塞队列是无界的,可以放任意多的任务。

因为阻塞队列是无界的,如果不停的往里放任务,可能造成OOM

适用于任务量已知,相对耗时的任务

public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        pool.execute(() -> {
            log.info("1");
        });

        pool.execute(() -> {
            log.info("2");
        });

        pool.execute(() -> {
            log.info("3");
        });
}

image-20230528194401491

ThreadFactory自定义线程名称

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

image-20230528194711239

@Slf4j
public class TestThreadPoolExecutors {
    public static void main(String[] args) {
        // 线程工厂,就是为了给线程起好名字

        ExecutorService selfNamePool = Executors.newFixedThreadPool(4, new ThreadFactory() {
            private AtomicInteger t = new AtomicInteger(1);
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "myThreadPool-" + t.getAndIncrement());
            }
        });

        selfNamePool.execute(() -> {
            log.info("1");
        });

        selfNamePool.execute(() -> {
            log.info("2");
        });

        selfNamePool.execute(() -> {
            log.info("3");
        });

    }
}

image-20230528194902109

newCachedThreadPool

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

这种线程池的特点:

  1. 没有核心线程,全是应急线程。
  2. 应急线程的存活时间是60秒
  3. 应急线程可以无限创建
  4. 这个阻塞队列也很特殊

SynchronousQueue这个队列类型,除了第一元素能放入到队列中,其他的元素都要一拿一放,一手交钱,一手交货才行。

@Slf4j
public class TestSynchronousQueue {

    public static void main(String[] args) throws InterruptedException {
        SynchronousQueue<Integer> tasks = new SynchronousQueue<>();


        new Thread(() -> {
            try {
                log.info("putting...{}", 1);
                tasks.put(1);
                log.info("putted...{}", 1);

                log.info("putting...{}", 2);
                tasks.put(2);
                log.info("putted...{}", 2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            try {
                log.info("taking...{}", 1);
                tasks.take();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t2").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            try {
                log.info("taking...{}", 2);
                tasks.take();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t3").start();
    }
}

一拿一放

image-20230528195531728

适合任务数比较密集,但每个任务执行时间较短的情况

newSingleThreadExecutor

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

这种线程池的特点是:

  1. 核心线程数等于最大线程数,也就是没有应急线程
  2. 也是无界阻塞队列

适用场景:希望任务可以串行执行的时候可以考虑这个线程池。

自己创建一个线程串行执行任务和newSingleThreadExecutor的区别

  1. 自己创建的线程如果执行过程中,有异常产生的话,就会结束执行,没有补救措施。
  2. newSingleThreadExecutor这种方式,如果执行过程中有异常产生了,那么线程池会再创建一个线程,进行补救
@Slf4j
public class TestNewSingleThreadExecutor {
    public static void main(String[] args) {
        new Thread(() -> {
            log.info("1");
            log.info("2");

            int i = 1 / 0;

            log.info("3");
        }, "t1").start();
    }
}

image-20230528200339134

@Slf4j
public class TestNewSingleThreadExecutor {
    public static void main(String[] args) {
        /*new Thread(() -> {
            log.info("1");
            log.info("2");

            int i = 1 / 0;

            log.info("3");
        }, "t1").start();*/


        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        threadPool.execute(() -> {
            log.info("1");
        });


        threadPool.execute(() -> {
            log.info("2");
            int i = 1 / 0;
        });

        threadPool.execute(() -> {
            log.info("3");
        });
    }
}

image-20230528200805523

newSingleThreadExecutor和newFixedThreadExecutor(1)的区别

还是从构造器说起:

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

返回的对象是ThreadPoolExecutor类型的,但是这个类是ExecutorService的实例,也就是有一个引用指向了ThreadPoolExecutor这个类。

正常情况下,执行调用ExecutorService中定义的方法:

image-20230528201949146

但是如果向下转型,就是ThreadPoolExecutor类型的,那能执行的方法就多了。其中有一个叫设置线程数量的方法,也就是后面线程数量是能变的。并不是1个不变的。

image-20230528202056037

newSingleThreadExecutor的构造器

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

返回的是FinalizableDelegatedExecutorService类型

static class FinalizableDelegatedExecutorService
    extends DelegatedExecutorService {
    FinalizableDelegatedExecutorService(ExecutorService executor) {
        super(executor);
    }
    protected void finalize() {
        super.shutdown();
    }
}

看这个类DelegatedExecutorService,没有提供很丰富的方法

static class DelegatedExecutorService extends AbstractExecutorService {
        private final ExecutorService e;
        DelegatedExecutorService(ExecutorService executor) { e = executor; }
        public void execute(Runnable command) { e.execute(command); }
        public void shutdown() { e.shutdown(); }
        public List<Runnable> shutdownNow() { return e.shutdownNow(); }
        public boolean isShutdown() { return e.isShutdown(); }
        public boolean isTerminated() { return e.isTerminated(); }
        public boolean awaitTermination(long timeout, TimeUnit unit)
            throws InterruptedException {
            return e.awaitTermination(timeout, unit);
        }
        public Future<?> submit(Runnable task) {
            return e.submit(task);
        }
        public <T> Future<T> submit(Callable<T> task) {
            return e.submit(task);
        }
        public <T> Future<T> submit(Runnable task, T result) {
            return e.submit(task, result);
        }
        public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
            throws InterruptedException {
            return e.invokeAll(tasks);
        }
        public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                             long timeout, TimeUnit unit)
            throws InterruptedException {
            return e.invokeAll(tasks, timeout, unit);
        }
        public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
            throws InterruptedException, ExecutionException {
            return e.invokeAny(tasks);
        }
        public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                               long timeout, TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException {
            return e.invokeAny(tasks, timeout, unit);
        }
    }

所以

Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改

FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因 此不能调用

ThreadPoolExecutor 中特有的方法 Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改

提交任务

image-20230528202836015

以下方法不一一测试了

// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);
// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
 throws InterruptedException;
// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
 long timeout, TimeUnit unit)
 throws InterruptedException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
 throws InterruptedException, ExecutionException;

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
 long timeout, TimeUnit unit)
 throws InterruptedException, ExecutionException, TimeoutException;

测试部分

public class TestSubmit {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(1);

        // 输出1
        threadPool.submit(() -> {
            System.out.println(1);
        });

        // 输出ok
        Future<String> result = threadPool.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "ok";
            }
        });
        String s = result.get();
        // 输出执行结束
        Future<String> result1 = threadPool.submit(() -> {

        }, "执行结束");
        System.out.println(result1.get());
    }
}

关闭线程池

image-20230528203950566

shutdown

线程池状态变为shutdown

不会接收新的任务

但已经提交过的任务会执行完

不会阻塞调用线程的执行

public class TestShutdown {
    public static void main(String[] args) {

        ExecutorService executorService = Executors.newFixedThreadPool(1);

        executorService.execute(() -> {
            System.out.println(1);
        });

        executorService.execute(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(2);
        });

        executorService.execute(() -> {
            System.out.println(3);
        });

        System.out.println("shutdown");
        executorService.shutdown();

        executorService.execute(() -> {
            System.out.println(4);
        });
    }
}

结果,2输出出来了,但是4没有输出出来

image-20230528204732256

shutdownNow

线程池状态变为 STOP

不会接收新任务

会将队列中的任务返回

并用 interrupt 的方式中断正在执行的任务

public class TestShutdown {
    public static void main(String[] args) {

        ExecutorService executorService = Executors.newFixedThreadPool(1);

        executorService.execute(() -> {
            System.out.println(1);
        });

        executorService.execute(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(2);
        });

        executorService.execute(() -> {
            System.out.println(3);
        });

        System.out.println("shutdown");
        List<Runnable> runnables = executorService.shutdownNow();

        System.out.println("=============");

        runnables.forEach(Runnable::run);

    }
}

测试结果,打断了

image-20230528205208380

判断线程池中的线程是否全部都结束了,可以用如下方法

boolean isTerminated();

在调用shutdown()方法之后,线程池中的线程是有可能继续执行任务的,但是主线程是不会等着线程池中的线程执行完之后再结束的。如果主线程想在线程池中的线程全部结束之后做点事情的话。可以调用下边的方法。

// 时间表示等待的时间,其实可以用Callable接口来获得Future类型,然后调用get()方法,也能阻塞主线程。
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

模式之工作线程(Work Thread)

定义

有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现 就是线程池,也体现了经典设计模式中的享元模式(复用)。

例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那 么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message)

注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率

例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成 服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工.

饥饿

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

  • 两个工人是同一个线程池中的两个线程
  • 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
    • 客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
    • 后厨做菜:没啥说的,做就是了
  • 比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好
  • 但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,饥饿

正常情况下,两个线程,分别左一个人的点菜,做菜

@Slf4j
public class TestHungry {
    // 菜品
    public static final List<String> FOOD_MENU = Arrays.asList("西红柿炒鸡蛋", "肥牛汤锅", "黄焖鸡米饭", "冒菜");

    public static final Random RANDOM = new Random();

    public static void main(String[] args) {

        // 创建线程,两个
        ExecutorService pool = Executors.newFixedThreadPool(2);

        pool.execute(() -> {
            log.info("点菜...");
            Future<String> result = pool.submit(() -> {
                log.info("做菜");
                return FOOD_MENU.get(RANDOM.nextInt(FOOD_MENU.size()));
            });
            try {
                log.info("{}做好了", result.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}

结果

image-20230529140857536

但是现在来两个人呢

@Slf4j
public class TestHungry {
    // 菜品
    public static final List<String> FOOD_MENU = Arrays.asList("西红柿炒鸡蛋", "肥牛汤锅", "黄焖鸡米饭", "冒菜");

    public static final Random RANDOM = new Random();

    public static void main(String[] args) {

        // 创建线程,两个
        ExecutorService pool = Executors.newFixedThreadPool(2);

        pool.execute(() -> {
            log.info("点菜...");
            Future<String> result = pool.submit(() -> {
                log.info("做菜");
                return FOOD_MENU.get(RANDOM.nextInt(FOOD_MENU.size()));
            });
            try {
                log.info("{}做好了", result.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        pool.execute(() -> {
            log.info("点菜...");
            Future<String> result = pool.submit(() -> {
                log.info("做菜");
                return FOOD_MENU.get(RANDOM.nextInt(FOOD_MENU.size()));
            });
            try {
                log.info("{}做好了", result.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}

结果,两个线程都在点菜,没人做菜了,就产生了饥饿问题。

image-20230529141014727

解决办法,可以多几个线程,多一个就行修改代码ExecutorService pool = Executors.newFixedThreadPool(3);

image-20230529141157071

但是这种扩展性不强,不推荐。

应该使用不同的任务类型使用不容的线程池这种方式

@Slf4j
public class TestHungry {
    // 菜品
    public static final List<String> FOOD_MENU = Arrays.asList("西红柿炒鸡蛋", "肥牛汤锅", "黄焖鸡米饭", "冒菜");

    public static final Random RANDOM = new Random();

    public static void main(String[] args) {

        // 创建线程,两个
        ExecutorService waiterPool = Executors.newFixedThreadPool(1);
        ExecutorService cookPool = Executors.newFixedThreadPool(1);

        waiterPool.execute(() -> {
            log.info("点菜...");
            Future<String> result = cookPool.submit(() -> {
                log.info("做菜");
                return FOOD_MENU.get(RANDOM.nextInt(FOOD_MENU.size()));
            });
            try {
                log.info("{}做好了", result.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        waiterPool.execute(() -> {
            log.info("点菜...");
            Future<String> result = cookPool.submit(() -> {
                log.info("做菜");
                return FOOD_MENU.get(RANDOM.nextInt(FOOD_MENU.size()));
            });
            try {
                log.info("{}做好了", result.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}

结果

image-20230529141428025

创建多少线程池合适

  • 过小会导致程序不能充分地利用系统资源、容易导致饥饿
  • 过大会导致更多的线程上下文切换,占用更多内存

CPU密集型运算

通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因 导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费

I/O密集型运算

CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程 RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。

经验公式如下:

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

例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式

4 * 100% * 100% / 50% = 8

任务调度线程池(newScheduledThreadPool)

让线程池执行任务延迟执行

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

Timer

优点是简单易用

缺点是所有的任务都是串行执行,效率低。如果执行期间产生异常,主线程会终止。后续的任务就无法处理。

串行执行

@Slf4j
public class TestTimer {
    public static void main(String[] args) {
        Timer timer = new Timer();

        TimerTask timerTask1 = new TimerTask() {
            @Override
            public void run() {
                log.info("task1...");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        TimerTask timerTask2 = new TimerTask() {
            @Override
            public void run() {
                log.info("task2...");
            }
        };
        log.info("starting...");
        timer.schedule(timerTask1, 1000);
        timer.schedule(timerTask2, 1000);
    }
}

如果不是串行执行的话,那么task2...的输出时间应该比starting...晚一秒

实际上晚了3秒,说明是等task1完成之后才执行的task2

image-20230529142743948

异常

在task1中手动加一个异常1/0

@Slf4j
public class TestTimer {
    public static void main(String[] args) {
        Timer timer = new Timer();

        TimerTask timerTask1 = new TimerTask() {
            @Override
            public void run() {
                log.info("task1...");
                int i = 1 / 0;
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        TimerTask timerTask2 = new TimerTask() {
            @Override
            public void run() {
                log.info("task2...");
            }
        };

        log.info("starting...");
        timer.schedule(timerTask1, 1000);
        timer.schedule(timerTask2, 1000);
    }
}

报错了,且任务2没有完成

image-20230529142931712

使用newScheduledThreadPool改写

不是串行

@Slf4j
public class TestNewScheduledThreadPool {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);

        log.info("starting...");


        pool.schedule(() -> {
            log.info("task1...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 1, TimeUnit.SECONDS);

        pool.schedule(() -> {
            log.info("task2...");
        }, 1, TimeUnit.SECONDS);
    }
}

结果,看到时间显示,task1的睡眠2秒并没有影响task2

image-20230529143607752

异常

手动添加异常

@Slf4j
public class TestNewScheduledThreadPool {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);

        log.info("starting...");


        pool.schedule(() -> {
            log.info("task1...");
            int i = 1 / 0;
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 1, TimeUnit.SECONDS);

        pool.schedule(() -> {
            log.info("task2...");
        }, 1, TimeUnit.SECONDS);
    }
}

结果,正常执行,且没有报错。

image-20230529143729611

但是在程序中,异常是一定要处理的

处理异常的方式

  1. 自己捕获
@Slf4j
public class TestNewScheduledThreadPool {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);

        log.info("starting...");


        pool.schedule(() -> {
            log.info("task1...");
            try {
                int i = 1 / 0;
            } catch (Exception e) {
                e.printStackTrace();
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 1, TimeUnit.SECONDS);

        pool.schedule(() -> {
            log.info("task2...");
        }, 1, TimeUnit.SECONDS);
    }
}

成功捕获异常

image-20230529143911801

  1. 利用Future.get()将异常输出
@Slf4j
public class TestNewScheduledThreadPool {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);

        log.info("starting...");


        ScheduledFuture<Boolean> result = pool.schedule(() -> {
            log.info("task1...");
            int i = 1 / 0;
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return true;
        }, 1, TimeUnit.SECONDS);

        log.info(String.valueOf(result.get()));
    }
}

可以看到,将异常输出出来了。

image-20230529144336422

使用newScheduledThreadPool来创建定时任务

每隔一秒钟执行一次任务

log.info("starting...");

pool.scheduleAtFixedRate(() -> {
    log.info("task1...");
}, 1, 1, TimeUnit.SECONDS);

结果

image-20230529144824395

这个方法虽然是延迟一秒执行,但是还是要看方法的具体执行时间的,如果执行时间大于了延迟的时间,那么方法执行完就会立即执行,如果小于延迟时间,是会等到设定的延迟时间的。

log.info("starting...");

pool.scheduleAtFixedRate(() -> {
    log.info("task1...");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}, 1, 1, TimeUnit.SECONDS);

第一次执行,18-17=1延迟了一秒,但是这个时候再要执行的时候,发现,前一个还没执行完,就会等执行完,然后立刻执行。

image-20230529145154579

但是这个方法

pool.scheduleWithFixedDelay(() -> {
    log.info("task1...");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}, 1, 1, TimeUnit.SECONDS);

会在前一个方法执行完之后再延迟的,所以就是隔三秒执行一次

image-20230529145603473

scheduleAtFixedRate在前一个周期开始执行的时候就已经开始延迟了,但是延迟到了1秒之后,发现前一个还在执行。就等执行完

scheduleWithFixedDelay在前一个周期执行结束之后才开延迟,所以会延迟3秒

定时任务

public class FixTimeTask {
    // 每周四晚上六点执行任务

    // 分析现在到周四要延迟多少

    // 然后间隔多少

    // 全部以毫秒为单位

    public static void main(String[] args) {
        // 间隔时间,一周的时间
        long intervalTime = 1000 * 60 * 60 * 24 * 7;
        // 现在的时间
        LocalDateTime now = LocalDateTime.now();
        // 到周四的时间
        LocalDateTime time = now.withNano(0).withSecond(0).withMinute(0).withHour(18).with(DayOfWeek.THURSDAY);

        // 但是有个问题,如果今天已经是周五了,那么time显示的还是这周的周四,很明显不对,应该是下周,所以要判断下
        if (now.isAfter(time)) {
            time = time.plusWeeks(1);
        }

        long delayTime = Duration.between(now, time).toMillis();

        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);

        pool.scheduleAtFixedRate(() -> {}, delayTime, intervalTime, TimeUnit.MILLISECONDS);

    }
}

最后

根据黑马满老师讲的并发编程整理,满老师讲非常的nice

https://www.bilibili.com/video/BV16J411h7Rd/?spm_id_from=333.337.search-card.all.click

posted @ 2023-05-30 21:10  Sstarry  阅读(8)  评论(0)    收藏  举报