目录
什么是线程?
什么是进程?
线程的生命周期
线程的6种状态互相转换
什么是多线程?
如何在程序中创建出多条线程?
多线程的注意事项
多线程的创建方式一:继承Thread类
方式一优缺点:
代码演示:
多线程的创建方式二:实现Runnable接口
方式二的优缺点
代码演示:
线程创建方式二的匿名内部类写法
前两种线程创建方式都存在一个问题
多线程的第三种创建方式:利用Callable接口、FutureTask类实现
FutureTask的API
线程创建方式三的优缺点
代码演示:
Thread提供了很多与线程操作相关的方法
代码演示:
线程安全问题
线程安全问题出现的原因?
取钱案例
代码:
线程同步
方法一、同步代码块
同步锁的注意事项
代码演示:
方法二、同步方法
同步方法底层原理
代码演示:
同步代码块好还是同步方法好?
方法三、Lock锁
Lock的常用方法
代码演示:
线程通信
什么是线程通信?
Object类的等待和唤醒方法:
线程通信的模型
线程池
什么是线程池?
线程池的工作原理
如何得到线程池对象?
ThreadPoolExecutor构造器
新任务拒绝策略:
ExecutorService的常用方法
代码演示:
使用线程池处理Runnable任务
使用线程池处理Callable任务
核心线程数量到底配置为多少好?
还可以通过Executors创建线程池对象
Executors使用可能存在的陷阱
练习
什么是线程?
- 线程(Thread)是一个程序内部的一条执行流程
- 程序中如果只有一条执行流程,那这个程序就是单线程的程序
什么是进程?
- 正在运行的程序(软件)就是一个独立的进程
- 线程是属于进程的,一个进程中可以同时运行很多个线程
- 进程中的多个线程其实是并发和并行执行的
并发:进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发
并行:在同一个时刻上,同时有多个线程在被CPU调度执行
多线程是并发和并行同时进行的!
线程的生命周期
- 也就是线程从生到死的过程中,经历的各种状态及状态转换
- Java总共定义了6种状态,6种状态都定义在Thread类的内部枚举类中
线程的6种状态互相转换
什么是多线程?
- 多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)
多线程体现在哪些方面?
比如日常的买票,百度网盘的多个下载任务,都使用到了多线程技术,再例如消息通信、淘宝、京东系统都离不开多线程技术
如何在程序中创建出多条线程?
- Java是通过java.lang.Thread 类的对象来代表线程的
多线程的注意事项
1、启动线程必须是调用start方法,不是调用run方法
- 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行
- 只有调用start方法才是启动一个新的线程执行
2、不要把主线程任务放在启动子线程之前
- 这样主线程一直是先跑完的,相当于是一个单线程的效果了
多线程的创建方式一:继承Thread类
- 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
- 创建MyThread类的对象
- 调用线程对象的start()方法启动线程(启动后还是执行run方法的)
方式一优缺点:
- 优点:编码简单
- 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展
代码演示:
MyThread:
//1、让子类继承Thread线程类
public class MyThread extends Thread{
//2、必须重写Thread类的run方法
@Override
public void run() {
//描述线程的执行任务
for (int i = 0; i < 5; i++) {
System.out.println("子线程输出" + i);
}
}
}
test:
//main方法是由一条默认的主线程负责执行
public class threadTest1 {
public static void main(String[] args) {
//3、创建MyThread线程类对象代表一个线程
Thread t = new MyThread();
//4、启动线程(自动执行run方法)
t.start(); //此时已是多线程,main线程,t线程
for (int i = 0; i < 5; i++) {
System.out.println("主线程main输出" + i);
}
}
}
运行结果:
多线程的创建方式二:实现Runnable接口
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
- 创建MyRunnable任务对象
- 把MyRunnable任务对象交给Thread处理:
- 调用线程对象的start()方法启动线程
方式二的优缺点
- 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强
- 缺点:需要多一个Runnable对象
代码演示:
MyRunnable:
//1、定义一个任务类,实现Runnable接口
public class MyRunnable implements Runnable{
//2、重写runnable的run方法
@Override
public void run() {
//线程要执行的任务
for (int i = 0; i < 5; i++) {
System.out.println("嘿嘿嘿嘿嘿!!!!!!");
}
}
}
test:
package com.zeyu.thread;
public class threadTest2 {
public static void main(String[] args) {
//3、创建任务对象
Runnable target = new MyRunnable();
//把任务对象交给一个线程对象处理
new Thread(target).start();
for (int i = 0; i < 5; i++) {
System.out.println("耶耶耶耶耶!!!!!!");
}
}
}
运行结果:
线程创建方式二的匿名内部类写法
- 可以创建Runnable的匿名内部类对象
- 再交给Thread线程对象
- 再调用线程对象的start()启动线程
public class threadTest2_2 {
public static void main(String[] args) {
//1、直接创建Runnable的匿名内部类形式(任务对象)
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("嘻嘻" + i);
}
}
};
new Thread(runnable).start();
//简化1
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("呵呵" + i);
}
}
}).start();
//简化2
new Thread(() ->{
for (int i = 0; i < 5; i++)
System.out.println("耶耶" + i);
}).start();
for (int i = 0; i < 5; i++) {
System.out.println("哈哈" + i);
}
}
}
前两种线程创建方式都存在一个问题
- 假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果
怎么解决这个问题?
- JDK 5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)
- 这种方式最大的优点:可以返回线程执行完毕后的结果
多线程的第三种创建方式:利用Callable接口、FutureTask类实现
- 创建任务对象
——>定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据
——>把Callable类型的对象封装成FutureTask(线程任务对象) - 把线程任务对象交给Thread对象
- 调用Thread对象的start方法启动线程
- 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果
FutureTask的API
线程创建方式三的优缺点
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果
- 缺点:编码复杂一点
代码演示:
MyCallable:
import java.util.concurrent.Callable;
//1、让这个类实现Callable接口
public class MyCallable implements Callable<String> {
int n;
public MyCallable(int n) {
this.n = n;
}
//2、重写call方法
@Override
public String call() throws Exception {
//描述现成的任务,返回线程执行结果
//求1-n的和
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return "子线程求出了1-" + n + "的和为:" + sum;
}
}
test:
package com.zeyu.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class threadTest3 {
public static void main(String[] args) throws Exception {
//3、创建一个Callable的对象
Callable<String> call = new MyCallable(100);
//4、把Callable的对象封装成一个FutureTask对象(任务对象)
//未来任务对象的作用:
//1、是一个任务对象,实现类Runnable对象
//2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕的结果
FutureTask<String> f1 = new FutureTask<>(call);
//5、把任务对象交给一个Thread对象
new Thread(f1).start();
//6、获取线程执行完毕后返回的结果
//注意:如果执行到这里上面的线程还没有执行完毕
//这里的代码会暂停,等上面的线程执行完毕后才会获取结果
System.out.println(f1.get());
}
}
运行结果:
Thread提供了很多与线程操作相关的方法
代码演示:
MyThread:
package com.zeyu.thread;
//1、让子类继承Thread线程类
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name){
super(name);
}
//2、必须重写Thread类的run方法
@Override
public void run() {
Thread t = Thread.currentThread(); //得到执行它的线程
// 描述线程的执行任务
for (int i = 0; i < 5; i++) {
System.out.println(t.getName() + "输出" + i);
}
}
}
test:
package com.zeyu.thread;
public class threadTest4 {
public static void main(String[] args) throws Exception{
Thread t1 = new MyThread("一号线程");
// t1.setName("一号线程");
t1.start();
t1.join(); //先执行完这个线程再执行别的
System.out.println(t1.getName());
Thread t2 = new MyThread("二号线程");
// t2.setName("二号线程");
t2.start();
System.out.println(t2.getName());
//主线程对象的名字
//那个线程执行它,它就会得到哪个线程对象
Thread m = Thread.currentThread();
m.setName("主线程");
System.out.println(m.getName());
for (int i = 0; i < 5; i++) {
System.out.println(m.getName() + "输出" + i);
}
Thread.sleep(5000); //暂停五秒
}
}
运行结果:
线程安全问题
- 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题
线程安全问题出现的原因?
- 存在多个线程在同时执行
- 同时访问一个共享资源
- 存在修改该共享资源
取钱案例
需求:
小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万
分析:
- 需要提供一个账户类,接着创建一个账户对象代表2个人的共享账户
- 需要定义一个线程类(用于创建两个线程,分别代表小明和小红)
- 创建2个线程,传入同一个账户对象给2个线程处理
- 启动2个线程,同时去同一个账户对象中取钱10万
代码:
账户类:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private double money; //余额
public static void test(){ //静态方法建议用类名.class作为锁
synchronized (Account.class) {
System.out.println();
}
}
//同步方法,内部隐含了一个锁(this)
public void drawMoney(double money){ //模拟取钱
String name = Thread.currentThread().getName();
if(this.money >= money){
System.out.println(name + "取钱" + money + "元成功!");
this.money -= money;
System.out.println(name + "取钱后,账户余额还剩" + this.money + "元");
}else{
System.out.println(name + "取钱失败,余额不足!");
}
}
public Account() {
}
public Account(double money) {
this.money = money;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
取钱线程类:
package com.zeyu.threadsafe;
public class DrawThread extends Thread{
private Account acc;
public DrawThread(Account acc, String name){
super(name);
this.acc = acc;
}
@Override
public void run() {
acc.drawMoney(100000);
}
}
test:
package com.zeyu.threadsafe;
public class threadSafeTest1 {
public static void main(String[] args) {
//创建一个账对象,代表俩个人的共同账户
Account account = new Account(100000);
//创建俩个线程代表小红和小明,再去同一个账户取钱10w
new DrawThread(account,"小红").start();
new DrawThread(account,"小明").start();
}
}
运行结果:
可以看到,此时就出现了线程安全问题,俩个人同时去银行取钱,且都判定余额足够,于是俩个人都取了10w出去,结果余额直接剩-10w,这显然是不行的,那么该如何解决这种线程安全问题?
线程同步
- 解决线程安全问题的方案
线程同步的思想
- 让多个线程实现先后依次访问共享资源,这样就解决了安全问题
方法一、同步代码块
- 作用:把访问共享资源的核心代码给上锁,以此保证线程安全
- 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
同步锁的注意事项
- 对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug
锁对象可以随便选择一个唯一的对象吗?
- 不能,会影响其他无关线程的执行
锁对象的使用规范
- 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象
代码演示:
同样是刚才的取钱案例,将核心代码块锁住,便可解决线程安全问题
synchronized (this) { //同步代码块
if(this.money >= money){
System.out.println(name + "取钱" + money + "元成功!");
this.money -= money;
System.out.println(name + "取钱后,账户余额还剩" + this.money + "元");
}else{
System.out.println(name + "取钱失败,余额不足!");
}
}
运行结果:
方法二、同步方法
- 作用:把访问共享资源的核心方法给上锁,以此保证线程安全
- 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
同步方法底层原理
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码
- 如果方法是实例方法:同步方法默认用this作为的锁对象
- 如果方法是静态方法:同步方法默认用类名.class作为的锁对象
代码演示:
直接将方法锁住:
//同步方法,内部隐含了一个锁(this)
public synchronized void drawMoney(double money){ //模拟取钱
String name = Thread.currentThread().getName();
if(this.money >= money){
System.out.println(name + "取钱" + money + "元成功!");
this.money -= money;
System.out.println(name + "取钱后,账户余额还剩" + this.money + "元");
}else{
System.out.println(name + "取钱失败,余额不足!");
}
}
运行结果:
同步代码块好还是同步方法好?
- 范围上:同步代码块锁的范围更小,同步方法锁的范围更大
- 可读性:同步方法更好
方法三、Lock锁
- LocK锁是IDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大
- Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象
Lock的常用方法
代码演示:
在类里面定义一个Lock锁对象,然后在核心代码块执行前用锁对象上锁,结束后再解锁,为了避免核心代码块出现错误导致无法解锁,可以用try-catch-finall 方式
定义Lock锁对象
private final Lock lk = new ReentrantLock(); //锁对象
上锁解锁:
try { //即使锁住的代码块出现了bug,也会解锁
lk.lock(); //上锁
if(this.money >= money){
System.out.println(name + "取钱" + money + "元成功!");
this.money -= money;
System.out.println(name + "取钱后,账户余额还剩" + this.money + "元");
}else{
System.out.println(name + "取钱失败,余额不足!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lk.unlock(); //解锁
}
运行结果:
线程通信
什么是线程通信?
- 当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。
线程通信的常见模型(生产者与消费者模型)
- 生产者线程负责生产数据
- 消费者线程负责消费生产者生产的数据,
- 注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产!
Object类的等待和唤醒方法:
注意:上述方法应该使用当前同步锁对象进行调用!
线程通信的模型
3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃
四种情况:
1、抢到桌子的是厨师,发现没有包子,做包子,做完之后唤醒他人,自己沉睡
2、抢到桌子的是厨师,发现有包子,唤醒他人,自己沉睡
3、抢到桌子的是吃货,发现有包子,吃包子,吃完之后唤醒他人,自己沉睡
4、抢到桌子的是吃货,发现没有包子,唤醒他人,自己盛水
所以最后必然会是生产者生产——>消费者消费的流程
代码演示:
Desk类:
import java.util.ArrayList;
public class Desk {
ArrayList<String> list = new ArrayList<>();
//因为有多个线程访问,所以肯定要给方法上锁,先保证线程安全,再进行线程通信
//因为使用的是同一个锁(this,也就是调用方法的对象,都是同一个),所以一把锁可以拦住所有厨师和吃货,桌子上只能有一个人
public synchronized void put() {
String name = Thread.currentThread().getName();
try { //判断是否有包子
if(list.size() == 0){
list.add(name + "做的肉包子");
System.out.println(name + "做了一个肉包子!");
//花俩秒做包子
Thread.sleep(2000);
//唤醒别人,等待自己(先唤醒,再等待!)
this.notify();
this.wait();
}else {
//有包子,不做了
//唤醒别人,等待自己
this.notify();
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized void get() {
String name = Thread.currentThread().getName();
try {
if(list.size() == 1){
//有包子,吃了
System.out.println(name + "吃了" + list.remove(0));
//吃包子花费一秒
Thread.sleep(1000);
//唤醒别人,等待自己
this.notify();
this.wait();
}else{
//没有包子
//唤醒别人,等待自己
this.notify();
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
test:
package com.zeyu.thread_communication;
public class thread_communicationTest1 {
public static void main(String[] args) {
Desk desk = new Desk();
new Thread(() -> {
while (true) {
desk.put();
}
},"厨师1").start();
new Thread(() -> {
while (true) {
desk.put();
}
},"厨师2").start();
new Thread(() -> {
while (true) {
desk.put();
}
},"厨师3").start();
new Thread(() -> {
while (true) {
desk.get();
}
},"吃货1").start();
new Thread(() -> {
while (true) {
desk.get();
}
},"吃货2").start();
}
}
运行结果:
线程池
什么是线程池?
- 线程池就是一个可以复用线程的技术
不使用线程池的问题
用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能
线程池的工作原理
复用线程,任务列表每有一个任务进来,工作线程就接手处理,处理完手上的任务再从任务列表找新任务
谁代表线程池?
- JDK5.0起提供了代表线程池的接口:ExecutorService
如何得到线程池对象?
- 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
- 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
ThreadPoolExecutor构造器
参数一:corePoolSize:指定线程池的核心线程的数量 正式工:3
参数二:maximumPoolSize:指定线程池的最大线程数量 最大员工数:5 决定 临时工:2
参数三:keepAliveTime:指定临时线程的存活时间 临时工空闲多久被开除
参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
参数五:workOueue:指定线程池的任务队列 客人排队的地方
参数六:threadFactory:指定线程池的线程工厂 负责招聘员工的(hr)
参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理(任务拒绝策略)
新任务拒绝策略:
ExecutorService的常用方法
代码演示:
使用线程池处理Runnable任务
import java.util.concurrent.*;
public class thread_poolTest1 {
public static void main(String[] args) {
//创建线程池
ExecutorService pool = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
Runnable target = new MyRunnable();
pool.execute(target); //自动创建新线程处理任务
pool.execute(target); //自动创建新线程处理任务
pool.execute(target); //自动创建新线程处理任务
pool.execute(target); //复用前面的核心线程
pool.execute(target); //复用前面的核心线程
pool.shutdown(); //等着线程池的任务全部结束之后再关闭线程池
// pool.shutdownNow(); //立即关闭线程池不管任务有没有全部结束
}
}
运行结果:
可以看到,线程一和线程三都处理了俩个任务,这就是线程的复用,而临时线程只有在核心线程都在工作且任务列表堆满的时候才会创建出来工作
使用线程池处理Callable任务
import java.util.concurrent.*;
public class thread_poolTest2 {
public static void main(String[] args) throws Exception {
//创建线程池对象
ExecutorService pool = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
//使用线程池处理Callable任务
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(200));
Future<String> f3 = pool.submit(new MyCallable(300));
Future<String> f4 = pool.submit(new MyCallable(400));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
pool.shutdown();
}
}
MyCallable:
import java.util.concurrent.Callable;
//1、让这个类实现Callable接口
public class MyCallable implements Callable<String> {
int n;
public MyCallable(int n) {
this.n = n;
}
//2、重写call方法
@Override
public String call() throws Exception {
//描述现成的任务,返回线程执行结果
//求1-n的和
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return Thread.currentThread().getName() + "线程求出了1-" + n + "的和为:" + sum;
}
}
运行结果:
这里也复用了线程2
核心线程数量到底配置为多少好?
- 计算密集型任务: 核心线程数量 = CPU的核数 + 1
- IO密集型任务,核心线程数 = CPU核数 * 2
cpu核数 = ctrl + alt + delete --> 任务管理器 --> 性能 --> cpu
还可以通过Executors创建线程池对象
例如:
//通过executors创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(3); //核心线程数
Executors是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象
注意 :这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象
Executors使用可能存在的陷阱
- 大型并发系统环境中使用Executors如果不注意可能会出现系统风险
练习
目标:有100份礼品小红,小明两人同时发送,当剩下的礼品小于10份的时候则不再送出,利用多线程模拟该过程并将线程的名称打印出来。并最后在控制台分别打印小红,小明各自送出多少分礼物
代码演示:
GIft类:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Gift {
private int gift; //礼物数
private final Lock lock = new ReentrantLock(); //Lock锁
private int xh = 0; //记录小红送出多少
private int xm = 0; //记录小明送出多少
public void giveGifts(){
Thread t = Thread.currentThread();
try {
lock.lock(); //上锁,避免线程安全问题
if(this.gift >= 10){
System.out.println(t.getName() + "送出了1份礼品,目前还剩" + -- this.gift + "份礼品...");
if(t.getName().equals("小红")) xh++;
else xm++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); //解锁
}
}
public Gift() {
}
public Gift(int gift) {
this.gift = gift;
}
public int getGift() {
return gift;
}
public void setGift(int gift) {
this.gift = gift;
}
public int getXh() {
return xh;
}
public void setXh(int xh) {
this.xh = xh;
}
public int getXm() {
return xm;
}
public void setXm(int xm) {
this.xm = xm;
}
}
MyThread类:
import java.util.concurrent.atomic.AtomicInteger;
public class MyThread extends Thread{
Gift gift = new Gift();
public MyThread(Gift gift, String name){
super(name);
this.gift = gift;
}
@Override
public void run() {
while (gift.getGift() >= 10) { //如果礼物数大于等于10就接着送
gift.giveGifts();
}
}
}
Test类:
public class giveGifts {
public static void main(String[] args) throws Exception {
// 目标:有100份礼品小红,小明两人同时发送,当剩下的礼品小于10份的时候则不再送出,
// 利用多线程模拟该过程并将线程的名称打印出来。并最后在控制台分别打印小红,小明各自送出多少分礼物。
Gift gift = new Gift(100); //礼物初始化
MyThread xh = new MyThread(gift, "小红"); //创建小红线程
MyThread xm = new MyThread(gift, "小明"); //创建小明线程
xh.start(); //小红开始送礼
xm.start(); //小明开始送礼
xh.join(); //小红和小明送完之后再总结送出多少
xm.join();
System.out.println("小红送出了" + gift.getXh() + "份");
System.out.println("小明送出了" + gift.getXm() + "份");
System.out.println("最终还剩" + gift.getGift() + "份礼品");
}
}
运行结果: