多线程基础详细总结以及常用API

多线程基础详细总结以及常用API

1. 线程与进程

1.1什么是进程?

一个正在运行的程序就是一个进程。进程是操作系统资源分配(计算资源,比如CPU,存储:内存)的最小单位。

1.2并发与多进程

首先,我们来介绍一下并发的基本概念和原理。对于现代操作系统来说,大部分操作系统都是多任务操作系统。什么是“多任务”呢?指的是每个系统在同时能够运行多个进程。

例如,在 windows 中,可能开着一个 Word 窗口写文档,开着一个 eclipse 写代码,开着一个 QQ 聊天,开着一个谷歌浏览网页,开着一个网易云听歌…这就意味着,在一个操作系统中,同时有多个程序在内存中运行着。每一个运行着的程序,就是操作系统中运行着的一个任务,也就是我们所说的“进 程”。例如,在 windows 中,可以通过“任务管理器”来查看系统中有多少进程正在运行。

我们知道,一般来说,在个人电脑上,往往只有一个 CPU。同时,每个程序运行的时候,都需要在CPU 上完成运算,也就是说,所有的程序运行时,都需要占用 CPU。那么在系统中,是如何做到使用单 CPU 同时运行多个程序的呢?接下来,我们来阐述一下单 CPU 执行多任务的原理。

假设我们同时开了多个程序:Word,Google,QQ,网易云音乐,对于操作系统来说,这意味着有四个进程要同时运行。为了解决这个问题,计算机规定了:让这四个进程轮流使用 CPU,每个进程用一小会儿。这个“一小会儿”,往往是若干个毫秒,这段时间被称为一个“CPU时间片”。这样,每一秒钟可能有成百上千个 CPU 时间片,也就是说,在一秒钟之内,这四个程序可能各自能够占用一小会儿 CPU,从而运行一下。从微观上来看,每一个特定的时刻,CPU 上只有一个程序在运行。示意图如下:
在这里插入图片描述

如上图所示,在某个特定的 CPU 时间片中,只运行一个程序。而操作系统控制 CPU,让多个程序不停的切换,从而保证多个程序能够轮流使用 CPU。从本质上说,单 CPU 一次只能运行一个任务。但是,由于 CPU 时间片非常短,每次从一个程序切换到另一个程序的时候速度很快。在一秒钟内,可能有成百上千个 CPU 时间片,也就是说系统可能进行了成百上千次任务的切换。由于人的反应相对计算机来说,是比较慢的,因此对于人来说,感受就是在一秒钟内,这几个进程同时在运行。

这就是单 CPU 执行多任务的原理:利用很短的 CPU 时间片运行程序,在多个程序之间进行 CPU 时间片的进行快速切换。可以把这种运行方式总结成为一句话:宏观上并行,微观上串行。这是说,从宏观上来看,一个系统同时运行多个程序,这些程序是并行执行的;而从微观上说,每个时刻都只有一个程序在运行,这些程序是串行执行的。

1.3 什么是线程?

上面所说的,是操作系统与多进程的概念。但是,由于 Java 代码是运行在 JVM 中的,对于某个操作系统来说,一个 JVM 就相当于一个进程。而 Java 代码不能够越过 JVM 直接与操作系统打交道,因此,Java 语言中没有多进程的概念。也就是说,我们无法通过 Java 代码写出一个多进程的程序来。

Java 中的并发,采用的是线程的概念。简单的来说,一个操作系统可以同时运行多个程序,也就是说,一个系统并发多个进程;而对于每个进程来说,可以同时运行多个线程,也就是:一个进程并发多个线程。

从上面的描述上我们可以看出,线程就是在一个进程中并发运行的一个程序流程。当若干的 CPU 时间片分配给 JVM 进程的时候,系统还可以把时间片进一步细分,分给 JVM 中的每一个线程来执行,从而达到“宏观上并行,微观上串行”的线程执行效果。

目前为止,我们见到的 Java 程序只有一个线程,也就是说,只有一个程序执行流程。这个流程从 main方法的第一行开始执行,以 main 方法的退出作为结束。这个线程我们称之为“主线程”。

那么,一个线程运行,需要哪些条件呢?首先,必须要给线程赋予代码。通俗的来说,就是必须要为线程写代码。这些代码是说明,启动线程之后,这个线程完成了什么功能,我们需要这个线程来干什么。

其次,为了能够运行,线程需要获得 CPU。只有获得了 CPU 之后,线程才能真正启动并且执行线程的代码。CPU 的调度工作是由操作系统来完成的。

第三,运行线程时,线程必须要获得数据。也就是说,在进行运算的时候,线程需要从内存空间中获得数据。关于线程的数据,有一个结论,叫做“堆空间共享,栈空间独立”。所谓的“堆空间”,保存的是我们利用 new 关键字创建出来的对象;而所谓的栈空间,保存的是程序运行时的局部变量。“堆空间共享,栈空间独立”的意思是:多线程之间共享同一个堆空间,而每个线程又拥有各自独立的栈空间。因此,运行程序时,多个不同线程能够访问相同的对象,但是多个不同线程彼此之间的局部变量是独立的,不可能出现多个线程访问同一个局部变量的情况。CPU、代码、数据,是线程运行时所需要的三大条件。在这三大条件中,CPU 是操作系统分配的,Java程序员无法控制;数据这部分,需要把握住“堆空间共享,栈空间独立”的概念;

1.4 什么是单线程?

单线程就是一心一意,用情专一的痴情少年

如果一个进程,只有一个线程。这样的程序叫做单线程程序。

  • 好处:资源可以最大化使用。不会出现争夺资源的问题。

  • 缺陷:效率很低,容易阻塞。无法处理并发任务(例如:多人聊天)。

当你程序启动的时候,JVM会创建一个线程执行你的main函数,这个线程称为主线程。

1.5 什么是多线程?

如果一个进程,拥有不止一个线程。这样的程序称为多线程程序。

  • 优势:可以同时执行多个任务。提高运行的效率。

1.6什么时候使用多线程?

  1. 多个任务互不影响,任务之间没有交集,谁先执行完,谁后执行完无所谓。这种时候可以使用多线程,让多个任务同时执行。

  2. 当你有一个任务很耗时,可以把这个耗时的任务放到一个单独线程里执行,这样就不会阻塞程序的执行。

  3. 你的需求只能靠多线程(多人聊天,英雄联盟各个角色的操作)完成的时候,要使用多线程。

  4. 买票 12306 1亿人同时抢票

  5. 聊天室

  6. 游戏

  7. 下载 多线程

  8. 定时任务 定时执行某个任务 备份数据 /批处理任务/日志记录

  9. 数据库分析 数据库迁移

2. java实现多线程的4种方式:

1、继承Thread类,重写run方法(其实Thread类本身也实现了Runnable接口)

  1. 创建一个类继承于Thread。

  2. 重写这个类的run方法。

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("接收消息.。。。。");
        }
    }
}

public class MyThread02 extends Thread{

    @Override
    public void run() {

        for (int i = 0; i < 1000; i++) {
            System.out.println("发送消息");
        }
    }
}

public class Test1 {

    public static void main(String[] args) {

        MyThread02 myThread02 = new MyThread02();
        myThread02.start();  //开始启用线程,发送信息
//        myThread02.run();   不会按照线程来执行

        MyThread myThread = new MyThread();
        myThread.start(); //开启线程    接收信息 
    }
}

匿名类的写法:

public class TestMultiplyThread2 { 
    public static void main(String[] args) {
        Thread t1 = new Thread("线程B") { 
            @Override 
            public void run() { 
                System.out.println("我是一个子线程"); 
                for(int i = 0; i < 500000; i++) { 
                    System.out.println(i);
                } 
            } 
        };
        t1.start(); 
        System.out.println("hello world"); 
    } 
}

2、实现Runnable接口

  1. 创建一个类实现Runnable接口

  2. 实现接口中的run方法

  3. 创建实现类的对象

  4. 实现类的对象作为Thread类的参数

  5. 启动线程

public class MyThread implements Runnable{

    private String msg;
    public MyThread(String msg) {
        this.msg = msg;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(msg);
        }
    }
}



public class Test {

    public static void main(String[] args) {

        Runnable my1 = new MyThread("发送信息");
        Thread thread = new Thread(my1);
        thread.start();

        Runnable my2 = new MyThread("接收信息.....");
        Thread thread2 = new Thread(my2);
        thread2.start();
    }
}

匿名类的写法:

public class TestMultiplyThread3 {
    public static void main(String[] args) { 
        // Runnable mr = new MyRunnable();
        Runnable mr = new Runnable() {
            @Override 
            public void run() {
                System.out.println("我是一个子线程");
                for(int i = 0; i < 500000; i++) { 
                    System.out.println(i); 
                } 
            } 
        };
        Thread t = new Thread(mr); 
        t.start(); 
        System.out.println("hello world"); 
    } 
}

3、通过Callable和FutureTask创建线程 ,重写call方法(有返回值)

在Java 1.5以前,创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。无论我们以怎样的形式实现多线程,都需要调用Thread类中的start方法去向操作系统请求io,cpu等资源。因为线程run方法没有返回值,如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。

而自从java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。

Callable和Future介绍

Callable接口代表一段可以调用并返回结果的代码;Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法在线程池中执行Callable内的任务。由于Callable任务是并行的(并行就是整体看上去是并行的,其实在某个时间点只有一个线程在执行),我们必须等待它返回的结果。

Callable的任务执行后可返回值,运行Callable任务可以拿到一个Future对象。Future表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检查计算的结果。通过Future对象可以了解任务的执行情况,可以取消任务的执行,还可以获取任务的执行结果。

(1)创建Callable实现类+重写call方法;

(2)借助执行调度服务ExecutorService,获取Future对象ExecutorService ser = Executors.newFixedThreadPool(2);

Future result = ser.submit(实现类对象)

(3)获取result.get();

(4)停止服务ser.shutdownNow();

public class MyCal1 implements Callable  {
    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 1000; i++) {
            System.out.println("接收消息");
        }
        return "接收成功";
    }
}

public class MyCal2 implements Callable {
    @Override
    public Object call() throws Exception {

        for (int i = 0; i < 1000; i++) {
            System.out.println("发送消息......");
        }
        return "发送成功";
    }
}

public class Test {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个连接池,可以支持两个线程的使用
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        MyCal1 myCal1 = new MyCal1();
        MyCal2 myCal2 = new MyCal2();

        //说明每个线程都开始执行业务代码,但是没有返回值
        Future future1 = executorService.submit(myCal1);
        Future future2 = executorService.submit(myCal2);

        Object o1 = future1.get();
        Object o2 = future2.get();
        System.out.println(o1);
        System.out.println(o2);

        executorService.shutdownNow(); //关闭线程池,释放资源
    }
}

练习:

使用callable完成,龟兔赛跑示例:

class Race implements Callable<Integer>{
    private String name;//名称
    private long time;//延时时间
    private boolean flag = true; 
    private int step = 0; 
    public Race() { }
    public Race(String name) { 
        super(); 
        this.name = name;
    }
    public Race(String name, long time) {
        super();
        this.name = name;
        this.time = time; 
    }
    public String getName() { 
        name;
 	}
    public void setName(String name) { 
        this.name = name; 
    }
    public long getTime() { 
        return time;
    }
    public void setTime(long time) { 
        this.time = time;
    }
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) { 
        this.flag = flag; 
    }
    public int getStep() { 
        return step; 
    }
    public void setStep(int step) { 
        this.step = step; 
    }
    @Override
    public Integer call() throws Exception {
        while (flag){
            Thread.sleep(time);
            step++; 
        } 
        return step; 
    } 
}


/** 使用Callable创建线程 */ 
public class Demo01 {
    public static void main(String[] args) throws ExecutionException, InterruptedException { 
        // 创建线程
        ExecutorService ser = Executors.newFixedThreadPool(2); 
        Race tortoise = new Race("乌龟",1000);
        Race rabbit = new Race("兔子",500); 
        Future<Integer> result1 = ser.submit(tortoise); 
        Future<Integer> result2 = ser.submit(rabbit); 
        Thread.sleep(2000); tortoise.setFlag(false); 
        rabbit.setFlag(false); // 获取值 
        Integer num1 = result1.get();
        Integer num2 = result2.get(); 
        System.out.println("乌龟"+num1); 
        System.out.println("兔子"+num2);//谁的步数多 谁跑的远 
        //停止服务 
        ser.shutdownNow();
    } 
}

3. 线程状态及声明周期

3.1 多线程的常用方法

currentThread() 获取当前线程对象。 类方法

setName(String name) 设置线程的名字。

getName() 获取线程的名字。

setPriority(int priority) 设置线程的优先级。 优先级的取值范围[1,10],默认是5,10为最高

getPriority() 获取线程的优先级。

getState() 获取线程的状态

join() 执行该线程,会阻塞当前线程。

sleep(long millis) 休眠指定时间(单位毫秒),会阻塞当前线程。类方法

start() 启动线程

yield() 暂定该线程的执行,交出CPU的使用权。

3.2 线程的状态及声明周期

简单描述,线程的创建有这样四个状态:初始状态、可运行状态、运行状态、终止状态。这四个状态的转换如下图:

在这里插入图片描述

初始状态:当我们创建了一个线程对象,而没有调用这个线程对象的 start()方法时,此时线程处于初始状态。创建了一个线程对象,不等于在系统中创建了一个新线程。当我们创建一个线程对象时,此时线程处于初始状态。也就是说,线程对象自身进行了一些初始化的操作,而并没有向操作系统申请创建系统中的线程。

另外,要注意的是,当一个线程调用 start()方法之后,由初始状态进入了可运行状态。这个状态的转换过程是一个单向的过程,只能有初始状态转换为可运行状态,而不能由可运行状态转换为初始状态。也就是说,一旦调用完了 start 方法之后,线程就进入了可运行状态而不会回到初始状态,因此在线程对象的整个生命周期中,只能调用一次 start()方法。如果对同一个线程对象多次调用 start()方法,会产生一个 IllegalStateException 异常。

可运行状态:处于可运行状态下的线程,具有这样的特点:线程已经为运行做好了完全的准备,只等着获得 CPU来运行。也就是说,线程是万事俱备,只欠 CPU。

运行状态:处于这种状态的线程获得了 CPU 时间片,正在执行代码。由于系统中只有一个 CPU,因此,同时只能有一个线程处于运行状态。

当 CPU 空闲的时候,操作系统会检查是否有可运行状态的线程。如果有一个或多个线程处于可运行状态,系统会根据一定的规则挑选一个线程,为这个线程分配一个 CPU 时间片,从而使得这个线程进入了运行状态。

当然,一个线程不能永远的占用 CPU,不可能永远的处于运行状态。系统为了实现多任务同时进行,会为线程分配一个 CPU 时间片。当 CPU 时间片到期而线程没有执行完毕时,操作系统会把处于运行状态的线程转换成可运行状态,然后再重新从可运行状态中选取一个线程,为其分配 CPU 时间片。这就是运行状态和可运行状态之间的转换。

**终止状态:**当一个线程执行完了 run()方法中的代码,该线程就会进入终止状态。要注意的是,这个状态转换也是一个单向箭头。也就是说,一旦一个线程从运行状态进入了终止状态,那这个线程就进入了生命的尾声,我们无法通过任何手段重新启动这个线程。

特别要提醒的是,在上一小节中,除了我们创建的两个线程之外,系统中还存在第三个线程:主线程。主线程的启动没有必要经历初始状态,当我们启动 JVM 执行程序时,JVM会执行某个类的主方法,此时主方法就在主线程中执行。此后,当主方法结束之后,主线程就进入了终止状态。需要注意的是,主线程进入终止状态,并不意味着整个程序就结束了。

sleep()与阻塞

除了上面所说的四种状态之外,还有一个很重要的状态:阻塞状态。

如果一个线程要进入运行状态,则必须要获得 CPU 时间片。但是,在某些情况下,线程运行不仅需要 CPU 进行运算,还需要一些别的条件,例如等待用户输入、等待网络传输完成,等等。如果线程正在等待其他的条件完成,在这种情况下,即使线程获得了 CPU 时间片,也无法真正运行。因此,在这种情况下,为了能够不让这些线程白白占用 CPU 的时间,会让这些线程会进入阻塞状态。最典型的例子就是等待I/O,也就是说,如果线程需要与 JVM 外部进行数据交互的话(例如等待用户输入、读写文件、网络传输等),这个时候,当数据传输没有完成时,线程即使获得 CPU 也无法运行,因此就会进入阻塞状态。

sleep方法的源码:

public static void sleep(long millis) throws InterruptedException

这个方法是一个 static 方法,也就意味着可以通过类名来直接调用这个方法。sleep 方法的参数是一个long 类型,表示要让这个线程“睡”多少个毫秒(注意,1 秒=1000 毫秒)。

另外,这个方法抛出一个 InterruptedException 异常,这个异常是一个已检查异常,根据异常处理的规则,这个异常必须要处理。我们修改上一节的代码,让两个线程 MyThread1 和 MyRunnable2 每次输出之后都“睡”200 毫秒。首先修改 MyThread1 类:

class MyThread1 extends Thread{
    public void run(){ 
        for(int i = 1; i<=1000; i++){ 
            System.out.println(i + " $$$"); 
            try{Thread.sleep(200); 
               }
            catch(InterruptedException e){ 
            } 
        } 
    }
}

在 for 循环中增加了 Thread.sleep 方法。要注意的是,由于 sleep 方法抛出一个已检查异常,所以必须要处理。由于 Thread 类中的 run 方法没有抛出任何异常,根据方法覆盖的要求,MyThread1 类中的 run 方法也不能抛出任何异常。因此,对于 sleep 方法的抛出的异常,只能用 try-catch 的方
法处理

下面是修改后的 MyRunnable2 的代码:

class MyRunnable2 implements Runnable{
    public void run(){
        for(int i = 1; i<=1000; i++){ 
            System.out.println(i + " ###"); 
            try{Thread.sleep(200); 
               }
            catch(InterruptedException e){
                
            } 
        } 
    } 
}
3.3 join()

除了使用 sleep()和等待 IO 之外,还有一个方法会导致线程阻塞,这就是线程的 join()。

class MyThread1 extends Thread{ 
    public void run(){
        for(int i = 0; i<100; i++){
            System.out.println(i + " $$$"); 
        }
    }
}


class MyThread2 extends Thread{ 
    Thread t;
    public void run(){ 
        try{
            t.join();
        }
        catch(Exception e){
        }
        for(int i = 0; i<100; i++){
            System.out.println(i + " ###");
        } 
    }
}


public class TestJoin{
    public static void main(String args[]){ 
        MyThread1 t1 = new MyThread1(); 
        MyThread2 t2 = new MyThread2(); 
        t2.t = t1; 
        t1.start();
        t2.start();
        } 
}

上面这个程序中,MyThread2 对象增加了一个属性 t。在主方法中,把 t1 对象赋值给 t,也就是让 t 属性和 t1 引用指向同一个对象。

最重要的一点是,在 MyThread2 类的 run()方法中,调用了 t 属性的 join()方法。这个方法能够让MyThread2 线程由运行状态进入阻塞状态,直到 t 线程结束。下面结合程序的状态转换图,来说明一下执行的过程。

首先 main 方法中,创建了两个线程对象,并且调用了 t1 和 t2 线程的 start()方法,这样,这两个线程就进入了可运行状态。假设操作系统首先挑选了 t1 线程进入了可运行状态,于是输出若干个“$$$”。经过一段时间之后,由于 CPU 时间片到期,t1 线程进入了可运行状态。假设经过了一段时间之后,操作系统选择了 t2 线程进入了可运行状态。进入了 t2 线程的run()方法之后,调用了 t 属性的 join()方法。由于 t 属性与 t1 指向同一个对象,因此这也就意味着在 t2 线程中,调用了 t1 线程的 join()方法。调用之后,t2 线程会进入阻塞状态。

此时,运行状态没有线程在执行,因此系统会从可运行状态中选择一个线程执行。由于可运行状态此时只有一个 t1 线程,因此这个线程会一直占用 CPU,直到线程代码执行结束。当t1 线程结束之后,t2 才会由阻塞状态进入可运行状态,此时才能够执行 t2 的代码。因此,从输出结果上来看,会先执行 t1 线程的所有代码,然后再执行 t2 线程的所有代码。

要注意的是,我们在 t2 线程中调用 t1 线程的 join()方法,结果是 t2 阻塞,直到 t1 线程结束。t2 线程是 join()方法的调用者,而 t1 线程是被调用者,在调用 join()方法的过程中,方法的调用者被阻塞,阻塞到被调用的线程结束。

我们可以用一个生活中的比喻来解释 join()方法。假设顾客到饭店里去吃饭,那么每一个顾客就可以认为是一个线程。顾客点菜,就可以当做是顾客线程要求启动一个厨师线程为自己做饭,在做饭过程中,顾客线程只能等待。因此,这也可以当做是顾客线程调用了厨师线程的 join()方法,等厨师做完饭了,顾客才能继续下一步:吃饭。在这个例子中,顾客线程就调用了厨师线程的 join()方法。在调用 join 方法的过程中,要注意,不能让两个线程相互 join()。例如,如果一个顾客点菜,相当于调用了厨师的 join()方法,顾客打算等厨师做完饭以后,吃完饭再给钱;而厨师呢,在拿到顾客下的单之后,希望顾客先给钱,之后再开始做饭,于是厨师也调用了顾客的 join()方法。这样的结果就是两边互相等待,结果谁都无法继续下去。我们拿代码模拟一下这种情况:

class MyThread1 extends Thread{ 
    Thread t;
    public void run(){ 
        try{t.join(); 
           }catch(Exception e){
        }
        for(int i = 0; i<100; i++){
            System.out.println(i + " $$$"); 
        } 
    }
}
class MyThread2 extends Thread{ 
    Thread t; 
    public void run(){ 
        try{
            t.join();
        }catch(Exception e){
        }
        for(int i = 0; i<100; i++){
            System.out.println(i + " ###");
        } 
    } 
}
public class TestJoin{
    public static void main(String args[])
    {
        MyThread1 t1 = new MyThread1(); 
        MyThread2 t2 = new MyThread2();
        t2.t = t1;
        t1.t = t2;
        t1.start();
        t2.start();
    } 
}

这段代码会迟迟无法运行下去,原因就在于,t1 线程等待 t2 线程结束,而 t2 线程等待t1 线程结束,这样的结果就是两边的线程都处于阻塞状态而无法运行。

那怎么解决这个问题呢?首先,写程序的时候应当小心,尽量不应该出现这样的代码。因为这样的代码在编译和运行时都不会出现任何错误和异常。另一方面,Java 也为 join()方法提供了一个替换的方案。

在 Thread 类中,除了有一个无参的 join()方法之外,还有一个有参的 join()方法,方法签名如下:

public final void join(long millis) throws InterruptedException

这个方法接受一个 long 类型作为参数,表示 join()最多等待多少毫秒。也就是说,调用这个 join()方法的时候,不会一直处于阻塞状态,而是有一个时间限制。就好像顾客等待厨师做饭时,不会无限制的等下去,如果菜一段时间内还不上,则顾客就会离开,而不会一直傻等下去。利用这个方法,修改上面的MyThread1 类:

class MyThread1 extends Thread{ 
    Thread t;
    public void run(){
        try{t.join(1000);
           }
        catch(Exception e){
        }
        for(int i = 0; i<100; i++){ 
            System.out.println(i + " $$$");
        }
    } 
}

在 Thread 类中,除了有一个无参的 join()方法之外,还有一个有参的 join()方法,方法签名如下:

public final void join(long millis) throws InterruptedException

这个方法接受一个 long 类型作为参数,表示 join()最多等待多少毫秒。也就是说,调用这个 join()方法的时候,不会一直处于阻塞状态,而是有一个时间限制。就好像顾客等待厨师做饭时,不会无限制的等下去,如果菜一段时间内还不上,则顾客就会离开,而不会一直傻等下去。利用这个方法,修改上面的MyThread1 类:

class MyThread1 extends Thread{ 
    Thread t;
    public void run(){
        try{t.join(1000);
           }
        catch(Exception e){
        }
        for(int i = 0; i<100; i++){ 
            System.out.println(i + " $$$");
        }
    } 
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王斐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值