java多线程

来源:互联网 发布:淘宝店发展战略 编辑:程序博客网 时间:2024/05/16 08:41
多线程概述
多线程引入
把备注部分的代码通过画图解释一下调用流程。这个程序只有一个执行流程,所以这样的程序就是单线程程序。


假如一个程序有多条执行流程,那么,该程序就是多线程程序。
接下来我们来看看到底什么是多线程




多线程的和进程的介绍
1:要想了解多线程,必须先了解线程,而要想了解线程,必须先了解进程,因为线程是依赖于进程而存在。


2:什么是进程?
通过任务管理器我们就看到了进程的存在。
而通过观察,我们发现只有运行的程序才会出现进程。
进程:就是正在运行的程序。
进程是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。

3:多进程有什么意义呢?
单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。
举例:一边玩游戏(游戏进程),一边听音乐(音乐进程)。
也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。
并且呢,可以提高CPU的使用率。

问题:
一边玩游戏,一边听音乐是同时进行的吗?
不是。因为单CPU在某一个时间点上只能做一件事情。
而我们在玩游戏,或者听音乐的时候,是CPU在做着程序间的高效切换让我们觉得是同时进行的。

4:什么是线程呢?
在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程。
线程:是程序的执行单元,执行路径。是程序使用CPU的最基本单位。
单线程:如果程序只有一条执行路径。
多线程:如果程序有多条执行路径。

5:多线程有什么意义呢?
多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。
程序的执行其实都是在抢CPU的资源,CPU的执行权。
多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。
我们是不敢保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。




Java程序运行原理
java 命令会启动 java 虚拟机,启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。在此之前的所有程序都是单线程的。
思考:
jvm虚拟机的启动是单线程的还是多线程的?


1:JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。

/*
 *进程:
 *正在运行的程序,是系统进行资源分配和调用的独立单位。
 *每一个进程都有它自己的内存空间和系统资源。
 *线程:
 *是进程中的单个顺序控制流,是一条执行路径
 *一个进程如果只有一条执行路径,则称为单线程程序。
 *一个进程如果有多条执行路径,则称为多线程程序。
 *
 *  举例:
 *  扫雷程序,迅雷下载
 *  
 *  大家注意两个词汇的区别:并行和并发。
 *前者是逻辑上同时发生,指在某一个时间内同时运行多个程序。
 *后者是物理上同时发生,指在某一个时间点同时运行多个程序。
         
 *
 * Java程序的运行原理:
 * 由java命令启动JVM,JVM启动就相当于启动了一个进程。
 * 接着有该进程创建了一个主线程去调用main方法。
 * 
 * 思考题:
 * jvm虚拟机的启动是单线程的还是多线程的?
 * 多线程的。
 * 原因是垃圾回收线程也要先启动,否则很容易会出现内存溢出。
 * 现在的垃圾回收线程加上前面的主线程,最低启动了两个线程,所以,jvm的启动其实是多线程的。
 */
public class MyThreadDemo {
public static void main(String[] args) {
System.out.println("hello");
new Object();
new Object();
new Object();
new Object();
//...
System.out.println("world");
}
}

多线程的实现方案1
通过查看API来学习多线程程序的实现
 参考Thread类
继承Thread类
步骤及代码演示
  几个小问题:
    为什么要重写run()方法
    启动线程使用的是那个方法
    线程能不能多次启动
run()和start()方法的区别


/*
 * 需求:我们要实现多线程的程序。
 * 如何实现呢?
 * 由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
 * 而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。
 * Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。
 * 但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。
 * 由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,
 * 然后提供一些类供我们使用。我们就可以实现多线程程序了。
 * 那么Java提供的类是什么呢?
 * Thread
 * 通过查看API,我们知道了有2中方式实现多线程程序。
 * 
 * 方式1:继承Thread类。
 * 步骤
 * A:自定义类MyThread继承Thread类。
 * B:MyThread类里面重写run()?
 * 为什么是run()方法呢?
 * C:创建对象
 * D:启动线程
 */
public class MyThreadDemo {
public static void main(String[] args) {
// 创建线程对象
// MyThread my = new MyThread();
// // 启动线程
// my.run();
// my.run();
// 调用run()方法为什么是单线程的呢?
// 因为run()方法直接调用其实就相当于普通的方法调用,所以你看到的是单线程的效果
// 要想看到多线程的效果,就必须说说另一个方法:start()
// 面试题:run()和start()的区别?
// run():仅仅是封装被线程执行的代码,直接调用是普通方法
// start():首先启动了线程,然后再由jvm去调用该线程的run()方法。
// MyThread my = new MyThread();
// my.start();
// // IllegalThreadStateException:非法的线程状态异常
// // 为什么呢?因为这个相当于是my线程被调用了两次。而不是两个线程启动。
// my.start();


// 创建两个线程对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();


my1.start();
my2.start();
}
}


/*
 * 该类要重写run()方法,为什么呢?
 * 不是类中的所有代码都需要被线程执行的。
 * 而这个时候,为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。
 */
public class MyThread extends Thread {


@Override
public void run() {
// 自己写代码
// System.out.println("好好学习,天天向上");
// 一般来说,被线程执行的代码肯定是比较耗时的。所以我们用循环改进
for (int x = 0; x < 200; x++) {
System.out.println(x);
}
}


}




如何获取和设置线程名称
Thread类的基本获取和设置方法
  public final String getName()
  public final void setName(String name)
其实通过构造方法也可以给线程起名字
思考:
如何获取main方法所在的线程名称呢?
public static Thread currentThread()
这样就可以获取任意方法所在的线程名称


public class MyThread extends Thread {


public MyThread() {
}

public MyThread(String name){
super(name);
}


@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}




/*
 * 如何获取线程对象的名称呢?
 * public final String getName():获取线程的名称。
 * 如何设置线程对象的名称呢?
 * public final void setName(String name):设置线程的名称
 * 
 * 针对不是Thread类的子类中如何获取线程对象名称呢?
 * public static Thread currentThread():返回当前正在执行的线程对象
 * Thread.currentThread().getName()
 */
public class MyThreadDemo {
public static void main(String[] args) {
// 创建线程对象
//无参构造+setXxx()
// MyThread my1 = new MyThread();
// MyThread my2 = new MyThread();
// //调用方法设置名称
// my1.setName("林青霞");
// my2.setName("张三");
// my1.start();
// my2.start();

//带参构造方法给线程起名字
// MyThread my1 = new MyThread("林青霞");
// MyThread my2 = new MyThread("张三");
// my1.start();
// my2.start();

//我要获取main方法所在的线程对象的名称,该怎么办呢?
//遇到这种情况,Thread类提供了一个很好玩的方法:
//public static Thread currentThread():返回当前正在执行的线程对象
System.out.println(Thread.currentThread().getName());
}
}


/*
名称为什么是:Thread-? 编号


class Thread {
private char name[];


public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null);
    }
    
     private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
        //大部分代码被省略了
        this.name = name.toCharArray();
    }
    
    public final void setName(String name) {
        this.name = name.toCharArray();
    }
    
    
    private static int threadInitNumber; //0,1,2
    private static synchronized int nextThreadNum() {
        return threadInitNumber++; //return 0,1
    }
    
    public final String getName() {
        return String.valueOf(name);
    }
}


class MyThread extends Thread {
public MyThread() {
super();
}
}


*/


线程调度


假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。那么Java是如何对线程进行调用的呢?
线程有两种调度模型:
分时调度模型   所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型   优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。 
Java使用的是抢占式调度模型。
演示如何设置和获取线程优先级
public final int getPriority()
public final void setPriority(int newPriority)


public class ThreadPriority extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}
/*
 * 我们的线程没有设置优先级,肯定有默认优先级。
 * 那么,默认优先级是多少呢?
 * 如何获取线程对象的优先级?
 * public final int getPriority():返回线程对象的优先级
 * 如何设置线程对象的优先级呢?
 * public final void setPriority(int newPriority):更改线程的优先级。 
 *  
 * 注意:
 * 线程默认优先级是5。
 * 线程优先级的范围是:1-10。
 * 线程优先级高仅仅表示线程获取的 CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。
 * 
 * IllegalArgumentException:非法参数异常。
 * 抛出的异常表明向方法传递了一个不合法或不正确的参数。 
 * 
 */
public class ThreadPriorityDemo {
public static void main(String[] args) {
ThreadPriority tp1 = new ThreadPriority();
ThreadPriority tp2 = new ThreadPriority();
ThreadPriority tp3 = new ThreadPriority();


tp1.setName("东方不败");
tp2.setName("岳不群");
tp3.setName("林平之");


// 获取默认优先级
// System.out.println(tp1.getPriority());
// System.out.println(tp2.getPriority());
// System.out.println(tp3.getPriority());


// 设置线程优先级
// tp1.setPriority(100000);

//设置正确的线程优先级
tp1.setPriority(10);
tp2.setPriority(1);


tp1.start();
tp2.start();
tp3.start();
}
}


线程控制
我们已经知道了线程的调度,接下来我们就可以使用如下方法对象线程进行控制
线程休眠
public static void sleep(long millis)
线程加入
public final void join()
线程礼让
public static void yield()
后台线程
public final void setDaemon(boolean on)
中断线程
public final void stop()
public void interrupt()




线程休眠


import java.util.Date;


public class ThreadSleep extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x + ",日期:" + new Date());
// 睡眠
// 困了,我稍微休息1秒钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}/*
 * 线程休眠
 *public static void sleep(long millis)
 */
public class ThreadSleepDemo {
public static void main(String[] args) {
ThreadSleep ts1 = new ThreadSleep();
ThreadSleep ts2 = new ThreadSleep();
ThreadSleep ts3 = new ThreadSleep();


ts1.setName("林青霞");
ts2.setName("林志玲");
ts3.setName("林志颖");


ts1.start();
ts2.start();
ts3.start();
}
}




线程加入
public class ThreadJoin extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}


/*
 * public final void join():等待该线程终止。 
 */
public class ThreadJoinDemo {
public static void main(String[] args) {
ThreadJoin tj1 = new ThreadJoin();
ThreadJoin tj2 = new ThreadJoin();
ThreadJoin tj3 = new ThreadJoin();


tj1.setName("李渊");
tj2.setName("李世民");
tj3.setName("李元霸");


tj1.start();
try {
tj1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

tj2.start();
tj3.start();
}
}




线程礼让
public static void yield()
public class ThreadYield extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
Thread.yield();
}
}
}


/*
 * public static void yield():暂停当前正在执行的线程对象,并执行其他线程。 
 * 让多个线程的执行更和谐,但是不能靠它保证一人一次。
 */
public class ThreadYieldDemo {
public static void main(String[] args) {
ThreadYield ty1 = new ThreadYield();
ThreadYield ty2 = new ThreadYield();


ty1.setName("林青霞");
ty2.setName("张三");


ty1.start();
ty2.start();
}
}
后台线程
public final void setDaemon(boolean on)


public class ThreadDaemon extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}




/*
 * public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。
 * 当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。 
 * 
 * 游戏:坦克大战。
 */
public class ThreadDaemonDemo {
public static void main(String[] args) {
ThreadDaemon td1 = new ThreadDaemon();
ThreadDaemon td2 = new ThreadDaemon();


td1.setName("关羽");
td2.setName("张飞");


// 设置收获线程
td1.setDaemon(true);
td2.setDaemon(true);


td1.start();
td2.start();


Thread.currentThread().setName("刘备");
for (int x = 0; x < 5; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}




中断线程


public class ThreadStop extends Thread {
@Override
public void run() {
System.out.println("开始执行:" + new Date());


// 我要休息10秒钟,亲,不要打扰我哦
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("线程被终止了");
}


System.out.println("结束执行:" + new Date());
}
}


/*
 * public final void stop():让线程停止,过时了,但是还可以使用。
 * public void interrupt():中断线程。 把线程的状态终止,并抛出一个InterruptedException。
 */
public class ThreadStopDemo {
public static void main(String[] args) {
ThreadStop ts = new ThreadStop();
ts.start();


// 你超过三秒不醒过来,我就干死你
try {
Thread.sleep(3000);
// ts.stop();
ts.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


线程的生命周期图
 








多线程的实现方案2
实现Runnable接口
    如何获取线程名称
    如何给线程设置名称
实现接口方式的好处
  可以避免由于Java单继承带来的局限性。
  适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,  较好的体现了面向对象的设计思想。


public class MyRunnable implements Runnable {


@Override
public void run() {
for (int x = 0; x < 100; x++) {
// 由于实现接口的方式就不能直接使用Thread类的方法了,但是可以间接的使用
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}


}


/*
 * 方式2:实现Runnable接口
 * 步骤:
 * A:自定义类MyRunnable实现Runnable接口
 * B:重写run()方法
 * C:创建MyRunnable类的对象
 * D:创建Thread类的对象,并把C步骤的对象作为构造参数传递
 */
public class MyRunnableDemo {
public static void main(String[] args) {
// 创建MyRunnable类的对象
MyRunnable my = new MyRunnable();


// 创建Thread类的对象,并把C步骤的对象作为构造参数传递
// Thread(Runnable target)
// Thread t1 = new Thread(my);
// Thread t2 = new Thread(my);
// t1.setName("林青霞");
// t2.setName("张三");


// Thread(Runnable target, String name)
Thread t1 = new Thread(my, "林青霞");
Thread t2 = new Thread(my, "张三");


t1.start();
t2.start();
}
}


 


案例
需求:
某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
两种方式实现
继承Thread类
实现Runnable接口


public class SellTicket extends Thread {


// 定义100张票
// private int tickets = 100;
// 为了让多个线程对象共享这100张票,我们其实应该用静态修饰
private static int tickets = 100;


@Override
public void run() {
// 定义100张票
// 每个线程进来都会走这里,这样的话,每个线程对象相当于买的是自己的那100张票,这不合理,所以应该定义到外面
// int tickets = 100;


// 是为了模拟一直有票
while (true) {
if (tickets > 0) {
System.out.println(getName() + "正在出售第" + (tickets--) + "张票");
}
}
}
}


/*
 * 某电影院目前正在上映贺岁大片(红高粱,少林寺传奇藏经阁),共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
 * 继承Thread类来实现。
 */
public class SellTicketDemo {
public static void main(String[] args) {
// 创建三个线程对象
SellTicket st1 = new SellTicket();
SellTicket st2 = new SellTicket();
SellTicket st3 = new SellTicket();


// 给线程对象起名字
st1.setName("窗口1");
st2.setName("窗口2");
st3.setName("窗口3");


// 启动线程
st1.start();
st2.start();
st3.start();
}
}


第二种方案
public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;


@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
}
}
}
}


/*
 * 实现Runnable接口的方式实现
 */
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();


// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");


// 启动线程
t1.start();
t2.start();
t3.start();
}
}


关于电影院卖票程序的思考
我们前面讲解过电影院售票程序,从表面上看不出什么问题,但是在真实生活中,售票时网络是不能实时传输的,总是存在延迟的情况,所以,在出售一张票以后,需要一点时间的延迟
改实现接口方式的卖票程序
  每次卖票延迟100毫秒


public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;


//@Override
//public void run() {
//while (true) {
//// t1,t2,t3三个线程
//// 这一次的tickets = 100;
//if (tickets > 0) {
//// 为了模拟更真实的场景,我们稍作休息
//try {
//Thread.sleep(100); // t1就稍作休息,t2就稍作休息
//} catch (InterruptedException e) {
//e.printStackTrace();
//}
//
//System.out.println(Thread.currentThread().getName() + "正在出售第"
//+ (tickets--) + "张票");
//// 理想状态:
//// 窗口1正在出售第100张票
//// 窗口2正在出售第99张票
//// 但是呢?
//// CPU的每一次执行必须是一个原子性(最简单基本的)的操作。
//// 先记录以前的值
//// 接着把ticket--
//// 然后输出以前的值(t2来了)
//// ticket的值就变成了99
//// 窗口1正在出售第100张票
//// 窗口2正在出售第100张票
//
//}
//}
//}

@Override
public void run() {
while (true) {
// t1,t2,t3三个线程
// 这一次的tickets = 1;
if (tickets > 0) {
// 为了模拟更真实的场景,我们稍作休息
try {
Thread.sleep(100); //t1进来了并休息,t2进来了并休息,t3进来了并休息,
} catch (InterruptedException e) {
e.printStackTrace();
}


System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
//窗口1正在出售第1张票,tickets=0
//窗口2正在出售第0张票,tickets=-1
//窗口3正在出售第-1张票,tickets=-2
}
}
}
}


/*
 * 实现Runnable接口的方式实现
 * 
 * 通过加入延迟后,就产生了连个问题:
 * A:相同的票卖了多次
 * CPU的一次操作必须是原子性的
 * B:出现了负数票
 * 随机性和延迟导致的
 */
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();


// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");


// 启动线程
t1.start();
t2.start();
t3.start();
}
}


改进后的电影院售票出现问题
问题
  相同的票出现多次
  CPU的一次操作必须是原子性的
还出现了负数的票
  随机性和延迟导致的


注意
线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。






解决线程安全问题的基本思想
首先想为什么出现问题?(也是我们判断是否有问题的标准)
  是否是多线程环境
  是否有共享数据
  是否有多条语句操作共享数据
如何解决多线程安全问题呢?
基本思想:让程序没有安全问题的环境。
怎么实现呢?
把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。






解决线程安全问题实现1
同步代码块
格式:
synchronized(对象){需要同步的代码;}
同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。
同步代码块的对象可以是哪些呢?


public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
//创建锁对象
private Object obj = new Object();


//@Override
//public void run() {
//while (true) {
//synchronized(new Object()){
//if (tickets > 0) {
//try {
//Thread.sleep(100); 
//} catch (InterruptedException e) {
//e.printStackTrace();
//}
//System.out.println(Thread.currentThread().getName() + "正在出售第"
//+ (tickets--) + "张票");
//}
//}
//}
//}

@Override
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
}
}
}


/*
 * 如何解决线程安全问题呢?
 * 
 * 要想解决问题,就要知道哪些原因会导致出问题:(而且这些原因也是以后我们判断一个程序是否会有线程安全问题的标准)
 * A:是否是多线程环境
 * B:是否有共享数据
 * C:是否有多条语句操作共享数据
 * 
 * 我们来回想一下我们的程序有没有上面的问题呢?
 * A:是否是多线程环境
 * B:是否有共享数据
 * C:是否有多条语句操作共享数据
 * 
 * 由此可见我们的程序出现问题是正常的,因为它满足出问题的条件。
 * 接下来才是我们要想想如何解决问题呢?
 * A和B的问题我们改变不了,我们只能想办法去把C改变一下。
 * 思想:
 * 把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行。
 * 问题是我们不知道怎么包啊?其实我也不知道,但是Java给我们提供了:同步机制。
 * 
 * 同步代码块:
 * synchronized(对象){
 * 需要同步的代码;
 * }
 * 
 * A:对象是什么呢?
 * 我们可以随便创建一个对象试试。
 * B:需要同步的代码是哪些呢?
 * 把多条语句操作共享数据的代码的部分给包起来
 * 
 * 注意:
 * 同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。
 * 多个线程必须是同一把锁。
 */
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();


// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");


// 启动线程
t1.start();
t2.start();
t3.start();
}
}




0 0