阻塞队列与线程池

阻塞队列与线程池

阻塞队列

阻塞队列类型

有七种类型的阻塞队列,但常用的线程池中主要用到三种阻塞队列,所以主要看看这三种阻塞队列。

  • ArrayBlockingQueue:由数组构成的有界阻塞队列,如果工作中用到阻塞队列,这个是首选。
  • LinkedBlockingQueue:由链表构成的有界(但长度默认为Integer.MAX_VALUE)的阻塞队列。如果使用,一定要注意设置默认值。
  • SynchronousQueue:只有一个元素的阻塞队列,即存即用。每个一个put操作必须被take后才能继续存入,反之亦然。

入队出队操作

方法类型抛出异常特殊值阻塞超时
插入add(e)offer(e)put(e)offer(e, time, unit)
移除remove()poll()take()poll(time, unit)
检查(队首,判空)element()peek()不可用不可用

生产消费者模式

传统版v1版本

使用 synchronized, wait, notifyAll

class SharedData{
    private int number = 0;

    public synchronized void increment() {
        while (number != 0) {
            try {
                //不生产,使用while循环判断防止虚假唤醒
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "生产,number == " + number);
        notifyAll();
    }

    public synchronized void decrement() {
        while (number == 0) {
            try {
                //不消费
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "消费,number == " + number);
        notifyAll();
    }
}
/**
 * 初始化一个0,两个线程对其操作,生产一个就消费一个(线程操作资源类)
 */
public class ProductConsumerTraditionTest {
    public static void main(String[] args) {
        SharedData sharedData = new SharedData();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                sharedData.increment();
            }
        }, "t1").start();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                sharedData.decrement();
            }
        }, "t2").start();
    }
}

t1生产,number == 1
t2消费,number == 0
t1生产,number == 1
t2消费,number == 0
t1生产,number == 1
t2消费,number == 0
t1生产,number == 1
t2消费,number == 0
t1生产,number == 1
t2消费,number == 0

Process finished with exit code 0

传统版v2版本

使用ReentrantLock, Condition.await, Condition.signalAll

class SharedData2 {
    private int number = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void increment() {
        lock.lock();
        try {
            while (number != 0) {
                //不生产,使用while循环判断防止虚假唤醒
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "生产,number == " + number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement() {
        lock.lock();
        try {
            while (number == 0) {
                //不消费
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "消费,number == " + number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

/**
 * 初始化一个0,两个线程对其操作,生产一个就消费一个(线程操作资源类)
 */
public class ProductConsumerTraditionTest2 {
    public static void main(String[] args) {
        SharedData2 sharedData = new SharedData2();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                sharedData.increment();
            }
        }, "t1").start();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                sharedData.decrement();
            }
        }, "t2").start();
    }
}

由于synchronized和ReentrantLock都保证了内存可见性,所以资源变量不需要用volatile修饰。

阻塞队列v3版

使用BlockQueue,我们不需要关心什么时候阻塞线程,什么时候唤醒线程,具体的细节BlockQueue帮我们实现了。否则我们要实现业务逻辑,自己控制这些细节还要兼顾性能和线程安全,大大增加了开发难度。

class SharedData3 {
    //开启,生产+消费
    private volatile boolean FLAG = true;
    private AtomicInteger mAtomicInteger = new AtomicInteger();

    //这里传的是接口
    private BlockingQueue<String> mBlockingQueue = null;

    public SharedData3(BlockingQueue<String> blockingQueue) {
        mBlockingQueue = blockingQueue;
        System.out.println(mBlockingQueue.getClass().getName());
    }

    public void myProduct() throws Exception {
        String data;
        boolean retValue;
        while (FLAG) {
            data = mAtomicInteger.incrementAndGet() + "";
            retValue = mBlockingQueue.offer(data, 2L, TimeUnit.SECONDS);
            if (retValue) {
                System.out.println(Thread.currentThread().getName() + "," + data + "插入成功");
            } else {
                System.out.println(Thread.currentThread().getName() + "," + data + "插入失败");
            }
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(Thread.currentThread().getName() + ",生产停止,FLAG = false");
    }

    public void myConsumer() throws Exception {
        String data;
        while (FLAG) {
            data = mBlockingQueue.poll(2L, TimeUnit.SECONDS);
            if (null == data || "".equals(data)) {
                FLAG = false;
                System.out.println(Thread.currentThread().getName() + ",超过2秒没有取到数据,消费退出");
                return;
            }
            System.out.println(Thread.currentThread().getName() + "," + data + "取出成功");
        }
    }

    public void stop() {
        this.FLAG = false;
    }
}

public class ProductConsumerTraditionTest3 {
    public static void main(String[] args) {
        SharedData3 mSharedData3 = new SharedData3(new ArrayBlockingQueue<>(10));
        new Thread(() -> {
            System.out.println("生产线程启动");
            try {
                mSharedData3.myProduct();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "product").start();

        new Thread(() -> {
            System.out.println("消费线程启动");
            try {
                mSharedData3.myConsumer();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "consume").start();

        try {
            Thread.sleep(5000);
            mSharedData3.stop();
            System.out.println("5s时间到,MainThread叫停,生产消费结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

java.util.concurrent.ArrayBlockingQueue
生产线程启动
product,1插入成功
消费线程启动
consume,1取出成功
product,2插入成功
consume,2取出成功
product,3插入成功
consume,3取出成功
consume,4取出成功
product,4插入成功
product,5插入成功
consume,5取出成功
5s时间到,MainThread叫停,生产消费结束
product,生产停止,FLAG = false
consume,超过2秒没有取到数据,消费退出

Process finished with exit code 0

线程池

线程池

线程池的主要工作是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务。如果线程数量超过了线程池最大线程数则排队等候,等其他线程执行完毕后,再从队列中取出任务来执行。

其主要特点为:线程复用;控制最大并发数、管理线程

  • 降低资源消耗:通过复用已创建的线程降低线程频繁地创建和销毁的消耗(创建和销毁大概2M)。
  • 提高响应速度:当任务达到时,任务可以不需要等到线程创建就能立即执行。
  • 提高现成的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一地分配,调优和监控。

几种线程池

public class ThreadPoolTest {
    public static void main(String[] args) {
        //定长为5的线程池
//        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        //单任务线程池
//        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        //无界线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();


        //模拟10个用户来办理业务
        try {
            for (int i = 0; i < 10; i++) {
                threadPool.execute(() -> {
                    try {
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() + ",办理业务");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}
  1. newFixedThreadPool:

        /**
         * Creates a thread pool that reuses a fixed number of threads
         * operating off a shared unbounded queue.  At any point, at most
         * {@code nThreads} threads will be active processing tasks.
         * If additional tasks are submitted when all threads are active,
         * they will wait in the queue until a thread is available.
         * If any thread terminates due to a failure during execution
         * prior to shutdown, a new one will take its place if needed to
         * execute subsequent tasks.  The threads in the pool will exist
         * until it is explicitly {@link ExecutorService#shutdown shutdown}.
         *
         * @param nThreads the number of threads in the pool
         * @return the newly created thread pool
         * @throws IllegalArgumentException if {@code nThreads <= 0}
         */
        public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
    

    定长线程池,适合执行长期任务。其底层数据结构使用的是LinkedBlockingQueue。

  2. newSingleThreadExecutor

        /**
         * Creates an Executor that uses a single worker thread operating
         * off an unbounded queue. (Note however that if this single
         * thread terminates due to a failure during execution prior to
         * shutdown, a new one will take its place if needed to execute
         * subsequent tasks.)  Tasks are guaranteed to execute
         * sequentially, and no more than one task will be active at any
         * given time. Unlike the otherwise equivalent
         * {@code newFixedThreadPool(1)} the returned executor is
         * guaranteed not to be reconfigurable to use additional threads.
         *
         * @return the newly created single-threaded Executor
         */
        public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
    

    单任务线程池,适用于一个任务一个任务执行的场景,线程池中只有一个线程。其底层数据结构使用的是LinkedBlockingQueue。

  3. newCachedThreadPool

        /**
         * Creates a thread pool that creates new threads as needed, but
         * will reuse previously constructed threads when they are
         * available.  These pools will typically improve the performance
         * of programs that execute many short-lived asynchronous tasks.
         * Calls to {@code execute} will reuse previously constructed
         * threads if available. If no existing thread is available, a new
         * thread will be created and added to the pool. Threads that have
         * not been used for sixty seconds are terminated and removed from
         * the cache. Thus, a pool that remains idle for long enough will
         * not consume any resources. Note that pools with similar
         * properties but different details (for example, timeout parameters)
         * may be created using {@link ThreadPoolExecutor} constructors.
         *
         * @return the newly created thread pool
         */
        public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
    

无界线程池,线程池中线程数量不固定,可以自动扩容。适合执行很多短期异步的小程序或者负载较轻的服务。其底层数据结构使用的是SynchronousQueue。

线程池七大参数

看源码可以看到:

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

ThreadPoolExecutor有五个参数,点进去:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

this,为该类的另一构造方法,再点进去:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

可以看到实际上有七个参数,封装了两个,还有五个参数暴露给开发者自定义。

  1. corePoolSize:线程池中的常驻核心线程数。
  2. maximumPoolSize:线程池能够容纳的同时执行的最大线程数,此值必须大于1。
  3. keepAliveTime:多余的空闲线程存活时间。。。
  4. unit:参数三的时间单位。
  5. BlockingQueue:阻塞队列,被提交但尚未被执行的任务队列。
  6. ThreadFactory:生成线程池中工作线程的工厂,用于创建线程,一般设置为默认即可。
  7. RejectedExecutionHandler:拒绝策略,表示当队列满了,并且工作线程数大于等于线程池中的最大线程数(maximumPoolSize)

线程池运行过程

  • 假如corePoolSize=2,maximumPoolSize=5,此时常驻线程已经被占用且正在执行任务,又进来三个线程,这三个线程并不会执行任务,而是放在阻塞队列中,等待核心线程执行完毕。假设阻塞队列的长度=maximumPoolSize-corePoolSize=3(实际上阻塞队列长度取决于底层阻塞队列的数据结构,LinkedBlockingQueue默认长度为Integer.MAX_VALUE),假设又进来两个线程,此时常驻核心线程数已满,阻塞队列也满了,线程池开始扩容,使得corePoolSize = maximumPoolSize = 5,阻塞队列里的任务加入到新扩容的线程中,后进来的两个线程放入阻塞队列中。
  • 上述情况,假设扩容完成后,corePoolSize = maximumPoolSize = 5满了,阻塞队列也满了,此时就需要拒绝策略,也就是RejectedExecutionHandler参数。
  • 随着时间的推移,假设任务并不多了,所有线程任务都执行完毕了,这时当前线程数量超过corePoolSize,当空闲时间达到keepAliveTime时,多余的空闲线程就会被销毁知道只剩下corePoolSize个线程数。
  • 定长线程池看其构造方法可以看到:corePoolSize == maximumPoolSize。

线程池拒绝策略

  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常,阻止系统正常运行。
  2. CallRunsPolicy:调用者运行策略,一种调节机制,该策略既不会抛弃任务,也不会抛弃异常,而是将某些任务回退到调用者,从而降低新任务的流量。
  3. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中尝试再次提交当前任务。
  4. DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许丢弃任务,这个是最好的方案。

使用哪种线程池

拒绝策略验证

先来看看AbortPolicy什么时候会报异常:

public class CustomThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
        try {
            for (int i = 0; i < 9; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "执行任务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

根据前面总结,可以猜到,当需要执行的任务 = maximumPoolSize + 阻塞队列长度时,会报RejectedExecutionException,运行结果如下:

pool-1-thread-2执行任务
pool-1-thread-2执行任务
pool-1-thread-2执行任务
pool-1-thread-2执行任务
pool-1-thread-1执行任务
pool-1-thread-3执行任务
pool-1-thread-4执行任务
pool-1-thread-5执行任务
java.util.concurrent.RejectedExecutionException

将拒绝策略改成:CallerRunsPolicy,可以看到:

pool-1-thread-2执行任务
main执行任务
pool-1-thread-2执行任务
pool-1-thread-1执行任务
pool-1-thread-2执行任务
main执行任务
pool-1-thread-2执行任务
pool-1-thread-1执行任务
pool-1-thread-3执行任务
pool-1-thread-4执行任务
pool-1-thread-5执行任务

Process finished with exit code 0

没有报异常,且拒绝策略是返回该调用者线程执行。

再将拒绝策略改成:DiscardOldestPolicy,可以看到:

pool-1-thread-2执行任务
pool-1-thread-3执行任务
pool-1-thread-1执行任务
pool-1-thread-1执行任务
pool-1-thread-3执行任务
pool-1-thread-2执行任务
pool-1-thread-5执行任务
pool-1-thread-4执行任务

Process finished with exit code 0

丢弃了两个任务,且丢弃的两个任务是阻塞队列中等待最久的任务。

再看看最后一个拒绝策略

pool-1-thread-1执行任务
pool-1-thread-2执行任务
pool-1-thread-2执行任务
pool-1-thread-2执行任务
pool-1-thread-1执行任务
pool-1-thread-3执行任务
pool-1-thread-4执行任务
pool-1-thread-5执行任务

Process finished with exit code 0

当阻塞队列满了,直接拒绝准备新加入的两个任务。

自定义线程池

以上参数的配置需要根据不同业务场景配置不同的线程池,主要分为以下两种。

  • CPU密集型:任务需要大量CPU运算,但没有阻塞,CPU一直全速运行,所以配置尽可能是少的线程数

    一般公式:CPU核数 + 1,或者CPU核数*2,一般设置CPU * 2即可。

  • IO密集型:任务中有大量的IO操作,容易阻塞,单线程阻塞会浪费CPU资源,所以配置多一点的线程数

    一般公式:CPU核数 / (1 - 阻塞系数),阻塞系数在0.8-0.9之间,比如8核CPU,8/(1-0.9) = 80个线程数。

这里以CPU密集型为例自定义线程池:

/**
 * 静态内部类单例,懒加载
 */
public class CPUThreadPool {

    private ThreadPoolExecutor executor;

    /**
     * CPU密集型线程池
     * corePoolSize:常驻线程,为CPU线程数
     * maximumPoolSize:最大线程数,为corePoolSize的两倍,阻塞队列满了扩容会使用
     */
    private CPUThreadPool() {
        /**
         * 给corePoolSize赋值:当前设备可用处理器核心数*2 + 1,能够让cpu的效率得到最大程度执行(有研究论证的)
         */
        //核心常驻线程池的数量,同时能够执行的线程数量
        int corePoolSize = Runtime.getRuntime().availableProcessors();
        //最大线程池数量,表示当缓冲队列满的时候能继续容纳的等待任务的数量
        int maximumPoolSize = corePoolSize * 2;
        //非常驻线程存活时间
        long keepAliveTime = 1L;
        //非常驻线程存活时间单位
        TimeUnit unit = TimeUnit.MINUTES;
        executor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                new SynchronousQueue<>(),//这里阻塞队列长度为1,由于不能设置为0,所以可能存在1个任务被阻塞的隐患,但测试中没有出现过
//                new LinkedBlockingQueue<>(1),
                Executors.defaultThreadFactory(),
                new NewThreadForRejectHandler()
        );

    }

    private static class ThreadPoolHolder {
        private static CPUThreadPool mInstance = new CPUThreadPool();
    }

    public static CPUThreadPool getInstance() {
        return ThreadPoolHolder.mInstance;
    }

    //自定义拒绝策略:被拒绝的任务在一个新的线程中执行
    private class NewThreadForRejectHandler implements RejectedExecutionHandler {
        @Override
        public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) {
            new Thread(runnable, "new thread for reject").start();
            System.out.println(("新线程中的任务被拒绝:Task " + runnable.toString() + "rejected from " + executor.toString()));
//            LogUtils.e("新线程中的任务被拒绝:Task " + runnable.toString() + "rejected from " + executor.toString());
        }
    }

    /**
     * 执行任务
     */
    public void execute(Runnable runnable) {
        if (runnable == null || executor == null) return;
        executor.execute(runnable);
    }

    /**
     * 从线程池中移除任务
     */
    public void remove(Runnable runnable) {
        if (runnable == null || executor == null) return;
        executor.remove(runnable);
    }

    /**
     * 关闭线程池
     * 不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
     */
    public void shutdown() {
        if (executor == null) return;
        executor.shutdown();
    }

    /**
     * 线程池中任务是否执行完
     *
     * @return
     */
    public boolean isTerminated() {
        if (executor == null) return false;
        return executor.isTerminated();
    }

    public int getActiveThreadCount() {
        return executor.getActiveCount();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值