7、Callable接口
1、创建线程的多种方式
创建方式 | 备注 |
---|---|
集成Thread类 | 无返回值 |
实现Runnable接口 | 无返回值 |
通过Callable接口 | 可以有返回值 |
通过线程池的方式 |
2、Callable和Runnable接口区别
区别 | Callable | Runnable |
---|---|---|
是否有返回值 | 是 | 否 |
是否抛出异常 | 是 | 否 |
方法实现不同 | call方法 | run 方法 |
代码实现:
package com.codetip.codejuc.juc.callable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
// 实现Runnable接口
class Mythread1 implements Runnable {
@Override
public void run() {
}
}
// 实现Callable接口
class Mythread2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 200;
}
}
public class CallOrRunDemo1 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// Runnable接口创建线程
// new Thread(new Mythread1(), "AA").start();
// 直接使用 Callable,报错
// new Thread(new Mythread2(), "AA").start();
// 可以找一个既和Runnable有关有何Callable有关的类
// Runnable接口有实现类 FutrueTask
// FctrueTask构造又可以传递Callable
// FutureTask
// FutureTask<Integer> f1 = new FutureTask<>(new Mythread2());
// new Thread(f1, "AA").start();
// lambda表达式写法
FutureTask<Integer> f2 = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName() + " come in Callable");
return 1024;
});
// 创建一个线程
new Thread(f2, "jack").start();
// 可以isDone 方法判断是否已经完成
// while (!f2.isDone()) {
// System.out.println("Wait...............");
// }
// 调用FutureTask的get方法
System.out.println(f2.get());
// 再次调用执行结果直接返回
System.out.println(f2.get());
// 主线程
System.out.println(Thread.currentThread().getName() + " come Over");
/**
* FutureTask 原理
* 1、在不影响主线程的任务,单开一个线程去执行其他线程,不影响主线程任务
* 例如:1、可以统一汇总任务
* 有四个同学:1计算 1+2+3 2计算:10+11+12.。。50 3计算:60+61 4计算:100+200
* 第二个同学计算量比较大,
* FutureTask 单独开启一个线程给2同学计算,先汇总1,3,4 同学的,最后等2计算出完成,统一汇总
* 例如、考试的情况,先做会的,在做不会的
*
* 最总汇总:只汇总一次
*/
}
}
8、JUC强大的辅助类
1、CountDownLatch 减少计数
-
概述
- CountDownLatch类是可以设置一个计数器,然后通过CountDown方法进行减1 操作,使用await方法方法等待计数器不大于0 ,然后继续执行await方法之后的语句。
- CountDownLatch主要有两个方法,当一个或多个线程调用await方法是,这些线程会阻塞
- 其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)
- 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
举例:6个同学全部走出教室后,班长才能锁门
// 不是用CountDownLatch
package com.codetip.codejuc.juc.callable;
// 演示 CountDownLatch
public class CountDownLatchDemo {
// (不适用CountDownLatch) 6个同学陆续离开教室后,班长才能锁门
public static void main(String[] args) {
// 6个同学陆续离开教室
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 同学离开教室");
}, i + "同学").start();
}
System.out.println(Thread.currentThread().getName() + "班长开始锁门走人了");
}
}
// 使用CountDownLatch
package com.codetip.codejuc.juc.callable;
import java.util.concurrent.CountDownLatch;
// 演示 CountDownLatch
public class CountDownLatchDemo {
// (不适用CountDownLatch) 6个同学陆续离开教室后,班长才能锁门
public static void main(String[] args) throws InterruptedException {
// 设置计数器 为 6
CountDownLatch count = new CountDownLatch(6);
// 6个同学陆续离开教室
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 同学离开教室");
// 计数减1
count.countDown();
}, i + "同学").start();
}
count.await();
System.out.println(Thread.currentThread().getName() + "班长开始锁门走人了");
}
}
2、CyclicBarrier循环栅栏
-
概述
-
一个同步的辅助类,它允许一组线程互相等待,直到达到设定的一个公共屏障点,在涉及一组固定大小的线程的程序中,这些线程必须不定时的互相等待,此时CyclicBarrier很有用。因为该Barrier在释放等待线程后可以重用,所以它称为循环的Barrier。
举例:七龙珠中:必须集齐七颗龙珠才能召唤神龙。
-
-
代码实现:
package com.codetip.codejuc.juc.callable; import java.util.concurrent.CyclicBarrier; // 集齐7颗龙珠召唤神龙 public class CyclicBarrierDemo { // 设置固定值 private static final int num = 7; public static void main(String[] args) { // 创建 CyclicBarrier CyclicBarrier cyclikBarrier = new CyclicBarrier(num, () -> { System.out.println("哈哈****,集齐七颗龙珠就可以召唤神龙了!!!!"); }); // 集齐七颗龙珠的过程 for (int i = 1; i <= 7; i++) { new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + " 星,龙珠被收集到了!"); 等待 cyclikBarrier.await(); } catch (Exception e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } } }
运行截图
3、Semaphore信号灯
-
概念
- 一个计数的信号量,从概念上来讲,信号量维护了一个许可集。如果有必要,在需要许可可用之前会阻塞每一个acquire(),然后在获取该许可,每个release()就是添加(释放)一个许可,杏儿可能释放一个正在阻塞的获取者,但是不使用时机的许可对象,Semapshore只对可用许可的号码进行计数,并蔡旭相应的行动。
-
举例:停车场。来一辆车就会获取一个许可(acquire),车满了,停车场不能在进入车辆了,其他必须等待。当有车辆离开,释放一个车位(release),其他车辆就可以继续获取(acquire)停车了。
代码案例:
package com.codetip.codejuc.juc.callable;
import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
// 6辆汽车,3个停车位
public class SemapHore {
// 定义3个车位
public static final int semaphoreNum = 3;
public static void main(String[] args) {
// 创建 semaphore ,设置许可数量
Semaphore semaphore = new Semaphore(semaphoreNum);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
// 随机停留时间
int wait = new Random().nextInt(5);
// 获取一个许可
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 号车,抢到了车位!!");
TimeUnit.SECONDS.sleep(wait);
System.out.println(Thread.currentThread().getName() + " -----号车,离开了车位!!,停留了:" + wait + "秒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
运行结果:
9、ReentrantReadWriteLock读写锁
1、悲观锁
-
修改数据时,认为别人都可能会修改,所以先进行上锁,修改完成后在解锁。
- 缺点:不支持并发,效率低
2、乐观锁
-
修改数据时,认为别人不会修改数据,得到数据的时候会有一个版本号,修改数据时会根据这个版本号需求改,如果版本号不一致,则别人修改过了。
- 支持并发,
3、读写锁
- 读锁:共享锁
- 写锁:独享锁
两者都会发送死锁:
读锁:有两个读操作,读1和读2, 线程在读的时候也可以做其他操作。读1 :进行了读的操作和写操作,读2:也正在读。读1 要等到读2 读完才能修改。这时读2也进行了修改操作,这时候就会出现死锁情况。读1 在等读2完成后才能进行修改,读2 在等读1完成后才能操作。
写锁:线程1:对第一条记录进行一次写操作。线程2对第二条记录进行了一次写操作。线程1又对第二条记录进行修改操作。同时线程2又对第一条进行修改操作。这个时候就会发送死锁,线程1 和线程 2 互相等待释放锁。
4、代码演示:
- 不加读写锁
package com.codetip.codejuc.juc.readOrWrite;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
// 资源类
class MyCahe {
// 创建Map集合
private volatile Map<String, Object> map = new HashMap<>();
// 存数据
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + " 正在写+++,Key为:" + key);
try {
TimeUnit.MICROSECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 放数据
map.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写+++执行完成,Key为:" + key);
}
// 读取数据
public Object get(String key) {
Object result = null;
System.out.println(Thread.currentThread().getName() + " 正在读取---,Key为:" + key);
try {
TimeUnit.MICROSECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 放数据
result = map.get(key);
System.out.println(Thread.currentThread().getName() + " 读取---作执行完成,Key为:" + key);
return result;
}
}
public class ReadWriteLockDemo1 {
public static void main(String[] args) {
MyCahe myCahe = new MyCahe();
// 创建线程放数据
for (int i = 1; i <= 3; i++) {
final int num = i;
new Thread(() -> {
myCahe.put(num + "", num + "");
}, String.valueOf(i)).start();
}
// 创建线程取数据
for (int i = 1; i <= 3; i++) {
final int num = i;
new Thread(() -> {
myCahe.get(num + "");
}, String.valueOf(i)).start();
}
}
}
运行结果:
2.加入读写锁
package com.codetip.codejuc.juc.readOrWrite;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
// 资源类
class MyCahe {
// 创建Map集合
private volatile Map<String, Object> map = new HashMap<>();
//创建读写锁的对象
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 存数据
public void put(String key, Object value) {
// 添加写锁
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在写+++,Key为:" + key);
TimeUnit.MICROSECONDS.sleep(300);
// 放数据
map.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写+++执行完成,Key为:" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放写锁
readWriteLock.writeLock().unlock();
}
}
// 读取数据
public Object get(String key) {
Object result = null;
// 添加读锁
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在读取---,Key为:" + key);
TimeUnit.MICROSECONDS.sleep(300);
// 放数据
result = map.get(key);
System.out.println(Thread.currentThread().getName() + " 读取---作执行完成,Key为:" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放读锁
readWriteLock.readLock().unlock();
}
return result;
}
}
public class ReadWriteLockDemo1 {
public static void main(String[] args) {
MyCahe myCahe = new MyCahe();
// 创建线程放数据
for (int i = 1; i <= 3; i++) {
final int num = i;
new Thread(() -> {
myCahe.put(num + "", num + "");
}, String.valueOf(i)).start();
}
// 创建线程取数据
for (int i = 1; i <= 3; i++) {
final int num = i;
new Thread(() -> {
myCahe.get(num + "");
}, String.valueOf(i)).start();
}
}
}
执行效果:
5、深入读写锁
- 读写锁:一个资源可以被多个读线程同时访问或者可以被一个写线程访问,但不同同时存在读写的线程,读写互斥,读读是共享的
读写锁演变的过程:
第一无锁 | 第二:添加锁 | 第三:读写锁ReentrantReadWriteLock |
---|---|---|
多线程抢夺资源(乱) | 使用synchronized和ReentrantLock | 读读可以共享,提升性能,同时多人可以读操作 |
都是独享的,每次只能一个操作 | 写还是一个 | |
缺点:读一 | 缺点:1:造成锁饥饿(一直读,没有写的操作) | |
写一 | 2:读的时候,不能写,只有读完成后,才可以写操作 |
6、锁降级
-
将写入锁降级为读锁,写的权限大于读的权限。
- 只能从写锁降级为读锁。 不能从读锁升级为读锁
-
降级过程:
注):此为MarkDown中的mermaid语法,运行结果就是上图
flowchart LR
id1[获取写锁]-->id2[获取读锁]-->
id3[释放写锁]-->id4[释放读锁]
package com.codetip.codejuc.juc.readOrWrite;
import java.util.concurrent.locks.ReentrantReadWriteLock;
// 锁降级演示
public class LockDemotion {
public static void main(String[] args) {
// 创建一个可重入锁
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 写锁
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
// 读锁
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
try {
// 获取到写锁
writeLock.lock();
System.out.println("+++++正在写操作!!!!");
// 获取读锁
readLock.lock();
System.out.println("-----正在读操作!!!!");
} finally {
// 释放写锁
writeLock.unlock();
// 释放读锁
readLock.unlock();
}
}
}
10、BlockingQueue阻塞队列
1、先回顾数据结构
队列 | 栈 |
---|---|
先进先出 | 先进后出 |
2、阻塞队列:
- 顾名思义,首先它是一个队列,通过一个共享的队列,可以使得数据有队列一端输入,从另一端输出。
- 当队列是空的,从队列中获取元素的操作将会被阻塞,直到其他线程往空的队列插入新的元素
- 当队列是满的,在队列中添加元素的操作将会被阻塞,直到其他线程从队列中移除一个或多个元素或者全部清空,使得队列空闲起来后并后续新增。
在多线程中,所谓的阻塞,在某种情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒执行
注):此为MarkDown中的mermaid语法,运行结果就是上图
flowchart LR
Thread1-->|Put|BlockingQueue-->|Take|Thread2
3、阻塞队列的代码架构
java.util.concurrent 接口 BlockingQueue
-
类型参数:
E
- 在此 collection 中保持的元素类型
-
所有超级接口:
Collection, Iterable, Queue
-
所有已知子接口:
BlockingDeque
-
所有已知实现类:
ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue
4、阻塞队列的分类
队列类名称 | 解释 |
---|---|
ArrayBlockingQueue(常用) | 由数组结构组成的有界阻塞队列 |
LinkedBlockingQueue(常用) | 由链表结构组成的有界阻塞队列(默认大小为:Integer。MAX_VALUE) |
DelayQueue | 使用优先级队列实现的延迟无界的阻塞队列 |
PriorityBlockingQueue | 支持优先级排序的无界阻塞队列 |
SynchronizedQueue | 不存储元素的阻塞队列,也即单个元素的队列 |
LinkedTransferQueue | 由链表组成的无界阻塞队列 |
LinkedBlockingDeque | 由链表组成的双向阻塞队列 |
- 队列的核心方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(time,unit) |
移除 | remove(e) | poll() | take() | poll(time,unit) |
检查 | element(e) | peek() | 不可用 | 不可用 |
名称 | 描述 |
---|---|
抛出异常 | 当队列满时:再往队列add元素,会抛出:java.lang.IllegalStateException: Queue full 异常 |
当队列空时:在从队列里remove元素,会抛出:main" java.util.NoSuchElementException 异常 | |
特殊值 | 插入方法:成功返回true失败false,移除方法:成功返回队列中的元素,内有则返回nul |
阻塞 | 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程,直到put数据or相应中断退出 |
当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用 | |
超时 | 当阻塞队列满时,队列阻塞生产者线程一定时间,超过限定时间后,生产者线程会退出 |
5、入门案例
package com.codetip.codejuc.juc.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class BlockingQueueDemo01 {
public static void main(String[] args) throws InterruptedException {
// 创建一个定长的租所队列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
// 第一组案例
// System.out.println(blockingQueue.add("a"));
// System.out.println(blockingQueue.add("b"));
// System.out.println(blockingQueue.add("c"));
// System.out.println(blockingQueue.add("d"));
// System.out.println(blockingQueue.element());
// System.out.println(blockingQueue.remove());
// 第二组
// System.out.println(blockingQueue.offer("a"));
// System.out.println(blockingQueue.offer("b"));
// System.out.println(blockingQueue.offer("c"));
// System.out.println(blockingQueue.offer("d"));
//
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// 第三组
// blockingQueue.put("a");
// blockingQueue.put("b");
// blockingQueue.put("c");
// blockingQueue.put("d");
// blockingQueue.take();
// blockingQueue.take();
// blockingQueue.take();
// blockingQueue.take();
// 第四组
System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("b", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("c", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("d", 2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
}
}
11、ThreadPool线程池
1、线程池概述
-
概念:
- 一种线程使用模式。线程过多会带来过渡开销,进而影响缓存局部性的和整体性能。而线程池内维护着多个线程,等待着监督管理者分配可执行的任务,这就避免了在处理短时间任务是创建和销毁线程的代价,线程池不仅能保证内核的充分利用,还能防止过分调度。
2、线程池架构
注):此为MarkDown中的mermaid语法,运行结果就是上图
classDiagram
class 工具类Executors{
}
接口Execure <|-- 接口ExecutorService : 继承
接口ExecutorService <|.. 抽闲类AbstractExecutoService : 实现
抽闲类AbstractExecutoService <|-- 实现类ThreadPoolExecutor : 继承
实现类ThreadPoolExecutor <|.. 实现类ThreadPoolExecutor
实现类ThreadPoolExecutor <|-- 实现类ScheduleThreadPoolExecutor : 继承
接口ExecutorService <|-- 接口ScheduleExecutoService : 继承
接口ScheduleExecutoService <|.. 实现类ScheduleThreadPoolExecutor: 实现
3、线程池使用方式
使用Executors工具类创建线程池
方法 | 描述 |
---|---|
Executors.newFixedThreadPool | 一个固定大小的线程池 |
Executors.newSingleThreadExecutor | 一个任务一个任务的执行,一个线程池一个任务。 |
Executors.newCachedThreadPool | 线程池根据需求创建线程,可扩容,遇强则强 |
代码演示
package com.codetip.codejuc.juc;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo001 {
public static void main(String[] args) {
// 一个池子五个线程
ExecutorService threadPool = Executors.newFixedThreadPool(5);
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " 正在办理业务!!!"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
System.out.println("-------------------------分割线-----------------------");
// 一池一线程
ExecutorService threadSingle = Executors.newSingleThreadExecutor();
try {
for (int i = 1; i <= 10; i++) {
threadSingle.execute(() -> System.out.println(Thread.currentThread().getName() + " 正在办理业务!!!"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadSingle.shutdown();
}
System.out.println("-------------------------分割线-----------------------");
// 可缓冲的线程
ExecutorService executorService = Executors.newCachedThreadPool();
try {
for (int i = 1; i <= 10; i++) {
executorService.execute(() -> System.out.println(Thread.currentThread().getName() + " 正在办理业务!!!"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}
线程池底层原理
以上的通过Executors创建的线程池的方法,查看源码发现都是使用的:ThreadPoolExecutor 这个方法
4、线程池的七个参数
-
ThreadPoolExecutor 参数说明
类名:ThreadPoolExecutor 说明 int corePoolSize 常驻(核心)线程数量 int maximumPoolSize 最大线程数量 Long keepAliveTime 线程的存活时间(线程多长时间不用就销毁掉,保留核心线程数量) TimeUnit unit 存活时间的单位 BlockingQueue workQueue 阻塞队列 ThreadFactory threadFactory 线程工程 RejectedExecutionHandler handler 拒绝策略
5、线程池底层工作流程
-
执行流程
-
第一:提交任务调用execute, 第二:核心线程会执行任务;第三:任务很多,核心线程执行同时执行不了,会放入阻塞队列中去
-
第四步:阻塞队列如果满了则会创建新的线程执行任务,线程总数不会大于设定的总线程数。
-
第五步:继续有新的任务,也没有新的线程可以创建了,会执行拒绝策略。
-
内置的决绝策略
决绝策略 描述 AbortPolicy 默认,直接抛出RejectedExecutionHandler异常,组织系统的正常运行 CallerRunsPolicy ”调用者运行“运行一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某学任务回退到调用者,从而降低新任务的流量 DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中并尝试再次提交任务 DiscardPolicy 该策略直接丢弃无法处理的任务,不予任何处理也不抛出任何异常,如果允许丢失,这是最好的一种策
重要,在项目中创建多线程,常见的Executors工具类创建的线程池的方式,都不推荐使用,而是通过ThreadPoolExecutor的方式自己创建线程池,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors创建线程池的对象的风险和弊端如下
方法 说明 FixedThreadPool和SingleThreadExecutor 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM CachedThreadPool和ScheduledThreadPool 允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM -
-
6、自定义线程池
代码如下:
package com.codetip.codejuc.juc.ThreadPool;
import java.util.concurrent.*;
// 创建自定义线程池
public class ThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = new ThreadPoolExecutor(
5,// 核心线程大小
7, // 最大线程数量
2L, // 线程的存活时间(线程多长时间不用就销毁掉,保留核心线程数量)
TimeUnit.SECONDS,// 存活时间单位
new ArrayBlockingQueue<>(3),// 阻塞队列
Executors.defaultThreadFactory(), // 线程工程
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
try {
for (int i = 1; i <= 10; i++) {
service.execute(() -> System.out.println(Thread.currentThread().getName() + " 正在办理业务!!!"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
service.shutdown();
}
}
}