Java基础学习——第八章 多线程
一、程序、进程、线程的理解
1. 基本概念: 程序、进程、线程
- **程序(program)**是为完成特定任务、用某种语言编写的一组指令的集合。即指==一段静态的代码==,静态对象
- **进程(process)**是==程序的一次执行过程==,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程 —— 生命周期
- 如:运行中的QQ,运行中的MP3播放器
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位, 系统在运行时会为每个进程分配不同的内存区域
- 线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程==共享相同的内存单元/内存地址空间,即它们从同一堆中分配对象,可以访问相同的变量和对象==。这使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能会带来安全的隐患
2. 单核CPU和多核CPU的任务执行
- 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱, 那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。通过CPU的调度算法,使用户感觉像是同时处理多个任务,但同一时刻只有一个执行流占用CPU执行
- 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
- **一个Java应用程序java.exe,至少有三个线程: main()主线程,gc()垃圾回收线程,异常处理线程。**当然如果发生异常,会影响主线程
3. 并行与并发
- **并行: 多核多CPU同一时刻执行多个任务(线程),**即同时做不同事情的能力
- **并发: 一个CPU(通过CPU的调度算法,采用时间片)交替执行多个任务(线程),**即交替做不同事情的能力
4. 使用多线程的优点
- 背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
- 多线程程序的优点:
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验
- 提高计算机系统CPU的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
- 何时需要多线程:
- 程序需要同时执行两个或多个任务
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等
- 需要一些后台运行的程序时
三、创建线程的两种方式
1. Thread类的概述
- JVM(Java虚拟机)允许应用程序同时执行多个线程,它通过 java.lang.Thread类 来体现
- Thread类的特性:
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
- 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
- Thread类——构造器:
- Thread():空间构造器,创建新的Thread对象
- Thread(String threadname): 创建线程并指定线程实例名
- Thread(Runnable target): 指定创建线程的目标对象,它实现了Runnable接口中的run方法
- Thread(Runnable target, String name): 创建新的Thread对象
2. 创建线程的两种方式(JDK5.0之前)
- 继承Thread类
- 实现Runnable接口
2.1 创建线程的方式一:继承Thread类
2.1.1 具体步骤
- 步骤1:创建一个继承于==Thread类的子类==
- 步骤2:在子类中==重写Thread类的run()方法==:重写的run()方法的方法体中应声明此线程需要执行的操作
- 步骤3:在主线程中(main方法中)创建Thread类的子类对象,即==创建线程对象==
- 步骤4:通过该线程对象(一个Thread类的子类对象对应一个线程)调用start()方法,开启线程
- start()方法的作用:① 启动当前线程;② 调用当前线程的run()方法
- 注意点:
- 若想启动一个新的线程,则必须创建一个线程对象,并通过其调用start()方法,表示启用了该线程对象;若跳过start()方法,手动调用run()方法,此时并没有启动该线程对象,即run()方法还是在主线程中执行的
- **run()方法由JVM调用,**什么时候调用,执行的过程控制都有操作系统的CPU调度决定
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则抛出“IllegalThreadStateException”异常
- 若想启用多个线程,则必须创建多个线程对象,且每个线程对象都调用start()方法启动
例1:遍历100以内的所有数
/*
方式一:继承Thread类
例子:遍历100以内的所有数
Thread.currentThread().getName():返回当前线程的名称(String)
*/
//步骤1: 创建一个继承于Thread类的子类
class MyThread extends Thread {
//步骤2: 在子类中重写Thread类的run()方法:将此线程需要执行的操作声明在重写的run()方法的方法体中
@Override
public void run() {
//例子:遍历100以内的所有数
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//步骤3: 创建Thread类的子类对象,即创建线程对象(这一部分也是在主线程中在做的)
MyThread mt = new MyThread();
//步骤4: 通过该线程对象调用start()方法,启动分线程(这一部分也是在主线程中在做的)
mt.start(); //Thread-0:调用start()方法后,重写的run()方法是在分线程中执行的
//若跳过start()方法,直接调用run()方法,此时并没有开启分线程,即该run()方法还是在主线程中执行的
//t1.run();
//再启动一个线程:需要再new一个新的线程对象,再调用start()方法
MyThread mt2 = new MyThread();
mt2.start(); //Thread-1
//同时,主线程也在执行程序,此时两个线程中的程序是同时执行的,没有先后顺序
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i); //main
}
}
}
例2:创建两个分线程
- 创建两个分线程:一个线程遍历100以内的偶数;另一个线程遍历100以内的奇数
- 正常按步骤写:
class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.start();
t2.start();
}
}
- 简便写法:创建Thread类的匿名子类
public class ThreadDemo {
public static void main(String[] args) {
//创建Thread类的匿名子类的方式
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
}
例3:多窗口卖票(继承Thread类)
- 例子:创建三个窗口卖票,总票数为100张,使用继承Thread类的方式创建多线程
- 需要把ticket属性设置为static的,即所有线程对象共享同一个ticket属性(共享100张票);若不声明为static,则在堆空间中,每个线程对象都有一个独立的ticket属性,即每个窗口卖独立的100张票
- 该写法目前存在线程安全问题,待解决!
class Window extends Thread {
public Window(String name) {
super(name);
}
//需要把ticket属性设置为static的,即所有线程对象共享同一个ticket属性(共享100张票)
//若不声明为static,则在堆空间中,每个线程对象都有一个独立的ticket属性,即每个窗口卖独立的100张票
private static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(getName() + ":卖票,票号为" + ticket);
ticket--;
} else {
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
//存在线程安全问题,待解决!
Window w1 = new Window("窗口一");
Window w2 = new Window("窗口二");
Window w3 = new Window("窗口三");
w1.start();
w2.start();
w3.start();
}
}
2.2 创建线程的方式二:实现Runnable接口
2.2.1 具体步骤
- 步骤1:创建一个实现了==Runnable接口的实现类==
- 步骤2:在实现类中==实现Runnable接口的抽象方法run()==:实现的run()方法的方法体中应声明此线程需要执行的操作
- 步骤3:在主线程中(main方法中)创建Runnable接口的实现类对象,
- 步骤4:将此==实现类对象作为参数传递给Thread类的构造器中,创建Thread类的对象,即创建线程对象==
- 步骤5:通过线程对象(一个Thread类的对象对应一个线程)调用start()方法,开启线程,start()方法会自动调用Runnable接口的实现类中重新实现的run()方法
例1:实现Runnable接口创建线程
//步骤1:创建一个实现了Runnable接口的实现类
class RunnableThread implements Runnable {
//步骤2:在实现类中实现Runnable接口的抽象方法run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i); //Thread-0
}
}
}
}
public class RunnableTest {
public static void main(String[] args) {
//步骤3:创建Runnable接口的实现类对象
RunnableThread mTHread = new RunnableThread();
//步骤4:将此实现类对象作为参数传递给Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(mTHread);
t1.setName("线程一");
//步骤5:通过线程对象(Thread类的对象)调用start()方法,start()方法会自动调用实现类中重新实现的run()方法
t1.start();
//再启动一个线程
Thread t2 = new Thread(mTHread);
t2.setName("线程二");
t2.start();
}
}
例2:多窗口卖票(实现Runnable接口)
- 例子:创建三个窗口卖票,总票数为100张,使用实现Runnable接口的方式创建多线程
- 无需把ticket属性设置为static的,因为三个线程对象调用start()方法后,都会通过同一个实现类对象调用实现类中的run()方法;ticket属性存储在实现类的内存空间中,即堆空间中只有唯一的一个ticket属性(共享100张票)
class Window1 implements Runnable {
//无需把ticket属性设置为static,因为三个线程对象调用start()方法后,都会通过同一个实现类对象调用
//run()方法;ticket属性存储在实现类的内存空间中,即堆空间中只有唯一的一个ticket属性(共享100张票)
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket);
ticket--;
} else {
return;
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
Window1 t = new Window1();
//创建三个Thread的对象(线程对象),将同一个实现类的对象作为参数传给Thread类的构造器
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//通过三个不同的线程对象调用start()方法,start方法自动调用同一个实现类中的run()方法
t1.start();
t2.start();
t3.start();
}
}
3. 两种创建线程方式的联系和区别
- 联系:Thread类本身就是Runnable接口的一个实现类
public class Thread extends Object implements Runnable
- 区别:
- 继承Thread类:线程需要执行的代码声明在Thread类的子类重写的run()方法中
- 实现Runnable接口:线程需要执行的代码声明在Runnable接口的实现类实现的run()方法中
- 实现Runnable接口的方式的优点:
- 避免了单继承的局限性:继承的方式导致子类只能继承于Thread类;而实现的方式使得实现类可以继承别的类,同时可以实现多个接口
- 实现的方式使得多个线程可以共享同一个接口实现类的对象,适合多个线程处理同一份共享数据的情况
- 开发中,优先选择实现Runnable接口的方式来创建多线程
四、Thread类中的常用方法
1. 常用方法
- **void start():**启动当前线程,并调用当前线程对象的run()方法
- **void run():**线程在被调度时执行的操作,通常需要在子类中进行重写
- **String getName():**返回当前线程对象的名称
- **void setName(String name):**设置当前线程对象的名称
- **static Thread currentThread():**返回当前线程对象。在Thread的子类中就表示this,通常用于主线程和Runnable实现类
- **static void yield():**线程让步
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法
- **void join():**在线程a中调用线程对象b的 join() 方法,线程a将进入阻塞状态,直到调用join() 方法的线程b执行完以后,线程a才会结束阻塞状态
- 在高优先级的线程中,也可以调用低优先级的线程对象的 join() 方法
- static void sleep(long millis):(指定时间:毫秒)
- 令当前活动线程在指定时间段内放弃对CPU控制(阻塞状态),使其他线程有机会被执行,时间到后重排队
- 抛出InterruptedException异常
- stop():强制线程生命期结束,不再使用
- **boolean isAlive():**返回boolean,判断线程是否还存活
class ThreadMethod extends Thread {
//构造器
public ThreadMethod(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
try {
//令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":" + i);
}
if (i % 20 == 0) {
//线程让步:暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
yield();
}
}
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
ThreadMethod t1 = new ThreadMethod("线程一");
//t1.setName("线程一");
t1.start();
//给主线程命名
Thread.currentThread().setName("主线程");
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if (i == 20) {
try {
//在线程a中调用线程对象b的join()方法,线程a将进入阻塞状态,
//直到调用join()方法的线程b执行完以后,线程a才会结束阻塞状态
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//判断线程是否还存活
System.out.println(t1.isAlive());
}
}
2. 线程优先级的设置
2.1 线程的调度
2.2 线程的优先级
- 线程的优先级等级:
- 最大优先级 MAX_PRIORITY: 10
- 最小优先级 MIN_PRIORITY: 1
- 默认优先级 NORM_PRIORITY: 5
- 如何获取和设置当前线程的优先级:
- **getPriority():**返回当前线程的优先等级
- **setPriority(int newPriority):**改变当前线程的优先等级
- 说明:
- 子类线程创建时继承父类线程的优先级
- 高优先级的线程会抢占低优先级线程的cpu执行权,但这种抢占只是体现为获得调度的概率较高;也就是说,低优先级线程只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
//低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
class ThreadMethod extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(getName() + ":" + getPriority() + ":" + i);
}
}
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
ThreadMethod t1 = new ThreadMethod();
//设置分线程的优先级
t1.setPriority(Thread.MAX_PRIORITY); //把分线程的优先级调高:10
t1.start();
//设置主线程的优先级
Thread.currentThread().setPriority(Thread.MIN_PRIORITY); //把主线程的优先级调低:1
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
}
}
}
五、线程的生命周期:五种状态
- 要想实现多线程,必须在主线程中创建新的线程对象(一个线程对象对应一个线程)。Java言==使用Thread类及其子类的对象来表示线程==,Thread.State类定义了线程在其一个完整的生命周期中通常要经历如下的五种状态:
- **新建:**当一个==Thread类或其子类的对象被声明并创建(new)时,新生的线程对象==处于新建状态
- **就绪:**处于新建状态的线程被==start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是还没分配到CPU资源==
- **运行:**当就绪的线程被调度并==获得CPU资源时,便进入运行状态==, ==run()==方法定义了线程的操作和功能
- **阻塞:**在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU执行权并临时中止自己的执行,进入阻塞状态。阻塞状态是临时的,不能作为最终状态
- **死亡:**当线程==执行完run()方法或线程被提前强制性地中止或出现异常且没处理导致结束==
六、线程的同步机制
1. 多线程的安全问题
- 多线程的安全问题:
- 多个线程执行的不确定性导致执行结果的不稳定
- 多个线程操作同一个共享数据,会导致操作的不完整性,从而破坏数据
- 问题的原因:在多个线程操作同一个共享数据的情况下,当某个线程只执行了部分操作(尚未操作完成)时,其他线程也参与进来,操作共享数据,从而导致共享数据的错误
- 解决方案:当一个线程在操作共享数据时(即使该线程出现了阻塞),不允许其他线程参与进来;直到该线程执行完所有操作后,才允许其他线程操作共享数据
- 在Java中,我们通过==同步机制==,来解决线程的安全问题
2. 同步机制的实现方式
- Java对于多线程的安全问题提供了专业的解决方式: 同步机制
- 解决线程安全问题的3个方式:
- 方式一:同步代码块(JDK5.0之前)
- 方式二:同步方法(JDK5.0之前)
- 方式三:Lock锁(JDK5.0新增)
- 同步机制的优缺点:
- 优点:解决了多线程的安全问题
- 缺点:同一时间只允许一个线程执行被同步的代码,其他线程只能等待,相当于是一个单线程的过程,效率变低
2.1 方式一:同步代码块(Synchronized关键字)
synchronized (同步监视器) {
// 需要被同步的代码(操作共享数据的代码);
}
- 需要被同步的代码:操作共享数据的代码
- 共享数据:多个线程共同操作的变量。如多窗口卖票问题中的ticket
- 同步监视器(同步锁) —— 对象:
- ① 任何一个类的对象,都可以作为同步锁;
- ② 操作共享数据的多个线程必须共用同一把锁,即==充当同步锁的对象必须是唯一的==
- ③ 在实现Runnable接口的方式中,锁可以是==this,表示当前对象,而当前调用run()方法的对象是实现类的对象,是唯一的;在继承Thread类的方式中,锁一般不能是this,因为调用run()方法的对象是Thread类的子类对象(线程对象),不是唯一的。但可以使用类名.class充当同步锁(类也是对象,且类只会加载一次==)
2.1.1 同步代码块处理实现Runnable接口的线程安全问题
- 以多窗口卖票问题为例
//在实现Runnable接口的方式中,锁可以是this,表示当前对象。当前调用run()方法的对象是实现类的对象,是唯一的
class Window1 implements Runnable {
//无需把ticket属性设置为static的,所有线程对象都调用同一个实现类中的run()方法,堆空间中只有唯一的一个ticket属性(共享100张票)
private int ticket = 100;
//同步监视器(锁):任何一个类的对象,都可以充当锁
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (this) { //this表示当前对象,而当前调用run()方法的对象是唯一的,即为实现类Window1的对象
//synchronized (obj) {
//即为需要被同步的代码:操作共享数据的代码
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
Window1 t = new Window1();
//创建三个Thread的对象(线程对象),将同一个实现类的对象作为参数传给Thread类的构造器
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//通过三个不同的线程对象调用start()方法,start方法自动调用同一个实现类中的run()方法
t1.start();
t2.start();
t3.start();
}
}
2.1.2 同步代码块处理继承Thread类的线程安全问题
//在继承Thread类的方式中,锁不能是this,因为调用run()方法的对象是Thread类的子类对象(线程对象),不是唯一的。但可以使用"类名.class"以类充当对象(类也是对象,且类只会加载一次)
class Window extends Thread {
public Window(String name) {
super(name);
}
//需要把ticket属性设置为static的,即所有线程对象共享同一个ticket属性(共享100张票)
//若不声明为static,则每个线程对象都有一个独立的ticket属性,即每个窗口卖独立的100张票
private static int ticket = 100;
//同步监视器(同步锁):① 任何一个类的对象,都可以作为同步锁;② 操作共享数据的多个线程必须共用同一把锁
//这里需要把锁声明为static,才能多个线程共用同一把锁
private static Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (Window.class) { //Window.class表示Window类,类也是对象,且是唯一的
//synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window("窗口一");
Window w2 = new Window("窗口二");
Window w3 = new Window("窗口三");
w1.start();
w2.start();
w3.start();
}
}
2.2 方式二:同步方法(Synchronized关键字)
- 如果需要被同步的代码(操作共享数据的代码)正好完整声明在一个方法中,可以直接将synchronized放在方法声明中,表示该方法为==同步方法==
public synchronized void show() {
// 操作共享数据的代码;
}
- 同步方法仍然涉及到同步监视器(同步锁),只是不需要显式的声明
- 非静态的同步方法,同步监视器(同步锁)是:this
- 静态的同步方法,同步监视器(同步锁)是:类名.class,即当前类本身
2.2.1 同步方法处理实现Runnable接口的线程安全问题
class Window2 implements Runnable {
//无需把ticket属性设置为static的,所有线程对象都调用同一个实现类中的run()方法,堆空间中只有唯一的一个ticket属性(共享100张票)
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
//同步方法
public synchronized void show() { //非静态同步方法的锁: this
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket);
ticket--;
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
Window2 t = new Window2();
//创建三个Thread的对象(线程对象),将同一个实现类的对象作为参数传给Thread类的构造器
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//通过三个不同的线程对象调用start()方法,start方法自动调用同一个实现类中的run()方法
t1.start();
t2.start();
t3.start();
}
}
2.2.2 同步方法处理继承Thread类的线程安全问题
class Window3 extends Thread {
public Window3(String name) {
super(name);
}
//需要把ticket属性设置为static的,即所有线程对象共享同一个ticket属性(共享100张票)
//若不声明为static,则每个线程对象都有一个独立的ticket属性,即每个窗口卖独立的100张票
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
//静态同步方法的锁: 类名.class,因为类只加载一次,类方法也只加载一次,所以锁是唯一的
public static synchronized void show() {
//非静态同步方法的锁: this,在继承Thread类的情况下,调用run方法的对象是不同的线程对象,故锁不唯一
//public synchronized void show() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket);
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w1 = new Window3("窗口一");
Window3 w2 = new Window3("窗口二");
Window3 w3 = new Window3("窗口三");
w1.start();
w2.start();
w3.start();
}
}
2.3 方式三:Lock锁
- 从JDK 5.0开始, Java提供了更强大的线程同步机制——通过==显式定义同步锁对象来实现同步。同步锁使用Lock对象==充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 在实现线程安全的控制中,比较常用的是ReentrantLock, 可以显式加锁、释放锁
class A{
//1.步骤1:实例化一个ReentrantLock类的对象
private final ReentrantLock lock = new ReenTrantLock();
public void test() {
//步骤2:将操作共享数据的代码用try-finally块包起来
try {
//步骤3:调用ReentrantLock类对象的lock()方法:相当于设置同步监视器(同步锁)
lock.lock();
//操作共享数据的代码;
} finally {
//步骤4:在finally中调用ReentrantLock类对象的unlock()方法:解锁方法
lock.unlock();
}
}
}
2.3.1 Lock锁处理实现Runnable接口的线程安全问题
class Windows implements Runnable {
private int ticket = 100;
//1.步骤1:实例化一个ReentrantLock类的对象
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//步骤2:将操作共享数据的代码用try-finally块包起来
try {
//步骤3:调用ReentrantLock类对象的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 {
//步骤4:在finally中调用ReentrantLock类对象的unlock()方法:解锁方法
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Windows w = new Windows();
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();
}
}
2.3.2 Lock锁处理继承Thread类的线程安全问题
class Windows2 extends Thread {
private static int ticket = 100;
//1.步骤1:实例化一个ReentrantLock类的对象
//在继承Thread类的方式下,ReentrantLock类的对象也需要声明为static的,否则每个Thread类的子类对象都会重新new一个ReentrantLock类对象
private static final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//步骤2:将操作共享数据的代码用try-finally块包起来
try {
//步骤3:调用ReentrantLock类对象的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 {
//步骤4:在finally中调用ReentrantLock类对象的unlock()方法:解锁方法
lock.unlock();
}
}
}
}
public class LockTest1 {
public static void main(String[] args) {
Windows2 t1 = new Windows2();
Windows2 t2 = new Windows2();
Windows2 t3 = new Windows2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
2.4 synchronized 与 Lock 的异同
- 相同点:二者是用来解决线程安全问题的
- 不同点:
- Lock是显式锁:手动开启 “lock()” 和关闭锁 “unlock()”;synchronized是隐式锁,出了作用域后自动释放
- Lock只有代码块锁,synchronized有代码块锁(同步代码块)和方法锁(同步方法)
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:Lock锁 —> 同步代码块(已经进入了方法体,分配了相应资源)—> 同步方法(在方法体外)
3. 线程安全的单例模式之懒汉式
/*
* 使用同步机制(①同步代码块;②同步方法)实现单例模式(懒汉式)的线程安全
* 推荐使用方法三:使用同步代码块的高效实现
*/
class Bank {
//1.私有化构造器
private Bank() {
}
//2.在类内部声明一个当前类对象的引用(private),但不对其进行初始化(懒汉式只有在调用方法后才new对象)
//4.由于静态方法中只能访问类中的静态成员变量,因此,指向类内部对象实例的引用类型变量也必须定义成静态的
private static Bank instance = null;
//3.提供一个公共的静态的方法,返回类内部创建的唯一的对象实例
//方式一:将该静态方法加上synchronized关键字,变成同步方法,此时该静态同步方法的锁是:Bank.class
//public static synchronized Bank getInstance() {
// if (instance == null) {
// //与饿汉式不同,懒汉式直接在方法内部创建对象,且只创建一次
// instance = new Bank();
// }
// return instance;
//}
//方式二:将操作共享数据的代码放在同步代码块中,此时锁是:Bank.class;因为静态方法中不能使用this
//方式二的效率比较低
//public static Bank getInstance() {
// synchronized (Bank.class) {
// if (instance == null) {
// //与饿汉式不同,懒汉式直接在方法内部创建对象,且只创建一次
// instance = new Bank();
// }
// return instance;
// }
//}
//方式三:使用同步代码块的高效实现
public static Bank getInstance() {
//在对象成功创建之后,其他线程再执行该部分代码,不会再进到同步代码块中,而是直接返回instance对象
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
//与饿汉式不同,懒汉式直接在方法内部创建对象,且只创建一次
instance = new Bank();
}
}
}
return instance;
}
}
4. 线程的死锁问题
4.1 线程死锁的概念
- 不同的线程分别占用对方需要的同步资源(同步锁)不放弃,都在等待对方放弃自己需要的同步资源(同步锁),这就是线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
4.2 线程死锁的例子
public class DeadLockTest {
public static void main(String[] args) {
final StringBuffer s1 = new StringBuffer();
final StringBuffer s2 = new StringBuffer();
//第一个线程: 使用继承Thread类的方式创建线程
//将Thread类的匿名子类的匿名对象作为线程对象
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();
//第二个线程: 使用实现Runnable接口的方式创建线程
//将Runnable接口的匿名实现类的匿名对象作为参数传递给Thread类的构造器,创建一个Thread类的匿名对象作为线程对象
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();
}
}
4.3 如何避免线程死锁
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
5. 线程的同步机制例题
- 有两个储户分别向同一个银行账户存3000元, 每次存1000,存3次。每次存完打印账户余额。问题: 该程序是否有安全问题,如果有,如何解决?
- 明确哪些代码是多线程运行代码,须写入run()方法
- 明确什么是共享数据。
- 明确多线程运行代码中哪些语句是操作共享数据的。
- 拓展问题:可否实现两个储户交替存钱的操作
class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
//存钱:直接写成同步方法,这里不用再声明为static,因为此时调用该方法的Account对象是唯一的
//非静态同步方法的同步锁为this,但此时this表示三个线程共用的Account对象,因此共用一把锁
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++) {
//三个线程都是通过同一个Account对象调用deposit()方法,因此锁是唯一的
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();
}
}
七、线程的通信
1. 线程通信的引入
例题:使用两个线程打印 1-100;线程1、线程2交替打印
class Number implements Runnable {
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (this) { //synchronized (obj) {
//唤醒被wait()的一个线程,使其结束阻塞状态
//方法的调用者必须是同步代码块或同步方法中的同步监视器(同步锁),此时同步锁为this
//若同步锁对象变为obj,则调用该方法的对象也必须为obj,即obj.notify();
notify(); //obj.notify();
if (number <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使当前线程阻塞并释放同步监视器(同步锁),使其他线程可访问(持锁)并修改共享数据
//方法的调用者必须是同步代码块或同步方法中的同步监视器(同步锁),此时同步锁为this
//若同步锁对象变为obj,则调用该方法的对象也必须为obj,即obj.wait();
wait(); //obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number n = new Number();
Thread t1 = new Thread(n);
Thread t2 = new Thread(n);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
2. 线程通信的三个方法
2.1 wait()
- 在当前线程中调用该方法:① this.wait():此时同步锁为this,且this可省略;② 对象的引用.wait():该==方法的调用者必须是同步锁对象==
- 调用该方法的条件:当前线程必须具有对同步监视器(同步锁)的所有权
- 作用:使当前线程挂起进入阻塞状态,并释放同步监视器(同步锁),使其他线程可获得同步锁并修改共享数据
- 当前线程在被wait()后,进入阻塞状态,等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对同步监视器(同步锁)的所有权后才能继续执行
2.2 notify() 和 notifyAll()
- 在当前线程中调用该方法:① this.wait():此时同步锁为this,且this可省略;② 对象的引用.wait():该==方法的调用者必须是同步锁对象==
- 调用该方法的条件:当前线程必须具有对同步监视器(同步锁)的所有权
- 作用:
- notify():唤醒被wait()的一个线程,使其结束阻塞状态;若有多个线程被wait(),则唤醒优先级最高的线程
- notifyAll():唤醒所有被wait()的线程
2.3 wait() | notify() | notifyAll()的说明
- 这三个方法==只能在同步代码块(synchronized)或同步方法中使用==,否则会报IllegalMonitorStateException异常
- 方法的调用者必须是同步代码块或同步方法中的同步监视器(同步锁),否则会报IllegalMonitorStateException异常;而==任意对象都可以作为synchronized的同步监视器(同步锁),因此这三个方法只能在Object类中声明==
3. sleep() 和 wait() 的异同
- 相同点:一旦执行这两个方法,都可以使当前线程进入阻塞状态
-
不同点:
- ① 两个方法声明的位置不同:Thread类中声明sleep()方法;Object类中声明wait()方法
- ② 调用的要求不同:sleep()是一个静态方法,可以在任何需要的场景下由Thread类直接调用;而wait()方法是一个非静态方法,必须由同步代码块或同步方法中的同步监视器(同步锁)调用
- ③ 是否释放同步监视器(锁):如果两个方法都使用在同步代码块或同步方法中,sleep()方法在使当前线程进入阻塞状态后,不会释放同步锁;而wait()方法在使当前线程进入阻塞状态后,会释放同步锁,使其他线程有机会获得同步锁的所有权
4. 总结:会释放锁 & 不会释放锁的操作
- 会释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步代码块、同步方法中遇到break、 return终止了该代码块、方法的执行
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
- 当前线程在同步代码块、同步方法中通过同步监视器(锁)调用了wait()方法,导致当前线程阻塞,并释放锁
- 不会释放锁的操作:
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器(锁);应尽量避免使用suspend()和resume()来控制线程
5. 经典例题:生产者 / 消费者问题
- 生产者(Producer)将产品交给店员(Clerk),消费者(Customer)从店员处取走产品。店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会叫消费者等一下,如果店中有产品了再通知消费者来取产品
- 这里可能出现两个问题:
- 生产者比消费者快时,消费者会漏掉一些数据没有取到
- 消费者比生产者快时,消费者会取相同的数据
- 问题分析:
- 是否是多线程问题?—— 是,生产者线程和消费者线程
- 是否有共享数据?—— 是,店员(或店员处的产品)
- 如何解决线程的安全问题?—— 使用同步机制:三种方法
- 是否涉及线程的通信?—— 是,使用线程通信的三个方法
//共享数据:店员(或店员处的产品)
class Clerk {
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 Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始生产产品......");
while (true) {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
//消费者线程
class Customer extends Thread {
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费产品......");
while (true) {
try {
Thread.sleep(20);
} catch (Exception e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
//两个线程对象共用同一个共享数据(店员对象)
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Customer customer1 = new Customer(clerk);
Customer customer2 = new Customer(clerk);
producer.setName("生产者1");
customer1.setName("消费者1");
customer2.setName("消费者2");
producer.start();
customer1.start();
customer2.start();
}
}
八、JDK5.0新增创建线程的两种方式
1. 创建线程的方式三:实现Callable接口
1.1 具体步骤
- 步骤1:创建一个实现了Callable接口的实现类
- 步骤2:在实现类中实现Callable接口的抽象方法call(),将此线程需要执行的操作声明在call()方法中,并指定返回值,返回值可以是任意类型的
- 步骤3:在主线程中(main方法中)创建Callable接口的实现类对象
- 步骤4:将此实现类对象作为参数传递给FutureTask类的构造器中,创建FutureTask类的对象
- 步骤5:由于FutureTask类也实现了Runnable接口,故其也是Runnable接口的实现类。将此FutureTask类的对象(Runnable接口实现类对象)作为参数传递给Thread类的构造器中,创建Thread类的对象(线程对象)
- 步骤6:通过线程对象(Thread类的对象)调用start()方法,启动线程
- 若需要获取call()方法中声明的返回值,则需要额外调用FutureTask类中定义的get()方法:
7. 步骤7:通过FutureTask类的对象调用get()方法,get()方法的返回值即为FutureTask构造器参数Callable实现类重写的call()方法的返回值
8. 步骤8:将调用get()方法的部分用try-catch包起来,解决call()方法中抛出的异常
//步骤1:创建一个实现了Callable接口的实现类
class NumThread implements Callable {
//步骤2:在实现类中实现Callable接口的抽象方法call(),返回值可以是任意类型的
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i < 100; i++) {
if (i % 2 == 0) {
sum += i;
}
}
return sum; //自动装箱 + 多态
}
}
public class CallableTest {
public static void main(String[] args) {
//步骤3:创建Callable接口的实现类对象
NumThread numThread = new NumThread();
//步骤4:将此实现类对象作为参数传递给FutureTask类的构造器中,创建FutureTask类的对象
FutureTask futureTask = new FutureTask(numThread);
//步骤5:将此FutureTask类的对象(Runnable接口实现类对象)作为参数传递给Thread类的构造器中,创建Thread类的对象(线程对象)
//步骤6:通过线程对象(Thread类的对象)调用start()方法,启动线程
new Thread(futureTask).start();
//*****若需要获取call()方法中声明的返回值,则需要额外调用FutureTask类中定义的get()方法*****
try {
//步骤7:通过FutureTask类的对象调用get()方法,get()方法的返回值即为FutureTask构造器参数Callable实现类重写的call()方法的返回值
//步骤8:将调用get()方法的部分用try-catch包起来,处理call()方法中抛出的异常
Object sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
1.2 为什么实现Callable接口的方式比实现Runnable接口的方式更强大?
- 相比Runnable接口中的run()方法,Callable接口中的call()方法可以有返回值
- Callable接口中的call()方法可以抛出异常,在方法外部通过try-catch捕获,从而获取异常信息
- Callable接口支持泛型的返回值
- 需要借助==FutureTask类==,比如获取返回结果
- Future接口:
- 可以对具体Runnable、 Callable任务的执行结果进行取消、查询是否完成、获取结果等
- FutrueTask是Futrue接口的唯一的实现类
- FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
- Future接口:
2. 创建线程的方式四:使用线程池
2.1 线程池的思想和优点
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
- 思想:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。避免频繁创建销毁、实现重复利用。此时不用每次都手动创建线程对象,但是仍需要将Runnable接口的实现类对象或Callable接口的实现类对象作为参数传递给线程池中已有的线程对象,即我们==仍需要在Runnable接口实现类的run()方法或Callable接口实现类的call()方法中规定当前线程需要执行的操作==,即这两个接口的实现类中实现的抽象方法是要告诉线程需要做什么
- 线程池的优点:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
2.2 线程池相关API
- JDK 5.0起提供了线程池相关的API: ExecutorService 和 Executors
- ExecutorService:真正的线程池接口。常见实现类ThreadPoolExecutor
- void execute(Runnable command) :执行任务/命令,没有返回值,适用于实现Runnable接口的方式,参数是Runnable接口的实现类对象
- <T> Future<T> submit(Callable<T> task):执行任务,有返回值,适用于实现Callable接口的方式,参数是Callable接口的实现类对象
- **void shutdown() **:关闭连接池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可以安排在给定延迟后运行命令或者定期地执行
2.3 具体步骤
- 步骤1:调用Executors类中的方法创建线程池对象(ExecutorService接口的实现类ThreadPoolExecutor的对象),并可以设置线程池的属性(线程管理)
- 步骤2:创建Runnable接口的实现类或Callable接口的实现类
- 步骤3:在实现类中实现Runnable接口的抽象方法run()或Callable接口的抽象方法call(),为线程指定需要执行的操作
- 步骤4:调用线程池对象的execute()方法或submit()方法,来执行指定的线程操作。需要将Runnable接口的实现类对象作为参数传递给execute()方法或将Callable接口的实现类对象作为参数传递给submit()方法
- execute()方法适用于实现Runnable接口的方式,参数是Runnable接口的实现类对象
- submit()方法适用于实现Callable接口的方式,参数是Callable接口的实现类对象
- 步骤5:调用线程池对象的shutdown()方法手动关闭连接池
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
//步骤2:创建Runnable接口的实现类或Callable接口的实现类
class NumberThread implements Runnable {
//步骤3:在实现类中实现Runnable接口的抽象方法run()或Callable接口的抽象方法call()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
//步骤2:创建Runnable接口的实现类或Callable接口的实现类
class NumberThread1 implements Callable {
//步骤3:在实现类中实现Runnable接口的抽象方法run()或Callable接口的抽象方法call()
@Override
public Object call() throws Exception {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
return null;
}
}
public class ThreadPool {
public static void main(String[] args) {
//步骤1:调用Executors类中的方法创建一个特定的线程池对象(ExecutorService接口的实现类ThreadPoolExecutor的对象)
ExecutorService service = Executors.newFixedThreadPool(10); //接口的引用指向了实现类的对象
//设置线程池的属性(线程管理)
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
service1.setCorePoolSize(15);
//步骤4:调用线程池对象的execute()方法或submit()方法,来执行指定的线程操作
//需要将Runnable接口的实现类对象作为参数传递给execute()方法或将Callable接口的实现类对象作为参数传递给submit()方法
service.execute(new NumberThread()); //execute()方法适用于实现Runnable接口的方式,参数是Runnable接口的实现类对象
service.submit(new NumberThread1()); //submit()方法适用于实现Callable接口的方式,参数是Callable接口的实现类对象
//步骤5:调用线程池对象的shutdown()方法手动关闭连接池
service.shutdown();
}
}