Java创建线程池的两种方式及多线程并发测试

Java创建线程的三种方式

  1. 继承Thread类创建线程类
  2. 实现Runnable接口
  3. 通过Callable和Future创建线程

线程池的优点

  • 重用存在的线程,减少对象创建、消亡的开销,性能佳
  • 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
  • 提供定时执行、定期执行、单线程、并发数控制等功能。

Java中创建线程池的两种方式

  • 通过Executors工厂方法创建
  • 通过new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)自定义创建

Executors工厂方法创建

  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行
  	@Test
    public void test() {
        //创建使用单个线程的线程池
        ExecutorService es1 = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            es1.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "正在执行任务");
                }
            });
        }
    }
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待(长度指定为1就变成了上面的)
    @Test
    public void test2() {
        //创建使用固定线程数的线程池
        ExecutorService es2 = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 10; i++) {
            es2.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "正在执行任务");
                }
            });
        }
    }
  • newCachedThreadPool 创建一个可缓存的线程池,如果线程池长度超过处理需求,可灵活回收空闲线程,若无可回收,则新建线程
    @Test
    public void test3() {
        //创建一个会根据需要创建新线程的线程池
        ExecutorService es3 = Executors.newCachedThreadPool();
        for (int i = 0; i < 20; i++) {
            es3.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "正在执行任务");
                }
            });
        }
    }
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行
   @Test
    public void test4() {
        //创建拥有固定线程数量的定时线程任务的线程池
        ScheduledExecutorService es4 = Executors.newScheduledThreadPool(2);
        System.out.println("时间:" + System.currentTimeMillis());
        for (int i = 0; i < 5; i++) {
            es4.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("时间:" + System.currentTimeMillis() + "--" + Thread.currentThread().getName() + "正在执行任务");
                }
            }, 3, TimeUnit.SECONDS);  //表示延迟3秒执行。
            //1, 3, TimeUnit.SECONDS:表示延迟1秒后每3秒执行一次
            //ScheduledExecutorService比Timer更安全,功能更强大

        }
    }
  • newSingleThreadScheduledExecutor创建一个单线程线程池,支持定时及周期性任务执行(就是上面定长指定为1)
    @Test
    public void test5() {
        //创建只有一个线程的定时线程任务的线程池
        ScheduledExecutorService es5 = Executors.newSingleThreadScheduledExecutor();
        System.out.println("时间:" + System.currentTimeMillis());
        for (int i = 0; i < 5; i++) {
            es5.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("时间:" + System.currentTimeMillis() + "--" + Thread.currentThread().getName() + "正在执行任务");
                }
            }, 3, TimeUnit.SECONDS);
        }
    }

new ThreadPoolExecutor()自定义创建

在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。而线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。

 public ThreadPoolExecutor(int corePoolSize, // 1
                              int maximumPoolSize,  // 2
                              long keepAliveTime,  // 3
                              TimeUnit unit,  // 4
                              BlockingQueue<Runnable> workQueue, // 5
                              ThreadFactory threadFactory,  // 6
                              RejectedExecutionHandler handler ) { //7
        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;
    }
  • corePoolSize 表示核心常驻线程池。即使空闲也会在线程池中保活,除非设置了允许核心线程池超时;
  • maximumPoolSize 表示线程池同时执行的最大线程数量;
  • keepAliveTime 表示线程池中的线程空闲时间,线程在销毁前等待新任务的最大时限;
  • unit 表示 keepAliveTime 的单位;
  • workQueue 存放执行前的任务。只会存放通过 execute 函数提交的 Runnable 任务;
  • threadFactory 创建新线程的工厂;
  • handler 线程超限且队列容量也达到最大值时执行受阻的处理策略。

上面参数对应的含义如下,比较重要的是corePoolSize、maximumPoolSize、workQueue、handler

序号名称类型含义
1corePoolSizeint核心线程池大小
2maximumPoolSizeint最大线程池大小
3keepAliveTimelong线程最大空闲时间
4unitTimeUnitkeepAliveTime时间单位
5workQueueBlockingQueue线程等待队列
6threadFactoryThreadFactory线程创建工厂
7handlerRejectedExecutionHandler拒绝策略

这三个稍微有印象即可,重点是下面的四个!!!

keepAliveTime: 当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;

unit: keepAliveTime的单位;

threadFactory: 线程工厂,用于创建线程,一般用默认即可;

corePoolSize和maximumPoolSize

corePoolSize:

线程池的基本大小,即在没有任务需要执行的时候线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。这里需要注意的是:在刚刚创建ThreadPoolExecutor的时候,线程并不会立即启动,而是要等到有任务提交时才会启动,除非调用了prestartCoreThread/prestartAllCoreThreads事先启动核心线程。再考虑到keepAliveTime和allowCoreThreadTimeOut超时参数的影响,所以没有任务需要执行的时候,线程池的大小不一定是corePoolSize。

maximumPoolSize:

线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。这里值得一提的是largestPoolSize,该变量记录了线程池在整个生命周期中曾经出现的最大线程个数。为什么说是曾经呢?因为线程池创建之后,可以调用setMaximumPoolSize()改变运行的最大线程的数目。

corePoolSize与maximumPoolSize的关系

首先corePoolSize肯定是 <= maximumPoolSize。

其他关系如下:

  1. 若当前线程池中线程数 < corePoolSize,则每来一个任务就创建一个线程去执行;
  2. 若当前线程池中线程数 >= corePoolSize,会尝试将任务添加到任务队列。如果添加成功,则任务会等待空闲线程将其取出并执行;
  3. 若队列已满,且当前线程池中线程数 < maximumPoolSize,创建新的线程;
  4. 若当前线程池中线程数 >= maximumPoolSize,则会采用拒绝策略(JDK提供了四种,下面会介绍到)。

注意:关系3是针对的有界队列,无界队列永远都不会满,所以只有前2种关系。

这段话详细了描述了线程池对任务的处理流程,这里用个图总结一下

img

workQueue任务队列

它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列;

1、直接提交队列:设置为SynchronousQueue队列,SynchronousQueue是一个特殊的BlockingQueue,它没有容量,没执行一个插入操作就会阻塞,需要再执行一个删除操作才会被唤醒,反之每一个删除操作也都要等待对应的插入操作。

public class ThreadPoolExecutorParam {
    private static ExecutorService pool;

    /**
     * 这种直接提交队列相当于没有缓存数量,提交的任务不会被保存,总是会马上提交执行,所以超过core以后,最多就还能创建max-core数量个线程
     */
    @Test
    public void test1() {

        pool = new ThreadPoolExecutor(2, 5, 1000, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 6; i++) {
            pool.execute(new ThreadTask());
        }
    }
}    

2、有界的任务队列:有界的任务队列可以使用ArrayBlockingQueue实现,如下所示

使用ArrayBlockingQueue有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量,若大于maximumPoolSize,则执行拒绝策略。在这种情况下,线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在corePoolSize以下,反之当任务队列已满时,则会以maximumPoolSize为最大线程数上限。

  /**
     * 参数  core:2   max:5    queue:10
     * 1、 如果超过15个线程就会执行拒绝,总共只有5个线程执行,也就是超过12个以后,会最多创建3个线程执行,所以就是最多15个线程创建
     * 2、如果创建的线程数量小于12个,就始终只有2个线程在执行
     * 3、也就是说超出的数量大于core+queue=12的线程,会根据max-core的数量=3,新创建最多3个线程执行
     * 4、如果设置的队列容量足够大,也跟无界队列一个意思?
     */
    @Test
    public void test2() {
        pool = new ThreadPoolExecutor(2, 5, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(10),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 13; i++) {
            pool.execute(new ThreadTask());
        }
    }

3、无界的任务队列:有界任务队列可以使用LinkedBlockingQueue实现,如下所示

使用无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是你corePoolSize设置的数量,也就是说在这种情况下maximumPoolSize这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池的线程数达到corePoolSize后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。

    @Test
    public void test3() {
        pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 13; i++) {
            pool.execute(new ThreadTask());
        }

    }

4、优先任务队列: 优先任务队列通过PriorityBlockingQueue实现,下面我们通过一个例子演示下

public class ThreadPool {
    private static ExecutorService pool;
    public static void main( String[] args )
    {
        //优先任务队列
        pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new PriorityBlockingQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
          
        for(int i=0;i<20;i++) {
            pool.execute(new ThreadTask(i));
        }    
    }
}

public class ThreadTask implements Runnable,Comparable<ThreadTask>{
    
    private int priority;
    
    public int getPriority() {
        return priority;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    public ThreadTask() {
        
    }
    
    public ThreadTask(int priority) {
        this.priority = priority;
    }

    //当前对象和其他对象做比较,当前优先级大就返回-1,优先级小就返回1,值越小优先级越高
    public int compareTo(ThreadTask o) {
         return  this.priority>o.priority?-1:1;
    }
    
    public void run() {
        try {
            //让线程阻塞,使后续任务进入缓存队列
            Thread.sleep(1000);
            System.out.println("priority:"+this.priority+",ThreadName:"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    
    }
}

执行结果:

priority:0,ThreadName:pool-1-thread-1
priority:9,ThreadName:pool-1-thread-1
priority:8,ThreadName:pool-1-thread-1
priority:7,ThreadName:pool-1-thread-1
priority:6,ThreadName:pool-1-thread-1
priority:5,ThreadName:pool-1-thread-1
priority:4,ThreadName:pool-1-thread-1
priority:3,ThreadName:pool-1-thread-1
priority:2,ThreadName:pool-1-thread-1
priority:1,ThreadName:pool-1-thread-1

大家可以看到除了第一个任务直接创建线程执行外,其他的任务都被放入了优先任务队列,按优先级进行了重新排列执行,且线程池的线程数一直为corePoolSize,也就是只有一个。

通过运行的代码我们可以看出PriorityBlockingQueue它其实是一个特殊的无界队列,它其中无论添加了多少个任务,线程池创建的线程数也不会超过corePoolSize的数量,只不过其他队列一般是按照先进先出的规则处理任务,而PriorityBlockingQueue队列可以自定义规则根据任务的优先级顺序先后执行。

拒绝策略

一般我们创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,但种模式下如果出现任务队列已满且线程池创建的线程数达到你设置的最大线程数时,这时就需要你指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池"超载"的情况。ThreadPoolExecutor自带的拒绝策略如下:

1、AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作;

2、CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行;

3、DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交;

4、DiscardPolicy策略:该策略会默默丢弃无法处理的任务,不予任何处理。当然使用此策略,业务场景中需允许任务的丢失;

以上内置的策略均实现了RejectedExecutionHandler接口,当然你也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略,我们看下示例代码:

public class ThreadPool {
    private static ExecutorService pool;
    public static void main( String[] args )
    {
        //自定义拒绝策略
        pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5),
                Executors.defaultThreadFactory(), new RejectedExecutionHandler() {
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.out.println(r.toString()+"执行了拒绝策略");
                
            }
        });
          
        for(int i=0;i<10;i++) {
            pool.execute(new ThreadTask());
        }    
    }
}

public class ThreadTask implements Runnable{    
    public void run() {
        try {
            //让线程阻塞,使后续任务进入缓存队列
            Thread.sleep(1000);
            System.out.println("ThreadName:"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    
    }
}

执行结果:

com.hhxx.test.ThreadTask@33909752执行了拒绝策略
com.hhxx.test.ThreadTask@55f96302执行了拒绝策略
com.hhxx.test.ThreadTask@3d4eac69执行了拒绝策略
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1

可以看到由于任务加了休眠阻塞,执行需要花费一定时间,导致会有一定的任务被丢弃,从而执行自定义的拒绝策略;

使用线程池测试并发

平常我们写代码的时候,有一些业务逻辑就要考虑在多个线程并发的情况下是否会有问题,这时候就可以采用线程池创建多个线程进行并发访问!!!

当然这只是单机测试,如果还想测试多台机器测试,可以用docker部署多台进行并发访问!!!!

直接new线程测试:

注意:

  • 假设线程里面执行的逻辑要5s,那thread1.start以后一定要睡眠足够的时间,不然很可能导致线程没有执行完毕!!!
  • 这里要睡眠的原因是因为这是跑测试用例,而实际生产部署以后,服务一直在运行,所以不会出现执行不完!!!

假如第一个线程执行要10s,第二个线程要5s,但是只睡眠了8s,只能执行完第二个线程,导致第一个线程没有执行完毕(正常情况下是直接new的多个线程跑一样的代码,这里只是说明自己跑单元测试的时候要注意的问题)

  	@Test
    public void test() throws InterruptedException {
        //假设线程里面执行的逻辑要10s,那thread1.start一定要睡眠足够的时间,不然很可能没有执行完毕!!!
        Thread thread1 = new Thread(() -> {
            System.out.println("线程1:" + JSON.toJSONString(System.currentTimeMillis()));
            try {
                Thread.sleep(10000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程1:" + JSON.toJSONString(System.currentTimeMillis()));
        });
        //假设线程里面执行的逻辑要10s,那如果只sleep 8S就会导致没有执行完毕!!!

        Thread thread2 = new Thread(() -> {
            System.out.println("线程2:" + JSON.toJSONString(System.currentTimeMillis()));
            try {
                //睡眠5s,模拟执行业务逻辑时间
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程2:" + JSON.toJSONString(System.currentTimeMillis()));
        });

        thread1.start();
        thread2.start();
        //平常使用单元测试的时候:这里一定要注意睡眠的时间要大于业务逻辑处理的时间,不然会直接结束程序,导致线程执行的逻辑没有执行完毕
        Thread.sleep(8000L);

    }

输出结果:这里线程1就没有输出Thread-0完成任务,因为执行时间大于10s,但是只睡眠了8s就结束了程序!!

Thread-0正在执行任务
Thread-1正在执行任务
Thread-1完成任务

使用线程池测试:

	 @Test
    public void test2() throws InterruptedException {
        //创建使用固定线程数的线程池
        ExecutorService es = new ThreadPoolExecutor(2, 5, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(10),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 2; i++) {
            es.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName() + "正在执行任务");
                        //睡眠5s,模拟执行业务逻辑时间
                        Thread.sleep(5000L);
                        System.out.println(Thread.currentThread().getName() + "完成任务");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
 		//平常使用单元测试的时候:这里一定要注意睡眠的时间要大于业务逻辑处理的时间,不然会直接结束程序,导致线程执行的逻辑没有执行完毕
        Thread.sleep(6000L);

    }

输出结果:

pool-1-thread-1正在执行任务
pool-1-thread-2正在执行任务
pool-1-thread-1完成任务
pool-1-thread-2完成任务

参考文章:

https://www.cnblogs.com/dafanjoy/p/9729358.html

https://blog.csdn.net/dabusiGin/article/details/105327873

https://blog.csdn.net/qq_35275233/article/details/87893337

https://www.cnblogs.com/jxxblogs/p/11655670.html

https://www.jianshu.com/p/c41e942bcd64

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Apple_Web

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值