线程基础知识
线程基础知识程序、进程、线程基本概念程序进程线程单核CPU与多核CPU并行与并发创建线程的两种方式1.继承Thread类方式2.实现Runnable接口方式3.两种方式对比线程的优先级线程的分类线程的生命周期线程安全问题线程的同步方式一:同步代码块方式二:同步方法方式三:Lock锁单例模式懒汉式改造成线程安全的双重检查锁线程的死锁问题线程的通信wait()与sleep()的区别新增创建线程方式一、
线程基础知识
程序、进程、线程基本概念
程序
是为完成特定任务、使用某种语言编写的一套指令集。既指一段静态的代码,静态对象。
进程
是程序的一次执行过程、或是正在运行的一个程序,是一个动态的过程:有它自身的产生、存在、和消亡的过程。—生命周期
程序时静态的、进程是动态的
例如:运行中的QQ、运行中的mp3
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
线程
1.进程可以进一步细化为线程、是一个程序内部的一条执行路径。
2.若一个进程同一时间段并行执行多个线程、则是支持多线程的
3.线程作为调度和执行的单位、每个线程拥有独立的运行栈、和程序计数器(pc)、线程切换的开销缩小
4.一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象、可以访问相同的变量和对象。
但是多个线程操作共享的资源可能会带来安全隐患# 一级目录
单核CPU与多核CPU
一个java.exe程序 至少三个线程、main()主线程、gc()垃圾回收线程、异常处理线程
并行与并发
并行:多个CPU同时执行多个任务。比如马路上的车流
并发:一个CPU(采用时间片)同时执行多个任务。比如商城秒杀
创建线程的两种方式
1.继承Thread类方式
创建流程:
①.创建一个类继承Thread类
②.重写Thread类的run()–》将此线程执行的操作声明在run()中
③.创建Thread类的子类的对象
④.通过此对象的start():1.启动当前线程 2.调用当前线程的run()
说明:
①.启动一个线程,必须调用start()、不能调用run()的方式启动线程、否则为main()主线程
②.再启动一个线程、必须重新创建一个新的Thread子类对象、调用start()
package com.joker.thread;
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("我是一个线程:"+Thread.currentThread().getName());
super.run();
}
}
2.实现Runnable接口方式
①.创建一个实现了Runnable接口的类
②.创建类去实现Runnable中的抽象方法:run()
③.创建实现类的对象
④.将此对象作为参数传递到Thread类的构造器中、创建Thread类的对象
⑤.通过Thread类的对象调用satrt()
package com.joker.thread;
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("我是一个线程:"+Thread.currentThread().getName());
}
}
3.两种方式对比
优先选择、实现Runnable接口的方式
①.实现的方式没类的单继承性的局限性
②.实现的方式更适合来处理多个线程共享数据的情况
联系: public class Thread implements Runnable
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中
Thread类中的常用方法:
1.start():启动当前线程;调用当前线程的run()
2.run():需要重写Thread类中的此方法、将创建的线程要执行的操作声明在此方法中
3.currentThread():静态方法、返回执行当前代码的线程
4.getName():获取当前线程的名字
5.setName():设置当前线程的名字
6.yield():释放当前cpu的执行权
7.join():在线程a中调用线程b的join(),此时线程a就进入到阻塞状态、直到线程b完全执行完成以后、线程a才会结束阻塞状态
8.stop():已过时、强制结束当前线程
9.sleep(millitime):让当前线程"睡眠"指定的millitime毫秒、在指定毫秒内、线程处于阻塞状态
10.isAlive():判断当前线程是否存活
public static void main(String[] args) {
//创建Thread 类的子类的对象
MyThread thread = new MyThread();
//调用start():1.启动当前线程 2.不能调用当前线程的run()启动线程。
try {
thread.sleep(2000);
thread.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
// thread.run();
}
线程的优先级
1.
MAX_PRIORITY:10
MIX_PRIORITY:1
NORM_PRIOTITY:5 -->默认优先级
2.
获取和设置当前线程的优先级
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级
说明:高优先级的线程要抢占低优先级线程的cpu执行权、
并不意味着只有当高优先级的线程执行完以后,低优先级的线程才会执行
线程的分类
java中的线程分为两种 1.守护线程 2.用户线程
1.唯一的区别就是判断JVM何时离开
2.守护线程是用来服务用户线程的,通过在start()方法前调用 thread.setDaemon(true) 可以把一个用户线程变为守护线程
3.java垃圾回收就是一个典型的守护线程
4.若JVM中都是守护线程、当前JVM将退出
线程的生命周期
1.新建:
当一个Thread类或其子类的对象被声明并创建时、新生的线程对象处于新建状态
2.就绪:
处于新建状态的线程被start()后、将进入线程队列等待cpu时间片、此时它已具备了运行的条件、只是没有分配到cpu的资源
3.运行:
当就绪的线程被调度并获得cpu资源时、便进入运行状态,run()方法定义了线程的操作和功能
4.阻塞:
在某种特殊情况下、被人为挂起、或者执行输入输出操作时、让出cpu并临时中止自己的执行、进入阻塞状态
5.死亡:
线程完成了它的全部工作或线程被提前强制性的中止或出现异常导致结束
Thread 中的状态
线程安全问题
线程的同步
1.多个线程执行的不确定性引起的结果不稳定
2.多个线程对账本的共享、会造成操作的不完整性、会破坏数据
问题:
多人同时购买车票时、会出现重票、错票、出现线程安全问题
如何出现:
当a线程进入购票流程时、验证余票通过后,被沉睡期间、b线程也进入该过程验票通过进行购票也就是当某个线程操作共享数据时、还未操作完成、其他线程也参与进来则会出现共享数据安全问题
如何解决:
当a线程进入购票流程后、不再允许其他线程进入、只能等待a线程操作结束后方可进入、即使a线程出现了阻塞、其他线程也不能进入。
java :
通过同步机制来解决线程安全问题、解决了线程安全问题、但是操作同步代码时、只能有一个线程参与、其他的线程等待、相当于是一个单线程的过程,效率会降低 – 局限性
方式一:同步代码块
synchronized( 同步监视器 ){
//需要被同步的代码
}
1.操作共享数据的代码、即为需要被同步的代码
2.共享数据:多个线程共同操作的变量、如车票
3.同步监视器: 即为锁、任何一个类的对象、都可以充当锁(多个线程必须共用同一把锁)
4.在实现Runnable接口创建多线程的方式中、我们可以考虑使用this充当同步监视器
方式二:同步方法
在实现Runnable类中
package com.joker.ticketthread;
/**
**/
public class TicketRunnable implements Runnable{
* 问题: 多人同时购买车票时、会出现重票、错票、出现线程安全问题
* 如何出现:当a线程进入购票流程时、验证余票通过后,被沉睡期间、b线程也进入该过程验票通过进行购票
* 也就是当某个线程操作共享数据时、还未操作完成、其他线程也参与进来则会出现共享数据安全问题
* 如何解决:当a线程进入购票流程后、不再允许其他线程进入、只能等待a线程操作结束后方可进入、即使a线程出现了阻塞、其他线程也不能进入。
* java : 通过同步机制来解决线程安全问题
* 解决了线程安全问题、但是操作同步代码时、只能有一个线程参与、其他的线程等待、相当于是一个单线程的过程,效率会降低 -- 局限性
*
* 方式一:同步代码块
* synchronized( 同步监视器 ){
* //需要被同步的代码
* }
* 1.操作共享数据的代码、即为需要被同步的代码
* 2.共享数据:多个线程共同操作的变量、如车票
* 3.同步监视器: 即为锁、任何一个类的对象、都可以充当锁(多个线程必须共用同一把锁)
* 4.在实现Runnable接口创建多线程的方式中、我们可以考虑使用this充当同步监视器
*
* 方式二:同步方法
*
*
//票总张数100
private int ticket = 100;
Object object = new Object();
@Override
public void run() {
while (true){
synchronized(object){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":卖票、票号为:"+ticket);
ticket--;
}else {
break;
}
}
//同步方法 show();
}
}
/**
* 同步方法
*/
public synchronized void show(){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":卖票、票号为:"+ticket);
ticket--;
}
}
}
在继承Thread类中
package com.joker.ticketthread;
/**
**/
public class TicketThread extends Thread{
//票总张数100
private int ticket = 100;
@Override
public void run() {
while (true){
synchronized(TicketThread.class){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":卖票、票号为:"+ticket);
ticket--;
}else {
break;
}
}
}
super.run();
}
}
主线程
package com.joker.ticketthread;
/**
**/
public class TicketTest {
public static void main(String[] args) {
TicketRunnable ticket = new TicketRunnable();
TicketThread thread = new TicketThread();
TicketLock lock = new TicketLock();
Thread t1 = new Thread(lock);
Thread t2 = new Thread(lock);
Thread t3 = new Thread(lock);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
TicketDemo demo = new TicketDemo();
Thread t4 = new Thread(demo);
Thread t5 = new Thread(demo);
t4.setName("四号储户");
t5.setName("五号储户");
t4.start();
t5.start();
}
}
方式三:Lock锁
解决线程安全问题synchronized 与 lock的异同
相同:
二者都可以解决线程安全问题
不同点:
1.synchronized机制在执行完相应的同步代码后,会自动释放同步监视器
2.lock需要手动的启动同步lock(),手动结束同步unlock().
说明:
1.Lock是显示锁(手动开启和关闭锁)、synchronized是隐式锁、出了作用域自动释放
2.Lock只有代码块锁、synchronized有代码块和方法锁
3.Lock锁、JVM将花费较少的时间来调度线程、性能更好、并且具有更好的扩展性。
package com.joker.ticketthread;
import java.util.concurrent.locks.ReentrantLock;
/**
**/
public class TicketLock implements Runnable{
* 解决线程安全问题、Lock锁
*
* synchronized 与 lock的异同
* 相同:二者都可以解决线程安全问题
* 不同点:1.synchronized机制在执行完相应的同步代码后,会自动释放同步监视器
* 2.lock需要手动的启动同步lock(),手动结束同步unlock().
*
* 1.Lock是显示锁(手动开启和关闭锁)、synchronized是隐式锁、出了作用域自动释放
* 2.Lock只有代码块锁、synchronized有代码块和方法锁
* 3.Lock锁、JVM将花费较少的时间来调度线程、性能更好、并且具有更好的扩展性。
*
//票总张数100
private int ticket = 100;
/**
* 1.实例化ReentrantLock
*/
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true){
try {
/**
* 2.调用锁定方法lock()
*/
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票、票号为:" + ticket);
ticket--;
} else {
break;
}
}finally {
/**
* 调用解锁方法
*/
lock.unlock();
}
}
}
}
单例模式懒汉式改造成线程安全的
双重检查锁
效率稍高 双重检查锁,先判断对象是否已经被初始化,再决定要不要加锁。
隐患:
实例化对象时,实际上可以分解成以下三个步骤:
1.分配内存空间
2.初始化对象
3.将对象指向刚分配的内存空间
但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:
1.分配内存空间
2.将对象指向刚分配的内存空间
3.初始化对象
考虑重排序后:a线程为instance
1.分配内存空间
2.指向内存空间 后、
3.b线程此时检查instance不为空return instance后访问(此时对象还未完成初始化)
4.a线程初始化instance
这种情况下b线程的访问是一个初始化未完成的对象。
正确的双检索:需要给instance加上 volatile 关键字、使用了volatile关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。
package com.joker.banktest;
/**
* 将一个单例模式懒汉式改造成线程安全的
**/
public class Bank {
/**
* 构造器
*/
public Bank(){
}
private volatile static Bank instance= null;
/**
* 只能创建一个当前类的实例
* @return
*/
public static Bank getinstance(){
* 方式一、效率稍低 用到了synchronized,会导致很大的性能开销,并且加锁其实只需要在第一次初始化的时候用到,之后的调用都没必要再进行加锁。
synchronized (Bank.class){
if(instance == null){
instance = new Bank();
}
}
* 方式二、效率稍高 双重检查锁,先判断对象是否已经被初始化,再决定要不要加锁。
*
* 隐患:
* 实例化对象时,实际上可以分解成以下三个步骤:1.分配内存空间 2.初始化对象 3.将对象指向刚分配的内存空间
* 但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:1.分配内存空间 2.将对象指向刚分配的内存空间 3.初始化对象
*
* 考虑重排序后:a线程为instance 1.分配内存空间 2.指向内存空间 后、 3.b线程此时检查instance不为空return instance后访问(此时对象还未完成初始化) 4.a线程初始化instance
* 这种情况下b线程的访问是一个初始化未完成的对象。
*
* 正确的双检索:需要给instance加上 volatile 关键字、使用了volatile关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。
*
if(instance == null){
synchronized (Bank.class){
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
线程的死锁问题
死锁:
1.不同的线程分别占用对方需要的同步资源不放弃、都在等待对方放弃自己需要的同步资源、形成了死锁。
2.出现死锁后、不会出现异常、不会出现提示、只是所有线程均处于阻塞状态、无法继续
解决方法:
1.专门的算法、原则
2.尽量减少同步资源的定义
3.尽量避免嵌套同步
package com.joker.banktest;
public class DieLock {
* @param args
*
* 死锁:
* 1.不同的线程分别占用对方需要的同步资源不放弃、都在等待对方放弃自己需要的同步资源、形成了死锁。
* 2.出现死锁后、不会出现异常、不会出现提示、只是所有线程均处于阻塞状态、无法继续
*
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
/**
* 继承Thread
*/
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
super.run();
}
}.start();
/**
* 实现Runnable
*/
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
线程的通信
wait():一旦执行此方法、当前线程就进入阻塞状态、并释放同步监视器
notify():一旦执行此方法、就会唤醒被wait()的一个线程、如果有多个线程被wait(),就唤醒优先级高的那个
notifyAll():一旦执行此方法、就会唤醒所有被wait()的线程.
说明:
1.wait()、notify()、notifyAll()、三个方法只能在同步代码块或者同步方法中
2.wait()、notify()、notifyAll()、三个方法的调用者必须是同步代码块或同步方法中的同步监视器
3.三个方法不是定义在Thread中,而是定义在java.lang.Object类中
package com.joker.communication;
/**
* 两个线程交替打印1-100
**/
public class Number implements Runnable{
*
* wait():一旦执行此方法、当前线程就进入阻塞状态、并释放同步监视器
* notify():一旦执行此方法、就会唤醒被wait()的一个线程、如果有多个线程被wait(),就唤醒优先级高的那个
* notifyAll():一旦执行此方法、就会唤醒所有被wait()的线程.
*
* 说明:
* 1.wait()、notify()、notifyAll()、三个方法只能在同步代码块或者同步方法中
* 2.wait()、notify()、notifyAll()、三个方法的调用者必须是同步代码块或同步方法中的同步监视器
* 3.三个方法不是定义在Thread中,而是定义在java.lang.Object类中
private int number = 1;
@Override
public void run() {
while (true){
synchronized (this) {
this.notify();
if (number <= 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得调用如下wait()方法的线程进入阻塞状态
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
主线程
package com.joker.communication;
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
wait()与sleep()的区别
1.相同点:
一旦执行方法、都可以使得当前的线程进入阻塞状态。
2.不同点:
- 两个方法声明的位置不同:Thread类中声明sleep()、Object类中声明wait()
- 调用的要求不同:sleep()可以在任何需要的场景下调用、wait()必须使用在同步代码块或同步方法中
- 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁
新增创建线程方式
一、 callable 接口
与使用Runnable相比、Callable功能更强大些
1.与run()方法、可以有返回值
2.方法可以抛出异常
3.支持泛型的返回值
4.需要借助FutureTask类、比如获取返回结果
实现步骤:
1.创建一个实现Callable的实现类
2.实现call方法、将此线程需要执行的操作声明在call()中、call()有返回值
3.创建callable接口实现类对象
4.将此callable接口实现类的对象作为传递到FutrueTask构造器中、创建FuntrueTask对象
5.将FuntrueTask的对象作为参数传递到Thread类的构造器中、创建Thread对象,并调用start()
6.获取Callable中call()方法的返回值
Futrue接口:
1.可以对具体的Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果
2.FutrueTask 是 Funtrue接口的唯一的实现类
3.FutrueTask 同时实现了 Runnable、 Futrue 接口。它既可以作为 Runnable 被线程执行、又可以作为 Futrue 得到 Callable 的返回值
实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
1.call()可以有返回值的
2.call()可以抛出异常、被外面的操作捕获、获取异常的信息
3.Callable是支持泛型的
package com.joker.callable;
import java.util.concurrent.Callable;
public class CallableDemo implements Callable {
* 1.创建一个实现Callable的实现类
* 2.实现call方法、将此线程需要执行的操作声明在call()中、call()有返回值
* 3.创建callable接口实现类对象
* 4.将此callable接口实现类的对象作为传递到FutrueTask构造器中、创建FuntrueTask对象
* 5.将FuntrueTask的对象作为参数传递到Thread类的构造器中、创建Thread对象,并调用start()
* 6.获取Callable中call()方法的返回值
* @return
* @throws Exception
@Override
public Object call() throws Exception {
int sum = 0;
for(int i=1;i<=100;i++){
if(i%2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
main()方法
package com.joker.callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
* Futrue接口、
* 1.可以对具体的Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果
* 2.FutrueTask 是 Funtrue接口的唯一的实现类
* 3.FutrueTask 同时实现了 Runnable、 Futrue 接口。它既可以作为 Runnable 被线程执行、又可以作为 Futrue 得到 Callable 的返回值
*
*
* 实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
* 1.call()可以有返回值的
* 2.call()可以抛出异常、被外面的操作捕获、获取异常的信息
* 3.Callable是支持泛型的
*
* @param args
public static void main(String[] args) {
3.创建callable接口实现类对象
CallableDemo callableDemo = new CallableDemo();
4.将此callable接口实现类的对象作为传递到FutrueTask构造器中、创建FuntrueTask对象
FutureTask futureTask = new FutureTask(callableDemo);
5.将FuntrueTask的对象作为参数传递到Thread类的构造器中、创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//get()方法的返回值即为FutrueTask构造器参数Callable实现类重写的call()的返回值
6.获取Callable中call()方法的返回值
Object sum = futureTask.get();
System.out.println("总和为:"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
二、 使用线程池
背景:
经常创建和销毁、使用量特别大的资源、比如并发情况下的线程对性能影响较大
思路:
提前创建好多个线程、放入线程池中、使用时直接获取、使用完放回池中、可以避免频繁创建与销毁、实现重复利用。
好处:
1.提高响应速度、减少了创建新线程的时间
2.降低资源消耗、重复利用线程池中的线程、不需要每次都创建
3.便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程池相关API
ExecutorService:线程池接口、常见子类:ThreadPoolExecutors
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- Futuresubmit (callabletask):执行任务,有返回值,一般又来执行Callable
- void shutdown():关闭连接池
Executors:工具类、线程池的工厂类、用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
- Executors.newScheduledTreadPool(n):创建一个线程池、它可安排在给定延迟后运行命令或者定期执行
package com.joker.threadpool;
/**
**/
public class NumberThread implements Runnable{
* 背景:经常创建和销毁、使用量特别大的资源、比如并发情况下的线程对性能影响较大
* 思路:提前创建好多个线程、放入线程池中、使用时直接获取、使用完放回池中、可以避免频繁创建与销毁、实现重复利用。
* 好处:
* 1.提高响应速度、减少了创建新线程的时间
* 2.降低资源消耗、重复利用线程池中的线程、不需要每次都创建
* 3.便于线程管理
* corePoolSize:核心池的大小
* maximumPoolSize:最大线程数
* keepAliveTime:线程没有任务时最多保持多长时间后会终止
@Override
public void run() {
for (int i = 0;i<=100;i++){
if(i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
package com.joker.threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class ThreadPool {
public static void main(String[] args) {
1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
设置线程池的属性
System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
2.执行指定的线程的操作、需要提供实现Runnable接口或Callable接口实现类的对象
NumberThread thread = new NumberThread();
service.execute(thread);//适用于Runnable
// service.submit();//适用于Callable
3.关闭连接池
service.shutdown();
}
}
更多推荐
所有评论(0)