public final class ThreadUtil extends Object
ThreadGroup线程组表示一个线程的集合。
此外,线程组也可以包含其他线程组。线程组构成一棵树,
在树中,除了初始线程组外,每个线程组都有一个父线程组。
允许线程访问有关自己的线程组的信息,但是不允许它访问有关其线程组的父线程组或其他任何线程组的信息。所有线程都隶属于一个线程组。那可以是一个默认线程组,亦可是一个创建线程时明确指定的组。
在创建之初,线程被限制到一个组里,而且不能改变到一个不同的组。每个应用都至少有一个线程从属于系统线程组。
若创建多个线程而不指定一个组,它们就会自动归属于系统线程组。
线程组也必须从属于其他线程组。
必须在构建器里指定新线程组从属于哪个线程组。
若在创建一个线程组的时候没有指定它的归属,则同样会自动成为系统线程组的一名属下。因此,一个应用程序中的所有线程组最终都会将系统线程组作为自己的“父” 之所以要提出“线程组”的概念,一般认为,是由于“安全”或者“保密”方面的理由。
根据Arnold和Gosling的说法:“线程组中的线程可以修改组内的其他线程,包括那些位于分层结构最深处的。一个线程不能修改位于自己所在组或者下属组之外的任何线程”
Callable和Runnable的区别如下:
Callablesince jdk1.5,而Runnablesince jdk1.0.Callable定义的方法是Callable.call(),而Runnable定义的方法是Runnable.run().Callable的Callable.call()方法有返回值,而Runnable的Runnable.run()方法没有返回值Callable的Callable.call()方法可抛出异常,而Runnable的Runnable.run()方法不能抛出异常
| Modifier and Type | Method and Description |
|---|---|
static <T> void |
execute(List<T> list,
int eachSize,
Map<String,?> paramsMap,
PartitionPerHandler<T> partitionPerHandler)
给定一个待解析的
list,设定每个线程执行多少条 eachSize,传入一些额外的参数 paramsMap,使用自定义的
partitionPerHandler,自动构造多条线程并运行. |
static <T> void |
execute(List<T> list,
int eachSize,
Map<String,?> paramsMap,
PartitionRunnableBuilder<T> partitionRunnableBuilder)
给定一个待解析的
list,设定每个线程执行多少条 eachSize,传入一些额外的参数 paramsMap,使用自定义的
partitionRunnableBuilder,自动构造多条线程并运行. |
static <T> void |
execute(List<T> list,
int eachSize,
PartitionPerHandler<T> partitionPerHandler)
给定一个待解析的
list,设定每个线程执行多少条 eachSize,使用自定义的
partitionRunnableBuilder,自动构造多条线程并运行. |
static <T> void |
execute(List<T> list,
int eachSize,
PartitionRunnableBuilder<T> partitionRunnableBuilder)
给定一个待解析的
list,设定每个线程执行多少条 eachSize,使用自定义的
partitionRunnableBuilder,自动构造多条线程并运行. |
static <T> void |
execute(List<T> list,
PartitionThreadConfig partitionThreadConfig,
Map<String,?> paramsMap,
PartitionPerHandler<T> partitionPerHandler)
给定一个待解析的
list,设定每个线程执行多少条 eachSize,传入一些额外的参数 paramsMap,使用自定义的
partitionPerHandler,自动构造多条线程并运行. |
static void |
execute(Runnable runnable,
int threadCount)
创建指定数量 threadCount 的线程,并执行.
|
static void |
sleep(long milliseconds)
强制当前正在执行的线程 休眠(暂停执行)
milliseconds 毫秒. |
static void |
startAndJoin(Thread[] threads)
|
public static final void sleep(long milliseconds)
milliseconds 毫秒.
该方法简便的地方在于,捕获了异常和记录了日志,不需要再写这些额外代码
- The thread does not lose ownership of any monitors.
- 当线程睡眠时,它睡在某个地方,在苏醒之前不会返回到可运行状态,
当睡眠时间到期,则返回到可运行状态。sleep()方法不能保证该线程睡眠到期后就开始执行- sleep()是静态方法,只能控制当前正在运行的线程
- sonarqube不建议在单元测试中使用 sleep, 参见 "Thread.sleep" should not be used in tests squid:S2925
public void testNegative1(){
ThreadUtil.sleep(1);
}
sleep()睡眠时,保持对象锁,仍然占有该锁;
而wait()睡眠时,释放对象锁。
milliseconds - 睡眠的毫秒数,可以使用 TimeInterval 常量IllegalArgumentException - 如果 milliseconds 参数是负数TimeUnit.SECONDS.sleep(3);,
Thread.sleep(long)public static void execute(Runnable runnable, int threadCount)
多线程调用某个 API 20 次
ThreadUtil.execute(new Runnable(){
public void run(){
String uri = "http://127.0.0.1:8084?name=jinxin&age=18";
LOGGER.debug(HttpClientUtil.get(uri, toMap("country", "china")));
}
}, 20);
如果 runnable 是null,抛出 NullPointerException
如果 threadCount <=0,抛出 IllegalArgumentException
runnable - the runnablethreadCount - 线程数量public static <T> void execute(List<T> list, int eachSize, PartitionRunnableBuilder<T> partitionRunnableBuilder)
list,设定每个线程执行多少条 eachSize,使用自定义的
partitionRunnableBuilder,自动构造多条线程并运行.
比如同步库存,一次从MQ或者其他接口中得到了5000条数据,如果使用单线程做5000次循环,势必会比较慢,并且影响性能; 如果调用这个方法,传入eachSize=100, 那么自动会开启5000/100=50 个线程来跑功能,大大提高同步库存的速度
其他的适用场景还有诸如同步商品主档数据,同步订单等等这类每个独立对象之间没有相关联关系的数据,能提高执行速度和效率
对于以下代码:模拟10个对象/数字,循环执行任务(可能是操作数据库等)
public void testExecuteTest() throws InterruptedException{ Date beginDate = now(); List<Integer>list = toList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); for (Integer integer : list){ //------ //模拟 do something //--------- Thread.sleep(1 * MILLISECOND_PER_SECONDS); } LOGGER.info("use time: [{}]", formatDuration(beginDate)); }统计总耗时时间 需要 use time:10秒28毫秒
此时你可以调用此方法,改成多线程执行:public void testExecuteTestUsePartitionRunnableBuilder() throws InterruptedException{ Date beginDate = now(); List<Integer>list = toList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); //每个线程执行2条数据, 没有自定义 paramsMap //将会自动创建 list.size()/2 =5个 线程执行 //每个线程执行的,将会是 PartitionRunnableBuilder build 返回的 Runnable ThreadUtil.execute(list, 1, new PartitionRunnableBuilder<Integer>(){ @Override public Runnable build(final List<Integer>perBatchList,PartitionThreadEntity partitionThreadEntity,Map<String, ?>paramsMap){ return new Runnable(){ @Override public void run(){ for (Integer integer : perBatchList){ //------ //模拟 do something //--------- try{ Thread.sleep(1 * MILLISECOND_PER_SECONDS); }catch (InterruptedException e){ LOGGER.error("", e); } } } }; } }); LOGGER.info("use time: [{}]", formatDuration(beginDate)); }统计总耗时时间 需要 use time:2秒36毫秒
对于上述的case,如果将 eachSize 参数由2 改成1, 统计总耗时时间 需要 use time:1秒36毫秒
可见 调用该方法,使用多线程能节省执行时间,提高效率; 但是也需要酌情考虑eachSize大小,合理的开启线程数量
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性;
需要注意合理的评估list的大小和eachSize比率;
不建议listsize很大,比如 20W,而eachSize值很小,比如2 ,那么会开启20W/2=10W个线程;此时建议考虑 线程池的实现方案
如果
list是null,抛出NullPointerException
如果list是empty,抛出IllegalArgumentException
如果eachSize <=0,抛出IllegalArgumentException
如果partitionRunnableBuilder是null,抛出NullPointerException
T - the generic typelist - 执行解析的list
比如 100000个 User,不能为null或者empty
eachSize - 每个线程执行多少个对象
比如 一个线程解析 1000个 User, 那么程序内部 会自动创建 100000/1000个 线程去解析;
必须>0
partitionRunnableBuilder - 每个线程做的事情,不能为nullDefaultPartitionThreadExecutor,
DefaultPartitionThreadExecutor.INSTANCEpublic static <T> void execute(List<T> list, int eachSize, PartitionPerHandler<T> partitionPerHandler)
list,设定每个线程执行多少条 eachSize,使用自定义的
partitionRunnableBuilder,自动构造多条线程并运行.
主要是用来简化 execute(List, int, PartitionRunnableBuilder) 调用
对于以下代码:
ThreadUtil.execute(list, 5, new PartitionRunnableBuilder可以重构成:<String>(){ @Override public Runnable build(final List<String>perBatchList,PartitionThreadEntity partitionThreadEntity,Map<String, ?>paramsMap){ return new Runnable(){ @Override public void run(){ map.putAll(handle(perBatchList, noList)); } }; } });ThreadUtil.execute(list, 5, new PartitionPerHandler上述事例,可以从 14 行代码, 精简到 7 行代码<String>(){ @Override public void handle(List<String>perBatchList,PartitionThreadEntity partitionThreadEntity,Map<String, ?>paramsMap){ map.putAll(CopyrightTest.this.handle(perBatchList, noList)); } });
比如同步库存,一次从MQ或者其他接口中得到了5000条数据,如果使用单线程做5000次循环,势必会比较慢,并且影响性能; 如果调用这个方法,传入eachSize=100, 那么自动会开启5000/100=50 个线程来跑功能,大大提高同步库存的速度
其他的适用场景还有诸如同步商品主档数据,同步订单等等这类每个独立对象之间没有相关联关系的数据,能提高执行速度和效率
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性;
需要注意合理的评估list的大小和eachSize比率;
不建议listsize很大,比如 20W,而eachSize值很小,比如2 ,那么会开启20W/2=10W个线程;此时建议考虑 线程池的实现方案
如果
list是null,抛出NullPointerException
如果list是empty,抛出IllegalArgumentException
如果eachSize <=0,抛出IllegalArgumentException
如果partitionPerHandler是null,抛出NullPointerException
T - the generic typelist - 执行解析的list
比如 100000个 User,不能为null或者empty
eachSize - 每个线程执行多少个对象
比如 一个线程解析 1000个 User, 那么程序内部 会自动创建 100000/1000个 线程去解析;
必须>0
partitionPerHandler - the partition per handlerDefaultPartitionThreadExecutor,
DefaultPartitionThreadExecutor.INSTANCEpublic static <T> void execute(List<T> list, int eachSize, Map<String,?> paramsMap, PartitionRunnableBuilder<T> partitionRunnableBuilder)
list,设定每个线程执行多少条 eachSize,传入一些额外的参数 paramsMap,使用自定义的
partitionRunnableBuilder,自动构造多条线程并运行.
比如同步库存,一次从MQ或者其他接口中得到了5000条数据,如果使用单线程做5000次循环,势必会比较慢,并且影响性能; 如果调用这个方法,传入eachSize=100, 那么自动会开启5000/100=50 个线程来跑功能,大大提高同步库存的速度
其他的适用场景还有诸如同步商品主档数据,同步订单等等这类每个独立对象之间没有相关联关系的数据,能提高执行速度和效率
对于以下代码:模拟10个对象/数字,循环执行任务(可能是操作数据库等)
public void testExecuteTest() throws InterruptedException{ Date beginDate = now(); List<Integer>list = toList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); for (Integer integer : list){ //------ //模拟 do something //--------- Thread.sleep(1 * MILLISECOND_PER_SECONDS); } LOGGER.info("use time: [{}]", formatDuration(beginDate)); }统计总耗时时间 需要 use time:10秒28毫秒
此时你可以调用此方法,改成多线程执行:public void testExecuteTestUsePartitionRunnableBuilder() throws InterruptedException{ Date beginDate = now(); List<Integer>list = toList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); //每个线程执行2条数据, 没有自定义 paramsMap //将会自动创建 list.size()/2 =5个 线程执行 //每个线程执行的,将会是 PartitionRunnableBuilder build 返回的 Runnable ThreadUtil.execute(list, 1, null, new PartitionRunnableBuilder<Integer>(){ @Override public Runnable build(final List<Integer>perBatchList,PartitionThreadEntity partitionThreadEntity,Map<String, ?>paramsMap){ return new Runnable(){ @Override public void run(){ for (Integer integer : perBatchList){ //------ //模拟 do something //--------- try{ Thread.sleep(1 * MILLISECOND_PER_SECONDS); }catch (InterruptedException e){ LOGGER.error("", e); } } } }; } }); LOGGER.info("use time: [{}]", formatDuration(beginDate)); }统计总耗时时间 需要 use time:2秒36毫秒
对于上述的case,如果将 eachSize 参数由2 改成1, 统计总耗时时间 需要 use time:1秒36毫秒
可见 调用该方法,使用多线程能节省执行时间,提高效率; 但是也需要酌情考虑eachSize大小,合理的开启线程数量
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性;
需要注意合理的评估list的大小和eachSize比率;
不建议listsize很大,比如 20W,而eachSize值很小,比如2 ,那么会开启20W/2=10W个线程;此时建议考虑 线程池的实现方案
比如你需要拿到最终每条数据执行的结果,以便后续进行处理(比如对失败的操作再次执行或者发送汇报邮件等)
public void testExecuteTestUsePartitionRunnableBuilderParamsMap() throws InterruptedException{ Date beginDate = now(); List<Integer>list = toList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); final Map<Integer, Boolean>indexAndResultMap = Collections.synchronizedSortedMap(new TreeMap<Integer, Boolean>()); ThreadUtil.execute(list, 2, null, new PartitionRunnableBuilder<Integer>(){ @Override public Runnable build( final List<Integer>perBatchList, final PartitionThreadEntity partitionThreadEntity, Map<String, ?>paramsMap){ return new Runnable(){ @Override public void run(){ int i = 0; for (Integer integer : perBatchList){ //------ //模拟 do something //--------- try{ Thread.sleep(1 * MILLISECOND_PER_SECONDS); }catch (InterruptedException e){ LOGGER.error("", e); } int indexInTotalList = getIndex(partitionThreadEntity, i); //模拟 当值是 5 或者8 的时候 操作结果是false boolean result = (integer == 5 || integer == 8) ? false : true; indexAndResultMap.put(indexInTotalList, result); i++; } } private Integer getIndex(PartitionThreadEntity partitionThreadEntity,int i){ int batchNumber = partitionThreadEntity.getBatchNumber(); return batchNumber * partitionThreadEntity.getEachSize() + i; } }; } }); LOGGER.debug(JsonUtil.format(indexAndResultMap)); LOGGER.info("use time: [{}]", formatDuration(beginDate)); }输出结果:
29:21 DEBUG (ThreadUtilExample.java:161) [testExecuteTestUsePartitionRunnableBuilderParamsMap()] { "0": true, "1": true, "2": true, "3": true, "4": true, "5": false, "6": true, "7": true, "8": false, "9": true } 29:21 INFO (ThreadUtilExample.java:164) [testExecuteTestUsePartitionRunnableBuilderParamsMap()] use time:2秒181毫秒
如果
list是null,抛出NullPointerException
如果list是empty,抛出IllegalArgumentException
如果eachSize <=0,抛出IllegalArgumentException
如果partitionRunnableBuilder是null,抛出NullPointerException
T - the generic typelist - 执行解析的list
比如 100000个 User,不能为null或者empty
eachSize - 每个线程执行多少个对象
比如 一个线程解析 1000个 User, 那么程序内部 会自动创建 100000/1000个 线程去解析;
必须>0
paramsMap - 自定义的相关参数
该参数目的是你可以在自定义的 partitionRunnableBuilder中使用;
如果你传入的partitionRunnableBuilder中不需要额外的自定义参数,那么此处可以传入null
partitionRunnableBuilder - 每个线程做的事情,不能为nullDefaultPartitionThreadExecutor,
DefaultPartitionThreadExecutor.INSTANCEpublic static <T> void execute(List<T> list, int eachSize, Map<String,?> paramsMap, PartitionPerHandler<T> partitionPerHandler)
list,设定每个线程执行多少条 eachSize,传入一些额外的参数 paramsMap,使用自定义的
partitionPerHandler,自动构造多条线程并运行.
主要是用来简化 execute(List, int, Map, PartitionRunnableBuilder) 调用
对于以下代码:
ThreadUtil.execute(list, 5, new PartitionRunnableBuilder可以重构成:<String>(){ @Override public Runnable build(final List<String>perBatchList,PartitionThreadEntity partitionThreadEntity,Map<String, ?>paramsMap){ return new Runnable(){ @Override public void run(){ map.putAll(handle(perBatchList, noList)); } }; } });ThreadUtil.execute(list, 5, new PartitionPerHandler上述事例,可以从 14 行代码, 精简到 7 行代码<String>(){ @Override public void handle(List<String>perBatchList,PartitionThreadEntity partitionThreadEntity,Map<String, ?>paramsMap){ map.putAll(CopyrightTest.this.handle(perBatchList, noList)); } });
比如同步库存,一次从MQ或者其他接口中得到了5000条数据,如果使用单线程做5000次循环,势必会比较慢,并且影响性能; 如果调用这个方法,传入eachSize=100, 那么自动会开启5000/100=50 个线程来跑功能,大大提高同步库存的速度
其他的适用场景还有诸如同步商品主档数据,同步订单等等这类每个独立对象之间没有相关联关系的数据,能提高执行速度和效率
统计总耗时时间 需要 use time:2秒36毫秒
对于上述的case,如果将 eachSize 参数由2 改成1, 统计总耗时时间 需要 use time:1秒36毫秒
可见 调用该方法,使用多线程能节省执行时间,提高效率; 但是也需要酌情考虑eachSize大小,合理的开启线程数量
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性;
需要注意合理的评估list的大小和eachSize比率;
不建议listsize很大,比如 20W,而eachSize值很小,比如2 ,那么会开启20W/2=10W个线程;此时建议考虑 线程池的实现方案
如果
list是null,抛出NullPointerException
如果list是empty,抛出IllegalArgumentException
如果eachSize <=0,抛出IllegalArgumentException
如果partitionPerHandler是null,抛出NullPointerException
T - the generic typelist - 执行解析的list
比如 100000个 User,不能为null或者empty
eachSize - 每个线程执行多少个对象
比如 一个线程解析 1000个 User, 那么程序内部 会自动创建 100000/1000个 线程去解析;
必须>0
paramsMap - 自定义的相关参数
该参数目的是你可以在自定义的 partitionRunnableBuilder中使用;
如果你传入的partitionRunnableBuilder中不需要额外的自定义参数,那么此处可以传入null
partitionPerHandler - the partition per handlerDefaultPartitionThreadExecutor,
DefaultPartitionThreadExecutor.INSTANCEpublic static <T> void execute(List<T> list, PartitionThreadConfig partitionThreadConfig, Map<String,?> paramsMap, PartitionPerHandler<T> partitionPerHandler)
list,设定每个线程执行多少条 eachSize,传入一些额外的参数 paramsMap,使用自定义的
partitionPerHandler,自动构造多条线程并运行.
比如同步库存,一次从MQ或者其他接口中得到了5000条数据,如果使用单线程做5000次循环,势必会比较慢,并且影响性能; 如果调用这个方法,传入eachSize=100, 那么自动会开启5000/100=50 个线程来跑功能,大大提高同步库存的速度
其他的适用场景还有诸如同步商品主档数据,同步订单等等这类每个独立对象之间没有相关联关系的数据,能提高执行速度和效率
对于以下代码:模拟10个对象/数字,循环执行任务(可能是操作数据库等)
public void testExecuteTest() throws InterruptedException{ Date beginDate = now(); List<Integer>list = toList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); for (Integer integer : list){ //------ //模拟 do something //--------- Thread.sleep(1 * MILLISECOND_PER_SECONDS); } LOGGER.info("use time: [{}]", formatDuration(beginDate)); }统计总耗时时间 需要 use time:10秒28毫秒
此时你可以调用此方法,改成多线程执行:public void testExecuteTestUsePartitionRunnableBuilder() throws InterruptedException{ Date beginDate = now(); List<Integer>list = toList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); //每个线程执行2条数据, 没有自定义 paramsMap //将会自动创建 list.size()/2 =5个 线程执行 //每个线程执行的,将会是 PartitionRunnableBuilder build 返回的 Runnable ThreadUtil.execute(list, 1, null, new PartitionPerHandler<Integer>(){ @Override public void handle(final List<Integer>perBatchList,PartitionThreadEntity partitionThreadEntity,Map<String, ?>paramsMap){ for (Integer integer : perBatchList){ //------ //模拟 do something //--------- try{ Thread.sleep(1 * MILLISECOND_PER_SECONDS); }catch (InterruptedException e){ LOGGER.error("", e); } } } }); LOGGER.info("use time: [{}]", formatDuration(beginDate)); }统计总耗时时间 需要 use time:2秒36毫秒
对于上述的case,如果将 eachSize 参数由2 改成1, 统计总耗时时间 需要 use time:1秒36毫秒
可见 调用该方法,使用多线程能节省执行时间,提高效率; 但是也需要酌情考虑eachSize大小,合理的开启线程数量
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性;
需要注意合理的评估list的大小和eachSize比率;
不建议listsize很大,比如 20W,而eachSize值很小,比如2 ,那么会开启20W/2=10W个线程;此时建议考虑 线程池的实现方案
比如你需要拿到最终每条数据执行的结果,以便后续进行处理(比如对失败的操作再次执行或者发送汇报邮件等)
public void testExecuteTestUsePartitionRunnableBuilderParamsMap() throws InterruptedException{ Date beginDate = now(); List<Integer>list = toList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); final Map<Integer, Boolean>indexAndResultMap = Collections.synchronizedSortedMap(new TreeMap<Integer, Boolean>()); ThreadUtil.execute(list, 2, null, new PartitionRunnableBuilder<Integer>(){ @Override public Runnable build( final List<Integer>perBatchList, final PartitionThreadEntity partitionThreadEntity, Map<String, ?>paramsMap){ return new Runnable(){ @Override public void run(){ int i = 0; for (Integer integer : perBatchList){ //------ //模拟 do something //--------- try{ Thread.sleep(1 * MILLISECOND_PER_SECONDS); }catch (InterruptedException e){ LOGGER.error("", e); } int indexInTotalList = getIndex(partitionThreadEntity, i); //模拟 当值是 5 或者8 的时候 操作结果是false boolean result = (integer == 5 || integer == 8) ? false : true; indexAndResultMap.put(indexInTotalList, result); i++; } } private Integer getIndex(PartitionThreadEntity partitionThreadEntity,int i){ int batchNumber = partitionThreadEntity.getBatchNumber(); return batchNumber * partitionThreadEntity.getEachSize() + i; } }; } }); LOGGER.debug(JsonUtil.format(indexAndResultMap)); LOGGER.info("use time: [{}]", formatDuration(beginDate)); }输出结果:
29:21 DEBUG (ThreadUtilExample.java:161) [testExecuteTestUsePartitionRunnableBuilderParamsMap()] { "0": true, "1": true, "2": true, "3": true, "4": true, "5": false, "6": true, "7": true, "8": false, "9": true } 29:21 INFO (ThreadUtilExample.java:164) [testExecuteTestUsePartitionRunnableBuilderParamsMap()] use time:2秒181毫秒
如果
list是null,抛出NullPointerException
如果list是empty,抛出IllegalArgumentException
如果partitionThreadConfig是null,抛出NullPointerException
如果partitionPerHandler是null,抛出NullPointerException
T - the generic typelist - 执行解析的list
比如 100000个 User,不能为null或者empty
partitionThreadConfig - the partition configparamsMap - 自定义的相关参数
该参数目的是你可以在自定义的 partitionRunnableBuilder中使用;
如果你传入的partitionRunnableBuilder中不需要额外的自定义参数,那么此处可以传入null
partitionPerHandler - the partition per handlerDefaultPartitionThreadExecutor.INSTANCEpublic static void startAndJoin(Thread[] threads)
threads 调用 Thread.start() 再循环 threads 调用 Thread.join().
threads are run concurrently and this method waits for them to finish.
如果 threads 是null,抛出 NullPointerException
如果 threads 是empty,抛出 IllegalArgumentException
threads - the threadsApplicationShutdownHooks.runHooks()Copyright © 2008-2019 by feilong