本文由 程序喵正在路上 原创,CSDN首发!
系列专栏:Java从入门到大牛
首发时间:2023年11月18日
欢迎关注点赞收藏留言
什么是线程?
线程(Thread)是一个程序内的一条执行流程,比如常见的 main 方法就是一个线程
public static void main(String[] args) {
// 代码...
for(int i = 0; i <10; i++) {
System.out.println(i);
}
//...
}
程序中如果只有一条执行流程,那么这个程序就是单线程的程序
什么是多线程?
多线程是指从软硬件上实现的多条执行流程的技术(多条线程由 CPU 负责调度执行)
多线程的用处
在生活中,有很多多线程应用的例子,比如,12306网站同时有多位用户在抢票,那么网站就需要提供多条线程来为多位用户服务;在使用百度网盘时,用户可能同时需要上传和下载资源,这也需要用到多线程技术;再比如消息通信、淘宝、京东系统都离不开多线程技术
如果在程序中创建出多条线程呢?
Java 是通过 java.lang.Thread 类的对象来代表线程的
多线程的创建方式一:继承Thread类
代码实现
MyThread类
/**
* 1. 让子类继承Thread线程类
*/
public class MyThread extends Thread {
@Override // 2. 必须重写Thread类的run方法
public void run() {
// 这里用来描述线程要执行的任务
for (int i = 1; i <= 5; i++) {
System.out.println("子线程MyThread输出:" + i);
}
}
}
测试类
/**
* 目标:掌握线程的创建方式一:继承Thread类
*/
public class ThreadTest1 {
// main方法是由一条默认的主线程负责执行的
public static void main(String[] args) {
// 3. 创建MyThread线程类的对象代表一个线程
Thread t = new MyThread(); // 多态写法
// 4. 启动线程,必须是调用start方法
t.start(); // 启动后,现在就存在两个线程:main线程和t线程
for (int i = 1; i <= 5; i++) {
System.out.println("主线程main输出:" + i);
}
}
}
因为有两个线程,线程执行的顺序也不确定,所有每次执行结果都可能不同
方法一优缺点:
多线程的注意事项
1、启动线程必须是调用 start 方法,不能调用 run 方法
2、不要把主线程任务放在启动子线程之前
多线程的创建方式二:实现Runnable接口
定义一个线程任务类 MyRunnable 实现 Runnable 接口,并重写 run() 方法
创建 MyRunnable 任务对象
调用线程对象的 start() 方法启动线程
代码实现
MyRunnable类
/**
* 1.定义一个任务类,实现Runnable接口
*/
public class MyRunnable implements Runnable {
// 2.重写Runnable的run方法
@Override
public void run() {
// 描述线程要执行的任务
for (int i = 1; i <= 5; i++) {
System.out.println("子线程输出:" + i);
}
}
}
测试类
/**
* 目标:掌握多线程的创建方式二:实现Runnable接口
*/
public class ThreadTest2 {
public static void main(String[] args) {
// 3.创建任务对象
Runnable target = new MyRunnable();
// 4.把任务对象交给一个线程对象处理
new Thread(target).start();
for (int i = 1; i <= 5; i++) {
System.out.println("主线程main输出:" + i);
}
}
}
方式二的优缺点
线程创建方式二的匿名内部类写法
匿名内部类写法实现
/**
* 目标:掌握多线程创建方式二的匿名内部类写法
*/
public class ThreadTest2_2 {
public static void main(String[] args) {
// 1、直接创建Runnable接口的匿名内部类形式(任务对象)
Runnable target = new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程1输出:" + i);
}
}
};
new Thread(target).start();
// 简化形式1:
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程2输出:" + i);
}
}
}).start();
// 简化形式2:
new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程3输出:" + i);
}
}).start();
for (int i = 1; i <= 5; i++) {
System.out.println("主线程main输出:" + i);
}
}
}
前两种线程创建方式都存在一个问题:假如线程执行完毕后有一些数据需要返回,他们重写的 run 方法均不能返回结果
怎么解决这个问题?
多线程的第三种创建方式:利用Callable接口和FutureTask类来实现
1、创建任务对象
2、把线程任务对象交给 Thread 对象
3、调用 Thread 对象的 start 方法启动线程
4、线程执行完毕后,通过 FutureTask 对象的 get 方法去获取线程任务执行的结果
FutureTask的API
MyCallable类
import java.util.concurrent.Callable;
/**
* 1、让这个类实现Callable接口
*/
public class MyCallable implements Callable<String> {
private 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;
}
}
测试类
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* 目标:掌握线程的创建方式三:实现Callable接口
*/
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();
Callable<String> call2 = new MyCallable(200);
FutureTask<String> f2 = new FutureTask<>(call2);
new Thread(f2).start();
// 6、获取线程执行完毕后返回的结果
// 注意:如果执行到这儿,假如上面的线程还没有执行完毕
// 这里的代码会暂停,等待上面线程执行完毕后才会获取结果
String rs = f1.get();
System.out.println(rs);
String rs2 = f2.get();
System.out.println(rs2);
}
}
线程创建方式三的优缺点
应用演示
MyThread类
public class MyThread extends Thread {
public MyThread(String name) {
super(name); // 为当前线程设置名字
}
@Override
public void run() {
// 获取当前线程对象
Thread t = Thread.currentThread();
for (int i = 1; i <= 3; i++) {
System.out.println(t.getName() + "输出:" + i);
}
}
}
测试类1
/**
* 目标:掌握Thread的常用方法
*/
public class ThreadTest1 {
public static void main(String[] args) {
Thread t1 = new MyThread("1号线程");
t1.start();
System.out.println(t1.getName()); // Thread-0
Thread t2 = new MyThread("2号线程");
t2.start();
System.out.println(t2.getName()); // Thread-1
//获取主线程对象的名字
Thread m = Thread.currentThread();
m.setName("main主线程");
System.out.println(m.getName());
for (int i = 1; i <= 5; i++) {
System.out.println(m.getName() + "输出:" + i);
}
}
}
测试类2
/**
* 目标:掌握sleep方法,join方法的作用
*/
public class ThreadTest2 {
public static void main(String[] args) throws Exception {
for (int i = 1; i <= 5; i++) {
System.out.println(i);
// 休眠5s
if(i == 3){
// 让当前执行的线程暂停5秒,再继续执行
Thread.sleep(5000);
}
}
// join方法作用:让当前调用这个方法的线程先执行完再执行其他线程
Thread t1 = new MyThread("1号线程");
t1.start();
t1.join();
Thread t2 = new MyThread("2号线程");
t2.start();
t2.join();
Thread t3 = new MyThread("3号线程");
t3.start();
t3.join();
}
}
Thread其他方法的说明
Thread 类还提供了诸如:yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用,在这里就不一一列出了
什么是线程安全问题?
多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题
取钱的线程安全问题
场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?
有可能会出现2人都成功取钱10万元的结果
需求
小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万元
分析
代码实现
账户类
public class Account {
private String cardId; // 卡号
private double money; // 余额。
public Account() {
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
// 取钱
public void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
// 1、判断余额是否足够
if(this.money >= money){
System.out.println(name + "来取钱" + money + "成功!");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else {
System.out.println(name + "来取钱:余额不足~");
}
}
// get、set方法
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
线程类
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); // 取钱
}
}
测试类
/**
* 目标:模拟线程安全问题
*/
public class ThreadTest {
public static void main(String[] args) {
// 1、创建一个账户对象,代表两个人的共享账户
Account acc = new Account("ICBC-110", 100000);
// 2、创建两个线程,分别代表小明 小红,再去同一个账户对象中取钱10万
new DrawThread(acc, "小明").start(); // 小明
new DrawThread(acc, "小红").start(); // 小红
}
}
线程同步
解决线程安全问题的方案
线程同步的思想
让多个线程实现先后依次访问共享资源,这样就解决了安全问题
线程同步的常见方案
加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来
同步代码块
作用:把访问共享资源的核心代码给上锁,以此保证线程安全
synchronized(同步锁) {
访问共享资源的核心代码
}
原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
同步锁的注意事项
对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug
同步代码块的实现
IDEA里加同步锁的快捷方式:鼠标选中要上锁的核心代码,按住 Ctrl+Alt+T,然后选择第9项 synchronized 即可
依旧是前面那个取钱的例子,我们只需要将账户类稍微修改一下即可,代码如下
public class Account {
private String cardId; // 卡号
private double money; // 余额
// ...
// 取钱
public void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
// 1、判断余额是否足够
// this正好代表共享资源!
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 + "来取钱:余额不足~");
}
}
}
// 如果是静态方法,建议使用 类名.class 作为锁
public static void test(){
synchronized (Account.class){
}
}
// ...
}
问题:锁对象随便选择一个唯一的对象好不好呢?
不好,可能会影响其他无关线程的执行
锁对象的使用规范
同步方法
作用:把访问共享资源的核心方法给上锁,以此保证线程安全
修饰符 synchronized 返回值类型 方法名称(形参列表) {
操作共享资源的代码
}
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
同步方法底层原理
同步方法代码实现
账户类修改如下
public class Account {
private String cardId; // 卡号
private double money; // 余额
// ...
// 同步方法
public synchronized void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
// 1、判断余额是否足够
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锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大
Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象
Lock锁代码实现
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private String cardId; // 卡号
private double money; // 余额
// 创建了一个锁对象
private final Lock lk = new ReentrantLock();
// ...
// 小明 小红线程同时过来的
public void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
// 为了防止try里面的代码块出现异常而导致无法解锁,
// 我们需要对其进行异常处理,并将解锁操作放到finally里面
try {
lk.lock(); // 加锁
// 1、判断余额是否足够
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(); // 解锁
}
}
// ...
}
什么是线程通信?
当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺
线程通信的常见模型(生产者与消费者模型)
注意:上述方法应该使用当前同步锁对象进行调用
为了更好地理解生产者与消费者模型,我们来看一个例子:
代码实现
桌子类
import java.util.ArrayList;
import java.util.List;
public class Desk {
private List<String> list = new ArrayList<>();
// 放1个包子的方法
// 厨师1 厨师2 厨师3
public synchronized void put() {
try {
String name = Thread.currentThread().getName();
// 判断是否有包子
if(list.size() == 0){
list.add(name + "做的肉包子"); // 没有包子,生产包子
System.out.println(name + "做了一个肉包子~~");
Thread.sleep(2000);
// 先唤醒别人, 再等待自己,顺序不能错
this.notifyAll();
this.wait();
}else {
// 有包子了,不做了。
// 唤醒别人, 等待自己
this.notifyAll();
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 吃货1 吃货2
public synchronized void get() {
try {
String name = Thread.currentThread().getName();
if(list.size() == 1){
// 有包子,吃了
System.out.println(name + "吃了:" + list.get(0));
list.clear();
Thread.sleep(1000);
this.notifyAll();
this.wait();
}else {
// 没有包子
this.notifyAll();
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试类
/**
* 目标:了解一下线程通信
*/
public class ThreadTest {
public static void main(String[] args) {
// 需求:3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上
// 2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃
Desk desk = new Desk();
// 创建3个生产者线程(3个厨师)
new Thread(() -> {
while (true) {
desk.put();
}
}, "厨师1").start();
new Thread(() -> {
while (true) {
desk.put();
}
}, "厨师2").start();
new Thread(() -> {
while (true) {
desk.put();
}
}, "厨师3").start();
// 创建2个消费者线程(2个吃货)
new Thread(() -> {
while (true) {
desk.get();
}
}, "吃货1").start();
new Thread(() -> {
while (true) {
desk.get();
}
}, "吃货2").start();
}
}
什么是线程池?
线程池就是一个可以复用线程的技术
不使用线程池的问题
用户每发起一个请求,系统后台就需要创建一个新线程来处理,这样下次有新任务来了又要创建新线程来处理,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能
Java中谁代表线程池?
JDK5.0 起提供了代表线程池的接口:ExecutorService
如何得到线程池对象?
ThreadPoolExecutor的构造器
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler)
创建一个线程池
public class ThreadPoolTest1 {
public static void main(String[] args) {
// 1、通过ThreadPoolExecutor创建一个线程池对象
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
}
}
线程池的注意事项
1、临时线程什么时候创建?
2、什么时候会开始拒绝新任务?
新任务拒绝策略
代码实现
MyRunnable类
public class MyRunnable implements Runnable{
@Override
public void run() {
// 描述任务
System.out.println(Thread.currentThread().getName() + " ==> 输出666~~");
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试类
import java.util.concurrent.*;
public class ThreadPoolTest1 {
public static void main(String[] args) {
// 1、通过ThreadPoolExecutor创建一个线程池对象
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
Runnable target = new MyRunnable();
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target); // 复用前面的核心线程
pool.execute(target);
pool.execute(target);
pool.execute(target); // 任务队列已满
// 到了临时线程的创建时机了
pool.execute(target);
pool.execute(target);
// 到了新任务的拒绝时机了!
pool.execute(target);
// pool.shutdown(); // 等着线程池的任务全部执行完毕后,再关闭线程池
// pool.shutdownNow(); // 立即关闭线程池!不管任务是否执行完毕!
}
}
代码实现
MyCallable类
import java.util.concurrent.Callable;
/**
* 1、让这个类实现Callable接口
*/
public class MyCallable implements Callable<String> {
private 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;
}
}
测试类
import java.util.concurrent.*;
public class ThreadPoolTest2 {
public static void main(String[] args) throws Exception {
// 1、通过ThreadPoolExecutor创建一个线程池对象
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
// 2、使用线程处理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());
}
}
什么是Executors
Executors 是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象
注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象
Executors使用可能存在的陷阱
大型并发系统环境中,使用Executors如果不注意可能会出现系统风险。
进程
并发的含义
进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
并行的理解
在同一个时刻上,同时有多个线程在被CPU调度执行。
什么是线程的生命周期
Java线程的状态
线程的6种状态互相转换
线程的6种状态总结
前面我们学到的,都是悲观锁,就是一上来就加锁,没有安全感,每次只能一个线程进入访问完毕后,再解锁,线程安全,但是性能较差
什么是乐观锁
一开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全问题的时候才开始控制,线程安全,并且性能较好
例子
有1个变量,100个线程,每个线程都要对其加100次
不加锁
MyRunnable.java
public class MyRunnable implements Runnable{
private int cnt; // 记录浏览人数
@Override
public void run() {
// 加100次
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " cnt --------> " + (++cnt));
}
}
}
Test1.java
public class Test1 {
public static void main(String[] args) {
// 需求:1个变量,100个线程,每个线程对其加100次
Runnable target = new MyRunnable();
for (int i = 0; i < 100; i++) {
new Thread(target).start();
}
}
}
加的顺序不一定是一个一个地加,还有可能出现加不到10000的情况,因为有可能在同一时刻两个线程同时抢到了执行权,对同一个数字进行加1,结果相同,但是少了一次加法
加悲观锁
MyRunnable.java
public class MyRunnable implements Runnable{
private int cnt; // 记录浏览人数
@Override
public void run() {
// 加100次
for (int i = 0; i < 100; i++) {
synchronized(this) {
System.out.println(Thread.currentThread().getName() + " cnt --------> " + (++cnt));
}
}
}
}
没有问题
加乐观锁
加乐观锁后,当有两个线程同时抢到执行权时,一个线程在对变量修改完成后,要赋值回去的时候会进行判断,若变量已经被修改了,则这一轮修改作废,重新进行一轮修改
MyRunnable2.java
public class MyRunnable2 implements Runnable{
// 整数修改的乐观锁:用原子类实现
private AtomicInteger cnt = new AtomicInteger();
@Override
public void run() {
// 加100次
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " cnt --------> " + cnt.incrementAndGet());
}
}
}
Test2.java
public class Test2 {
public static void main(String[] args) {
// 需求:1个变量,100个线程,每个线程对其加100次
Runnable target = new MyRunnable2();
for (int i = 0; i < 100; i++) {
new Thread(target).start();
}
}
}
需求描述
有100份礼品,小红、小明两人同时发送,当剩下的礼品数小于10份的时候则不再送出,利用多线程模拟该过程并将线程的名称打印出来,并最后在控制台分别打印小红、小明分别送出了多少份礼物
代码
SendThread.java
import java.util.List;
import java.util.Random;
// 发送礼品
public class SendThread extends Thread {
private List<String> gift;
private int cnt; // 计数
public SendThread(List<String> gift, String name) {
super(name);
this.gift = gift;
}
@Override
public void run() {
// 发礼品
// 注意:锁必须唯一
Random r = new Random();
String name = Thread.currentThread().getName();
while (true) {
synchronized(gift) {
if (gift.size() < 10) {
break;
}
String rs = gift.remove(r.nextInt(gift.size()));
System.out.println(name + "发出了:" + rs);
cnt++;
}
}
}
public int getCnt() {
return cnt;
}
}
Demo.java
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Demo1 {
public static void main(String[] args) throws Exception {
// 创建100份礼品
List<String> gift = new ArrayList<>();
String[] names = {"口红", "鲜花", "包包", "剃须刀", "皮带", "手表"};
Random r = new Random(); // 随机分配礼品名称
for (int i = 0; i < 100; i++) {
gift.add(names[r.nextInt(names.length)] + (i+1));
}
System.out.println(gift);
// 定义线程类,创建线程对象,去集合中拿礼品给别人
SendThread xh = new SendThread(gift, "小红");
xh.start();
SendThread xm = new SendThread(gift, "小明");
xm.start();
xh.join();
xm.join();
System.out.println("小红送出了" + xh.getCnt() + "份礼物");
System.out.println("小明送出了" + xm.getCnt() + "份礼物");
}
}