程序:程序是为完成特定任务,用某种语言编写的一组指令的集合。
进程:进程是程序的一次执行过程,或是正在运行的一个程序,它是系统进行资源分配和调度的一个独立单位,系统在运行时会为每个进程分配不同的内存区域。进程有其自身的产生、存在和消亡的过程(生命周期)。值得注意的是,程序是静态的代码,进程是动态的过程。
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径,它是一个基本的 CPU 执行单元,也是程序执行流的最小单元(操作系统调度的最小单位)。线程最直接的理解就是 “轻量级进程”。若一个进程同一时间并行执行多个线程,就是支持多线程的。一个进程中的多个线程共享相同的内存单元 /内存地址空间,这就使线程间的通信更简便、高效。但多个线程操作共享的系统图资源可能会带来安全隐患。
1.1.1 并发与并行并发:并发是指两个或多个事件在同一时间间隔内发生,同一时间间隔是一段时间。
并行:并行是指两个或多个事件在同一时刻同时发生。
注意并发和并行是两个不同的概念。并发在宏观上有多个程序在同时执行,而在每个时刻,单处理机环境下实际仅能有一个程序执行,因此微观上这些程序仍是分时交替执行的。
1.1.2 多线程的优点- 提高应用程序的相应。对图形化界面更有意义,可增强用户体验。dfa
- 提高计算机系统 CPU 的利用率。
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
- 程序需要同时执行多个任务时。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、搜索等。
- 需要一些后台运行的程序时。
方式一:继承 Thread 类创建线程
- 创建一个继承于 Thread 类的子类。
- 重写 Thread 类的 run() 方法定义线程体。
- 创建 Thread 类的子类的对象。
- 调用该对象的 start() 方法启动线程。线程启动后自动执行 run() 方法,线程执行完毕后进入终止状态。
// 1.创建一个继承与 Thread 类的子类
class MyThread extends Thread {
// 2.重写 run() 方法(将此线程执行的操作声明在 run() 方法中)
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
if(i % 2 == 0) {
System.out.println(i);
}
}
}
}
public class TestThread {
public static void main(String[] args) {
// 3.创建 Thread 类的子类对象
MyThread t1 = new MyThread();
// 4.调用该对象的 start() 方法启动线程
t1.start();
// t1.run(); // 如果直接调用 t1.run() 则没有启动线程,只是对象调用方法
for (int i = 0; i < 2; i++) {
System.out.println(i + "******");
}
}
}
结果:
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 0****** 1****** 78 80 82 84 86 88 90 92 94 96 98 100 102 104 106 108 110 112 114 116 118 120 122 124 126 128 130 132 134 136 138 140 142 144 146 148 150 152 154 156 158 160 162...
当我们的主线程(main),调用创建的对象的 start() 方法后启动了一个新线程,新线程是遍历从 0 到 10000 的所有偶数,当新线程遍历到 76 时切换回了我们的主线程,主线程又遍历了从 0 到 2 之间的所有偶数,当遍历结束又切换到了新线程,这就可以看出线程之间的交互性。(具体结果由 CPU 的切换频率决定,每个人的结果可能都不太一样,如果结果体现不出来可以将 for 循环遍历的范围扩大后再运行。)
注意:
- 不能通过直接调用 run() 的方式启动线程。只有 start() 能启动线程,如果直接调用 run() 只是对象调用方法。
- 一个线程的对象只能启动一个线程,如果再想启动该线程,需要重新创建一个线程的对象。
方式二:实现 Runnable 接口创建线程
-
创建一个实现了 Runnable 接口的类。
-
实现类去实现 Runnable 中的抽象方法:run() 。
-
创建实现类的对象。
-
将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象。
-
通过 Thread 类的对象调用 start() 方法启动线程。
// 1.创建一个实现了 Runnable 接口的类 class MThread implements Runnable { // 重写 Thread 类的 run() @Override public void run() { for (int i = 0; i < 100; i++) { if(i % 2 == 0) { System.out.println(i); } } } } public class TestThread3 { public static void main(String[] args) { // 3.创建 Thread 类的对象 MThread mThread = new MThread(); // 4.将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象 Thread t1 = new Thread(mThread); // 5.通过 Thread 类的对象调用 start() 方法启动线程 t1.start(); // 再启动一个线程 Thread t2 = new Thread(mThread); t2.start(); } }
开发中,优先选择实现 Runnable 接口的方式。
原因:
-
实现的方式没有类的单继承的局限性。
-
实现的方式更适合来处理多个线程共享数据的情况。
在单核 CPU 情况下执行多线程时,Java 采用的是优先级调度的策略。每个 Java 线程都有一个优先级,范围在 MIN_PRIORITY(该常量的值为1)和 MAX_PRIORITY(该常量的值为10)之间。在默认情况下,每个线程的优先级都为 NORM_PRIORITY(该常量的值为5)。虽然每个线程都有自己的优先级,但是不能绝对地说线程调度是按照优先级进行调度的。只是从概率上讲,高优先级的线程高概率的情况下被执行,并不意味着只有当高优先级的线程执行完以后,低优先级的进程才执行。
在 Java 中,可以用 setPriority() 方法来调整一个线程的优先级,该方法有一个整形的参数,在 1 到 10 范围内。可以用 getPriority() 方法返回线程的优先级。
1.2.3 Thread 类的相关方法- start() :启动当前线程,调用当前线程的 run() 方法。
- run() :通常需要重写 Thread 类中的此方法,将创建的线程要执行的操作声明在此方法中。
- currentThread() :静态方法,返回执行当前代码的线程。
- getName() :获取当前线程的名字。
- setName() :设置当前线程的名字。
- yield() :释放当前 CPU 的执行权。
- join() :在线程 A 中,调用线程 B 的 join(),此时线程 A 就会进入阻塞状态,知道线程 B 完全执行结 束后,线程 A 才结束阻塞状态。
- stop() :已过时。当执行此方法时,强制结束当前线程。
- sleep(long millitime) : 让当前线程 “睡眠” 指定的 millitime 毫秒。在指定的 millitime 毫秒时间内,当前线程处于阻塞状态。
- isAlive() :判断当前线程是否存活。
线程在创建之后,就开始了它的生命周期。一个线程在其整个生命周期中可处于不同的状态。JDK 中用
线程的生命周期可分为如下几种状态:新建状态(new)、可运行状态(runnable)、运行状态(running)、阻塞状态(blocked)、终止状态(dead)。
新建状态:当一个 Thread 类或其子类的对象被声明或创建时,新生的线程对象处于新建状态,但是尚未取得运行该线程所需要的系统资源。
就绪状态:处于新建状态的线程被 start() 后,将进入线程队列等待 CPU 时间片,此时它已具备了运行的条件,只是没有分配到 CPU 资源。
运行状态:当就绪状态的线程被调度并获得 CPU 资源时,使用 run() 方法可让线程进入运行状态。
阻塞状态:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态。无论线程以何种方式进入阻塞状态,都会有相应的事件出现使线程返回到运行状态。
终止状态:当线程执行完毕或被提前强制性地终止或出现异常导致结束。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OJ2pYYTE-1638873742477)(D:单宇楠md文档md插图线程的生命周期.png)]
1.4 线程的同步线程在执行过程中,必须考虑的一个重要问题是与其他线程之间的共享数据或协调执行的问题。例如:以 A 和 B 这两个共享同一个账户的客户为例。如果开始银行账号的余额是 500 元人民币,A 存入了 200 元,并且同时 B 取出了 100 元。此时显示给 A 的余额是 600 元,而不是 700 元。这个例子中的错误是由线程间的并发引起的,当某个线程操作账户的过程中,尚未操作完成,其他线程参与进来,也操作该账户。如果两个线程同步,就不会出现上述问题。解决的方法是,如果 A 在存款时先做一个标记(锁定该账号),表示该账号正在被操作,然后再开始进行计算,修改余额的操作。这时 B 来取款,发现该账号上有正在被操作的标记(被锁定),则 B 只能等待。等 A 完成所有的存款事务后,B 才能对账号进行取款操作。这样 A 和 B 的操作就同步了。这个过程就是线程间的同步(synchronize),这种标记就是锁(lock)。
1.4.1 同步解决线程安全问题Java 提供了一种能够同步代码和数据的机制,来解决线程的安全问题。
方式一:同步代码块
synchronized(同步监视器) {
// 需要被同步的代码
}
说明:
- 操作共享数据的代码,即为需要被同步的代码。
- 共享数据:多个线程共同操作的变量。比如:银行账号的余额就是共享数据。
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。要求:多个线程必须要共用同一把锁。
补充:
- 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
- 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,可以考虑使用当前类充当同步监视器。
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。例如:
public synchronized void show() {
// 需要被同步的代码
}
说明:
-
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
-
非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身
同步的方式,解决了线程的安全问题。(好处)。但是操作同步代码时,只能由一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。(局限性)
1.4.2 线程的死锁问题死锁:不同的线程分别占用对方需要同步的资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
注意:出现死锁后,不会出现异常,也不会出现提示,只是所有的线程都处于阻塞状态,无法继续。使用同步时,需要避免出现死锁。
public class TestThread7 {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){ // 匿名类
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
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();
}
}
1.4.3 Lock(锁)
从 JDK 5.0 开始,Java 提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当。java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock ,可以显式加锁和释放锁。
import java.util.concurrent.locks.ReentrantLock;
class Window implements Runnable {
private int ticket = 100;
// 1.实例化 ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@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 {
// 3. 调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class TestLock {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
1.4.4 synchronized 与 lock 的对比
- Lock 是显式锁(手动开启和关闭锁),synchronized 是隐式锁,出了作用域自动释放。
- Lock 只有代码块锁,synchronized 有代码块锁和方法锁。
- 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
优先使用顺序:
Lock → 同步代码块(已经进入了方法体,分配了相应资源)→ 同步方法(在方法体之外)
常见面试题:
- synchronized 与 lock 的异同?
相同:二者都可以解决线程安全问题。
不同:synchronized 机制在执行完相应的同步代码以后,自动的释放同步监视器。
lock 需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())。
- 如何解决线程安全问题?有几种方式?
同步代码块、同步方法、Lock 锁。
1.4.5 练习银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000元,存三次。每次存完打印账户余额。
分析:
1.是否是多线程问题? 是,两个储户线程。
2.是否有共享数据? 有,账户(或账户余额)。
3.是否有线程安全问题? 有。
4.需要考虑如何解决线程安全问题? 同步机制:有三种方式。
class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
// 存钱
public synchronized void deposit(double amt) {
if(amt > 0) {
balance += amt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":存钱成功。余额为:" + balance);
}
}
}
class Customer extends Thread {
private Account acct;
public Customer(Account acct) {
this.acct = acct;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
acct.deposit(1000);
}
}
}
public class AccountTest {
public static void main(String[] args) {
Account acct = new Account(0);
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
结果:
甲:存钱成功。余额为:1000.0 甲:存钱成功。余额为:2000.0 甲:存钱成功。余额为:3000.0 乙:存钱成功。余额为:4000.0 乙:存钱成功。余额为:5000.0 乙:存钱成功。余额为:6000.01.5 线程的通信
Java 虽然内置了 synchronized 关键词用于对多线程进行同步,但是这还不能满足对多线程进行同步的所有需要。因为 synchronized 关键词仅仅能够对方法或代码块进行同步,如果一个应用需要跨越多个方法进行同步以及多个线程相互间进行交互,关键词 synchronized 就不能胜任了。Java提供了以下三个方法用于线程间的相互通信。
- wait() : 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify() : 一旦执行此方法,就会唤醒被 wait 的一个线程。如果有多个线程被 wait,就唤醒优先级高的那个。
- notifyAll() : 一旦执行此方法,就会唤醒所有被 wait 的线程。
说明:
-
wait(),notify(),notifyAll() 三个方法必须使用在同步代码块或同步方法中。(lock 中不可用)
-
wait(),notify(),notifyAll() 三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则,会出现 IllegalMonitorStateException 异常。
-
wait(),notify(),notifyAll() 三个方法是定义在 java.lang.Object 类中。
class Number implements Runnable {
private int number = 1;
@Override
public void run() {
while(true) {
synchronized (this) {
notify();
if(number < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
// 使得调用如下 wait() 方法的线程进入阻塞状态
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
public class TestCommunication {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
常见面试题:
sleep() 和 wait() 的异同?
-
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
-
不同点:1) 两个方法声明的位置不同:Thread 类中声明 sleep(),Object 类中声明 wait()。
2) 调用的要求不同:sleep() 可以在任何需要的场景下调用。wait() 必须使用在同步代码块或 同步方法中。
3) 关于是否释放同步监视器:如果两个方法都使用在同步代码块和同步方法中,sleep() 不会 释放锁,wait() 会释放锁。
经典例题:生产者 - 消费者问题
生产者和消费者共享同一个初始为空、数量固定(比如:20)的缓冲区,只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。
分析:
- 是否是多线程问题? 是,生产者线程,消费者线程
- 是否有共享数据? 是,产品
- 如何解决线程安全问题? 同步机制,有三种方法
- 是否涉及线程的通信? 是
class Buffer { // 缓冲区
private int productCount = 0;
// 生产产品
public synchronized void produceProduct() {
if(productCount < 20) {
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
notify();
}else {
try {
wait(); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 消费产品
public synchronized void consumeProduct() {
if(productCount > 0) {
System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
productCount--;
notify();
}else {
try {
wait(); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread { // 生产者
private Buffer buffer;
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
System.out.println(getName() + ":开始生产产品...");
while(true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
buffer.produceProduct();
}
}
}
class Consumer extends Thread { // 消费者
private Buffer buffer;
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费产品...");
while(true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
buffer.consumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Buffer buffer = new Buffer();
Producer p1 = new Producer(buffer);
p1.setName("生产者1");
Consumer c1 = new Consumer(buffer);
c1.setName("消费者1");
p1.start();
c1.start();
}
}
1.6 JDK 5.0 新增的线程创建方式
新增方式一:实现 Callable 接口
Runnable 封装一个异步运行的任务,可以把它想象成为一个没有参数和返回值的异步方法。Callable 与Runnable 类似,但是有返回值。Callable 接口是一个参数化的类型,只有一个方法 call()。
与使用 Runnable 相比,Callable 功能更强大。
- 相比 run() 方法,call() 方法可以有返回值。
- call() 方法可以抛出异常,被外面的操作捕获,获取异常的信息。
- Callable 是支持泛型的返回值。
- 需要借助 FutureTask 类,比如获取返回结果。
Future 接口
- 可以对具体的 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等。
- FutureTask 是 Future 接口的唯一的实现类
- FutureTask 同时实现了Runnable,Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
// 1. 创建一个实现 Callable 的实现类。
class NumThread implements Callable {
// 2.实现 call 方法,将此线程需要执行的操作声明在 call() 中。
@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;
}
}
public class ThreadNew {
public static void main(String[] args) {
// 3. 创建 Callable 接口实现类的对象
NumThread numThread = new NumThread();
// 4. 将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中,创建 FutureTask 的对象。
FutureTask futureTask = new FutureTask(numThread);
// 5. 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start()。
new Thread(futureTask).start();
try {
// 6. 获取 Callable 中的 call 方法中的返回值。
// get() 返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call() 的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
新增方式二:使用线程池
线程池是一种线程使用模式。如果我们需要多次使用线程,就意味着需要多次创建并销毁线程,这样的过程中会消耗很多i资源。所以为了避免在处理短时间任务时创建与销毁线程的代价,就提出了线程池的概念。线程池就是提前创建好多个线程,放入线程池中,使用是直接获取,使用完再放回线程池中,实现重复利用。
线程池优点
- 提高响应速度(减少了创建新线程的时间)。
- 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)。
- 便于线程管理:
- corePoolSize:常驻核心线程数。
- maximumPoolSize:最大线程数。
- keepAliveTime:线程没有任务时最多保持多长时间后会终止。
- …
线程池相关 API
JDK 5.0 开始提供了线程池相关 API:ExecutorService 和 Executors 。
ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor 。
- void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行 Runnable 。
- Future submit(Callable task):执行任务,有返回值,一般用来执行 Callable 。
- void shutdown():关闭连接池。
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池。
- Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池。
- Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池。
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
class NumberThread implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if(i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class NumberThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if(i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
// 1. 提供指定线程数量的线程池。
ExecutorService service = Executors.newFixedThreadPool(10);
// ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
// 设置线程池的属性
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
// 2. 执行指定的线程的操作。需要提供实现 Runnable 接口或 Callable 接口实现类的对象。
service.execute(new NumberThread()); // 适合使用于 Runnable
service.execute(new NumberThread1());
// service.submit(); // 适合使用于 Callable
// 3. 关闭线程池
service.shutdown();
}
}
常见面试题
创建线程有几种方式?分别是什么?
- 继承 Thread 类创建线程。
- 实现 Runnable 接口创建线程。
- 使用 Callable 和 Future 创建线程。
- 使用线程池。



