Java多线程(超详解)

目录

1. 线程简介

1.1 程序

1.2 进程

1.3 线程

1.4 多线程

1.5 普通方法调用和多线程

2. 线程创建

2.1 继承Thread类

2.2 实现Runnable接口     

2.3 实现Callable接口(了解)

2.4 网图下载

2.4.1 通过继承Thread类实现网图下载    

2.4.2 通过实现Runnable接口实现网图下载

2.4.3通过实现Callable接口实现网图下载

3. Lambda表达式

4.线程状态

4.1 线程的状态

4.1.1停止线程

4.2 线程的调度

4.2.1 线程的优先级

4.2.2 线程休眠

4.2.3 线程让步

4.2.4 线程插队

4.2.5 线程状态观测

5.守护(daemon)线程

6. 线程同步与通信问题

6.1 三大不安全案例

6.2 多线程同步

6.2.1 同步代码块

6.2.2 同步方法

6.2.3 拓展:CopyOnWriteArrayList

6.3 多线程通信

7. 线程池

8. 练习

8.1 龟兔赛跑

8.2 模拟多人爬山

8.3 模拟叫号看病


1. 线程简介

1.1 程序

程序是指令和数据的集合,其本身没有任何运行的含义,是一个静态的概念。

1.2 进程

在一个操作系统中,每个独立的程序都可以称为一个进程,也就是“正在运行的程序”,(进程就是程序执行的过程)。它是一个动态的概念,是系统分配资源的单位。

1.3 线程

  • 每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些运执行单元可以看作程序执行的一 条路径,被称为线程。
  • 线程是CPU调度和执行的单位。
  • 操作系统中的每一个进程中都至少存在一个线程。
  • 当一个Java程序启动时就会产生一个进程, 该进程会默认创建一个线程, 在这个线程上会运行main ()方法中的代码。

1.4 多线程

  • 在一个进程中,同时运行了多个线程,用来完成不同的工作,则称之为多线程。
  • 多个线程交替占用CPU资源,而非真正的并行执行。
  • 好处:
    • 充分利用CPU资源。
    • 简化编程模型。
    • 带来良好的用户体验。
  • 注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。

1.5 普通方法调用和多线程

 

核心概念:

  • 线程就是独立的执行路径。
  •  在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程。
  • main()称之为主线程,为系统的入口,用于执行整个程序。
  •  在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的。
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制。
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销。
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

案例一:输出线程名称

package demo01;
public class Example1 {
    public static void main(String[] args) {
        Thread thread =Thread.currentThread();
        System.out.println("当前线程是:"+thread.getName());
        thread.setName("MyThread");
        System.out.println("当前线程是:"+thread.getName());
    }
}

运行结果:

2. 线程创建

2.1 继承Thread类

将一个类声明为Thread的子类。这个子类应该重写run类的方法Thread。然后可以分配并启动子类的实例。

可以分为三步:

  • 自定义线程类继承Thread类。
  • 重写run()方法,编写线程执行体。
  • 创建线程对象,调用start()方法启动线程。

案例一:单线程例子


public class Example2 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.run();
        while (true){
            System.out.println("Demo1类的main方法运行");
        }
    }
}
class MyThread {
    public void run(){
        while (true){
            System.out.println("MyThread类的run方法运行");
        }
    }
}

运行结果:

 分析:该程序一直在运行myThread对象的run方法,而不会运行System.out.println("Demo1类的main方法运行");语句,因为该程序是一个线程程序,是一条单路径。myThread.run()语句是一个死循环程序,不运行完不会执行System.out.println("Demo1类的main方法运行");语句。

案例二:多线程

package demo02;

public class TestThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        while (true){
            System.out.println("Demo类的main运行方法");
        }

    }

}
class MyThread extends Thread{
    public void run(){
        while (true){
            System.out.println("MyThread类的run方法运行");
        }
    }
}

运行结果:

案例三:多线程

package demo03;

public class Test {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();  //开辟新线程
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        Thread.currentThread().setName("子线程");
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}

运行结果:

 2.2 实现Runnable接口     

创建一个线程是声明实现类Runnable接口。那个类然后重写run类的方法。然后可以分配类的实例,在创建的时候作为参数传递,并启动。

可以分为三步:

  • 定义MyRunnable类实现Runnable接口
  • 实现run()方法,编写线程执行体

  • 创建线程对象,调用start()方法启动线程

案例一

class MyThread implements Runnable{
    public void run(){
        while (true){
            System.out.println("MyThread类的run方法");
        }
    }
}
public class Demo01 {
    public static void main(String[] args) {
        //创建一个实现了Runnable接口的对象
        MyThread myThread = new MyThread();
        //将该对象传给Thread类的构造函数
        Thread thread = new Thread(myThread);
        //thread对象调用start方法,在这里不会调用自身的run方法,它会调用myThread对象的run方法
        thread.start();
        while (true){
            System.out.println("Demo类的main方法");
        }
    }
}

运行结果:

 案例二:

public class MyThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}
class Test{
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread1 = new Thread(myThread);
        thread1.start();
        Thread thread2 = new Thread(myThread);
        thread2.start();
    }
}

运行结果:

 总结:

  • 继承Thread类
    • 子类继承Thread类具有多线程能力
    • 启动线程:子类对象.start()
    • 不建议使用:避免OOP单继承局限性
  • 实现Runnable接口
    • 实现接口Runnable具有多线程能力
    • 启动线程:传入目标对象+Thread对象.start()
    • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

2.3 实现Callable接口(了解)

  1. 实现Callable接口,需要返回值类型
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
  5. 提交执行:Future<Boolean> result1 = ser.submit(t1);
  6. 获取结果:boolean r1 = result1.get()
  7. 关闭服务: ser.shutdownNow();

2.4 网图下载

2.4.1 通过继承Thread类实现网图下载    

1. 导入commons-io-2.6.jar包

 commons-io-2.6.jar包下载地址: 

https://mvnrepository.com/artifact/commons-io/commons-io/2.6j
将该文件复制到lib包中,效果如下:

选中lib这个包,点击右键,选择Add as Library,再点击ok即可成功再idea中导入common-io-2.6.jar工具包:

2.实现代码: 

import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
//多线程同步下载图片
public class TestThread extends Thread{
    private String url; //网络图片地址
    private String name;//保存的文件名
    public TestThread(String url,String name){
        this.url = url;
        this.name = name;
    }
    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("下载了文件名为:"+name);
    }

    public static void main(String[] args) {
        TestThread testThread1 = new TestThread("https://dss2.bdstatic.com/5bVYsj_p_tVS5dKfpU_Y_D3/res/r/image/2022-10-16/toutiao1.png","1.jpg");
        TestThread testThread2 = new TestThread("https://dss2.bdstatic.com/5bVYsj_p_tVS5dKfpU_Y_D3/res/r/image/2022-10-16/toutiao2.png","2.jpg");
        TestThread testThread3 = new TestThread("https://dgss0.baidu.com/6ONWsjip0QIZ8tyhnq/it/u=1848053418,1608633216&fm=173&app=49&f=JPEG?w=312&h=208&s=A10BDE1442585DCE0642B4C2030050BA","3.jpg");
        TestThread testThread4 = new TestThread("https://dgss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2303065390,19508065&fm=173&app=49&f=JPEG?w=312&h=208&s=E8720DD7568066E40838C97203008033","4.jpg");
        testThread1.start();
        testThread2.start();
        testThread3.start();
        testThread4.start();
    }
}
//下载器
class WebDownloader{
    //下载方法
    public void downloader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常,downloader方法出现问题!");
        }
    }
}

运行结果:输出下载好的文件名

 下载好的图片位置

注意:四个线程同时启动,一起运行,所以输出顺序不一样。

2.4.2 通过实现Runnable接口实现网图下载

只演示代码,其他步骤和2.4.1一样。

import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;

//多线程同步下载图片
public class MyThread implements Runnable{
    private String url; //网络图片地址
    private String name;//保存的文件名
    public MyThread(String url,String name){
        this.url = url;
        this.name = name;
    }
    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("下载了文件名为:"+name);

    }

    public static void main(String[] args) {
        MyThread myThread1 = new MyThread("https://dss2.bdstatic.com/5bVYsj_p_tVS5dKfpU_Y_D3/res/r/image/2022-10-16/toutiao1.png","1.jpg");
        MyThread myThread2 = new MyThread("https://dss2.bdstatic.com/5bVYsj_p_tVS5dKfpU_Y_D3/res/r/image/2022-10-16/toutiao2.png","2.jpg");
        MyThread myThread3 = new MyThread("https://dgss0.baidu.com/6ONWsjip0QIZ8tyhnq/it/u=1848053418,1608633216&fm=173&app=49&f=JPEG?w=312&h=208&s=A10BDE1442585DCE0642B4C2030050BA","3.jpg");
        MyThread myThread4 = new MyThread("https://dgss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2303065390,19508065&fm=173&app=49&f=JPEG?w=312&h=208&s=E8720DD7568066E40838C97203008033","4.jpg");
        new Thread(myThread1).start();
        new Thread(myThread2).start();
        new Thread(myThread3).start();
        new Thread(myThread4).start();

    }
}
//下载器
class WebDownloader{
    //下载方法
    public void downloader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常,downloader方法出现问题!");
        }
    }
}

2.4.3通过实现Callable接口实现网图下载

只演示代码,其他步骤和2.4.1一样。

import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
//多线程同步下载图片
public class CallableThread implements Callable<Boolean> {
    private String url; //网络图片地址
    private String name;//保存的文件名
    public CallableThread(String url,String name){
        this.url = url;
        this.name = name;
    }
    @Override
    public Boolean call() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("下载了文件名为:"+name);
        return true;
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableThread callableThread1 = new CallableThread("https://dss2.bdstatic.com/5bVYsj_p_tVS5dKfpU_Y_D3/res/r/image/2022-10-16/toutiao1.png","1.jpg");
        CallableThread callableThread2= new CallableThread("https://dss2.bdstatic.com/5bVYsj_p_tVS5dKfpU_Y_D3/res/r/image/2022-10-16/toutiao2.png","2.jpg");
        CallableThread callableThread3= new CallableThread("https://dgss0.baidu.com/6ONWsjip0QIZ8tyhnq/it/u=1848053418,1608633216&fm=173&app=49&f=JPEG?w=312&h=208&s=A10BDE1442585DCE0642B4C2030050BA","3.jpg");
        CallableThread callableThread4= new CallableThread("https://dgss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2303065390,19508065&fm=173&app=49&f=JPEG?w=312&h=208&s=E8720DD7568066E40838C97203008033","4.jpg");
        //创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(4);
        //提交执行
        Future<Boolean> result1 = ser.submit(callableThread1);
        Future<Boolean> result2 = ser.submit(callableThread2);
        Future<Boolean> result3 = ser.submit(callableThread3);
        Future<Boolean> result4 = ser.submit(callableThread4);
        //获取结果
        boolean a1 = result1.get();
        boolean a2 = result1.get();
        boolean a3 = result1.get();
        boolean a4 = result1.get();
        System.out.println(a1);
        System.out.println(a2);
        System.out.println(a3);
        System.out.println(a4);
        //关闭服务
        ser.shutdownNow();
    }
}
//下载器
class WebDownloader{
    //下载方法
    public void downloader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常,downloader方法出现问题!");
        }
    }
}

3. Lambda表达式

  • 避免匿名内部类定义过多
  • 其实质属于函数式编程概念

函数式接口定义:

1. 任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。

public interface Runnable{
    public abstract void run()
}

2. 对于函数式接口,我们可以通过Lambda表达式来创建该接口的对象。

案例一:(无参数)

package Demo11;
public class MyLambda {
    //3.静态内部类
    //优化
    static class Run2 implements IRun{
        @Override
        public void Lambda() {
            System.out.println("2 I'm learning the lambda expression.");
        }
    }

    public static void main(String[] args) {
        IRun iRun = new Run();
        iRun.Lambda();

        iRun = new Run2();
        iRun.Lambda();

        //4.局部内部类
        class Run3 implements IRun{
            @Override
            public void Lambda() {
                System.out.println("3 I'm learning the lambda expression.");
            }
        }
        iRun = new Run3();
        iRun.Lambda();

        //5. 匿名内部类
        iRun = new IRun() {
            @Override
            public void Lambda() {
                System.out.println("4 I'm learning the lambda expression.");
            }
        };
        iRun.Lambda();

        //6.用λ简化
        iRun = ()->{
            System.out.println("5 I'm learning the lambda expression.");
        };
        iRun.Lambda();
    }
}
//1. 定义一个函数式接口
interface IRun{
    void Lambda();
}
//2. 实现类
class Run implements IRun{
    @Override
    public void Lambda() {
        System.out.println("1 I'm learning the lambda expression.");
    }
}

运行结果:

案例二:带参数

1.

package Demo11;
public class MyLambda2 {
    public static void main(String[] args) {
        Igo go = new go();
        go.run(1);
    }
}
interface Igo{
    void run(int a);
}
class go implements Igo{
    @Override
    public void run(int a) {
        System.out.println("1 go to-->"+a);
    }
}

2.静态内部类

package Demo11;
public class MyLambda2 {
    static class go implements Igo{
        @Override
        public void run(int a) {
            System.out.println("1 go to-->"+a);
        }
    }
    public static void main(String[] args) {
        Igo go = new go();
        go.run(1);
    }
}
interface Igo{
    void run(int a);
}

3.局部内部类

package Demo11;
public class MyLambda2 {
    public static void main(String[] args) {
        class go implements Igo{
            @Override
            public void run(int a) {
                System.out.println("1 go to-->"+a);
            }
        }
        Igo go = new go();
        go.run(1);
    }
}
interface Igo{
    void run(int a);
}

4.匿名内部类

package Demo11;
public class MyLambda2 {
    public static void main(String[] args) {
        Igo go = new Igo() {
            @Override
            public void run(int a) {
                System.out.println("1 go to-->"+a);
            }
        };
        go.run(1);
    }
}
interface Igo{
    void run(int a);
}

5.lambda表达式

package Demo11;
public class MyLambda2 {
    public static void main(String[] args) {
        //lambda简化
//        Igo go = (int a)->{
//            System.out.println("1 go to-->"+a);
//        };
        //简化一:去掉参数类型
//        Igo go = (a)->{
//            System.out.println("1 go to-->"+a);
//        };
        //简化二:去掉括号
//        Igo go = null;
//        go = a->{
//            System.out.println("1 go to-->"+a);
//        };
        //简化三:去掉{}
        Igo go = null;
        go = a-> System.out.println("1 go to-->"+a);
        go.run(1);
    }
}
interface Igo{
    void run(int a);
}

运行结果均为:

总结:
1.Lambda表达式只能有一行代码的情况下才能简化为一行,如果有多行,那么就要用代码块包裹,也  就是花括号。
2. 前提是接口是函数式接口。
3. 多个参数也可以去掉参数类型,要去掉就都去掉(加括号),不能只去掉一个。

4.线程状态

4.1 线程的状态

  1. 新建状态(new):Thread t = new Thread(); 创建一个线程对象后,该线程对象就处新建状态。此时它不能运行,和其他Java对象一样,仅仅由Java虚报机为其分配了内存,没有表现出任何线程的动态特征,
  2. 就绪状态(Runnable):当线程对象调用了start ()后,该线程就进人就绪状态,也称可运行状态。处于就绪状态的线程放人到可运行池中,此时它只是其就进运行的条件,能否获得CPU的使用权,正需要等待系统的调度。
  3. 运行状态(Running):如果处于就绪状态的线程获得了 CPU的使用权,开始执行run()方法,则该线程处于运行状态。当一个线程启动后,它不可能一直处于运行状态,当使用完系统分配CPU使用权时同后,系统就会剥夺该线程占有的CPU资源,让其他线程获得执行的机会。
  4. 阻塞状态(Blocked):一个正在执行的线程在某些特殊情况下,如执行耗时的输入/输出操作时,会放弃CPU的使用权,进人阻塞状态。线程进人阻塞状态后,就不能进人排队队列。只有当引起阻塞的原因被消除后,线程才可以转人就绪状态。常见的线程状态转换成阻塞状态的原因,以及如何从阻塞状态转换成就绪状态。
    1. 当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进人阻塞状态。如果想从阻塞状态进人就绪状态必须得获取到其他线程所持有的锁。
    2. 当线程调用了一个阻塞式的IO方法时,该线程就会进人阻塞状态,如果想进人就绪状态就必须要等到这个阻塞的IO方法返回。
    3. 当线程调用了某个对象的wait ( )方法时,也会使线程进人阻塞状态,如果想进入就绪状态就需要使用notify ( )方法唤醒该线程。
    4. 当线程调用了Thread的sleep (long millis)方法时,也会使线程进人阻塞状态,在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进人就绪状态,
    5. .当在一个线程中调用了另 一个线程的join ()方法时,会使当前线程进人阻塞状态,在这种情况下,需要等到新加人的线程运行结束后才会结束阻塞状态,进入就绪状态。
    6. 需要注意的是,线程从阻塞状态只能进入就绪状态,而不能直接进人运行状态,也就是说结束阻塞的线程需要重新进人可运行池中,等待系统的调度
  5. 死亡状态(Terminated):线程的run()方法正常执行完毕或者线程抛出一个未捕获的异常或者错误时,线程就进人死亡状态。一旦进人死亡状态,线程将不再拥有运行的资格,也不能再转化到其他状态。

    演示代码

public class MyThread implements Runnable{
    @Override
    public void run() {
        try {
            System.out.println("线程t在运行");
            Thread.sleep(2000);
            System.out.println("线程t在短时间休眠后重新运行");
            int i = 10/0;
        } catch (Exception e) {
            System.out.println("线程被中断");
        }
    }

    public static void main(String[] args) {
        Thread t = new Thread(new MyThread());
        System.out.println("线程t为新建");
        t.start();
        System.out.println("线程t为就绪");
    }
}

 运行结果:

4.1.1停止线程

  • 不推荐使用JDK提供的stop()、destroy()方法。【已废弃】
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止变量当flag=false,则终止线程运行。

案例:

//测试停止线程
/*
1.建议线程正常停止-->利用次数,不建议死循环
2.建议使用标志位
3.不要使用stop或者destroy等过时或者JDK不建议使用的方法(存在一些BUG或者弊端)
 */
public class TestStop implements Runnable{
    //1.设置一个标志位 私有的 保证安全
    private boolean flag = true;
    @Override
    public void run() {
        int i = 0;
        while (flag){
            System.out.println("线程正在运行!"+i++);
        }
    }
    //2.设置一个公开的方法停止线程,转换标志位
    public void stop(){
        this.flag = false;
    }
    public static void main(String[] args) {
        TestStop testStop = new TestStop();
        new Thread(testStop).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main"+i);
            if (i==100){//100的时候停止
                testStop.stop();//调用stop方法停止运行(自己写的stop方法)
                System.out.println("线程停止运行!");
                //两个i是分别存活在两个线程里的独立参数,两个线程没关系,不需要break
                //注意:这里是主线程到100时,停止子线程
            }
        }
    }
}

运行结果:

4.2 线程的调度

线程调度指按照特定机制为多个线程分配CPU的使用权。

线程方法
方法 说明
setPriority(int newPriority) 更改线程的优先级
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠
void join() 等待该线程终止
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
void interrupt() 中断线程,别用这个方式
boolean isAlive() 测试线程是否处于活动状态

4.2.1 线程的优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
  • 优先级越高的线程获得CPU执行的机会越大。
  • 优先级越低的线程获得CPU执行的机会越小。
  • 线程的优先级用数字表示,范围从1~10
    • Thread.MIN_PRIORITY= 1;
    • Thread.MAX_PRIORITY = 10;
    • Thread.NORM_PRIORITY = 5;
  • 使用以下方式改变或获取优先级
    • getPriority() . setPriority(int xxx)

案例

public class TestThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"正在运行:"+i);
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new TestThread(),"线程A");//通过构造方法指定线程名
        Thread t2 = new Thread(new TestThread(),"线程B");
        //设置线程的优先级
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.MIN_PRIORITY);
        System.out.println("********线程的优先级*********");
        System.out.println("线程A的优先级:"+t1.getPriority());
        System.out.println("线程B的优先级:"+t2.getPriority());
        System.out.println("************************");
        t1.start();
        t2.start();
    }


4.2.2 线程休眠

  • 让线程暂时睡眠指定时长,线程进入阻塞状态。
  • 睡眠时间过后线程会再进入可运行状态。
  • sleep (时间)指定当前线程阻塞的毫秒数。
  • sleep存在异常InterruptedException。
  • sleep时间达到后线程进入就绪状态。
  • sleep可以模拟网络延时,倒计时等。
  • 每一个对象都有一个锁,sleep不会释放锁。

案例一

public class Sleep {
    public static void main(String[] args) {
        System.out.println("Wait");
        Wait.bySet(5);//让主线程等待5秒后在执行
        System.out.println("start");
    }
}
class Wait{
    public static void bySet(long s){
        for (int i = 0; i < s; i++) {
            System.out.println(i+1+"秒");
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

案例二:倒计时

public class TestStop {
    public static void main(String[] args) {
        try {
            tenDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void tenDown() throws InterruptedException {
        int a =10;
        while (true){
            Thread.sleep(1000);
            System.out.println(a--);
            if (a<=0){
                break;
            }
        }
    }
}

4.2.3 线程让步

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞。
  • 将线程从运行状态转为就绪状态。
  • 让cpu重新调度,礼让不一定成功!看CPU心情。

案例:

public class MyThread {
    public static void main(String[] args) {
        System.out.println("*******线程的礼让*******");
        TestThread my = new TestThread();
        Thread t1 = new Thread(my,"线程A");
        Thread t2 = new Thread(my,"线程B");
        t1.start();
        t2.start();

    }
}
class TestThread implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"正在运行"+i);
            if (i==3){
                System.out.println("线程礼让:");
                Thread.yield();
            }
        }
    }
}

 4.2.4 线程插队

  • 也叫线程的强制执行。
  • 使当前线程暂停执行,等待其他线程结束后再继续执行本线程
    • public final void join()
    • public final void join(long mills)
    • public final void join(long mills,int nanos)
    • millis:以毫秒为单位的等待时长
    • nanos:要等待的附加纳秒时长
    • 需处理InterruptedException异常

案例:

public class TestThread {
    public static void main(String[] args) {
        System.out.println("*******线程强制执行********");
        //创建子线程并启动
        Thread temp = new Thread(new MyThread());
        temp.start();
        for (int i = 0; i < 20; i++) {
            if (i==5){
                try {
                    temp.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"运行"+i);
        }
    }
}
class MyThread implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //输出当前线程信息
            System.out.println(Thread.currentThread().getName()+"运行"+i);
        }
    }
}

4.2.5线程状态观测

Thread.State
线程状态。线程可以处于以下状态之一

  • NEw:尚未启动的线程处于此状态。
  • RUNNABLE在Java虚拟机中执行的线程处于此状态。
  • BLOCKED被阻塞等待监视器锁定的线程处于此状态。·
  • WAITING正在等待另一个线程执行特定动作的线程处于此状态。
  • TIMED_WAITING正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
  • TERMINATED已退出的线程处于此状态。

一个线程可以在给定时间点处于一个状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。

案例:

//观察测试线程的状态
public class TestState {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{//lambda表达式
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("*******************");

        });
        //观察状态
        Thread.State state = thread.getState();
        System.out.println(state);//NEW
        //观察启动后
        thread.start();//启动线程
        state = thread.getState();
        System.out.println(state);//Run
        while (state !=Thread.State.TERMINATED){//只要线程不终止就一直输出状态
            Thread.sleep(100);
            state = thread.getState();//更新线程状态
            System.out.println(state);//输出状态
        }
    }
}

5.守护(daemon)线程

  • 线程分为用户线程和守护线程

  • 虚拟机必须确保用户线程执行完毕

  • 虚拟机不用等待守护线程执行完毕

  • 如:后台记录操作日志,监控内存,垃圾回收等待

案例:人生不过三万天

//测试守护线程
//国家守护你
public class TestDaemon {
    public static void main(String[] args) {
        China china = new China();
        You you = new You();
        Thread thread = new Thread(china);//让国家编程守护线程
        thread.setDaemon(true);//setDaemon返回值是Boolean 默认是false表示用户线程
        thread.start();//守护线程启动
        new Thread(you).start();//用户线程启动
    }
}
//国家
class China implements Runnable{  //守护线程
    @Override
    public void run() {
        while(true){//国家永远存在
            System.out.println("国家保护着你!");
        }
    }
}
class You implements Runnable{ //用户线程

    @Override
    public void run() {
        for (int i = 0; i < 30000; i++) {
            System.out.println("你开心的活着");
        }
        System.out.println("*******goodbye!*******");
    }
}

注意:goodbye!之后依旧会输入一段国家保护着你,因为虚拟机停止需要时间。

6. 线程同步与通信问题

6.1 三大不安全案例

案例一:购票案例

package demo06;
//多个线程同时操作同一个对象
//买火车票案例
public class TestThread implements Runnable{
    //票数
    private int ticketNums = 10;

    @Override
    public void run() {
        while (true){
            if (ticketNums<=0){
                break;
            }
            try {//模拟延时
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"--->拿到了第"+ticketNums--+"票");
        }
    }

    public static void main(String[] args) {
        TestThread ticket = new TestThread();
        new Thread(ticket,"林枫").start();
        new Thread(ticket,"叶凡").start();
        new Thread(ticket,"萧炎").start();
    }
}

运行结果:

案例二:银行取钱

//不安全取钱
//两个人去银行取钱,账户
public class UnsafeBank {
    public static void main(String[] args) {
        //账户
        Account account = new Account(1000,"生活费");
        Drawing you = new Drawing(account,500,"你");
        Drawing son = new Drawing(account,1000,"儿子");
        you.start();
        son.start();
    }
}
//账户
class Account{
    int money; //余额
    String name;

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}
//银行:模拟取款
class Drawing extends Thread{
    Account account;//账户
    int drawingMoney;
    //现在手里还有多少钱
    int nowMoney;
    public Drawing(Account account,int drawingMoney,String name){
        super(name);
        this.account=account;
        this.drawingMoney=drawingMoney;

    }
    //取钱
    @Override
    public void run(){
        //判断有没有钱
        if (account.money-drawingMoney<0){
            System.out.println(Thread.currentThread().getName()+"钱不够取不了");
            return;
        }
        try {//sleep放大问题的发生性
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //卡里的余额=余额-你取的钱
        account.money=account.money-drawingMoney;
        //你手里的钱
        nowMoney=nowMoney+drawingMoney;
        System.out.println(account.name+"余额为:"+account.money);
        //Thread.currentThread().getName()=this.getName()
        System.out.println(this.getName()+"手里的钱:"+nowMoney);
    }

}

运行结果:

 案例三:线程不安全的集合

import java.util.ArrayList;
import java.util.List;

//线程不安全的集合
public class UnsafeList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 100000; i++) {
            new Thread(()->{
               list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

运行结果:

6.2 多线程同步

  • 多线程的并发执行虽然可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。
  • 并发:同一个对象被多个线程同时操作。
  • 处理多线程问题时,多个线程访问同一个对象﹐并且某些线程还想修改这个对象﹒这时候我们就需要线程同步.线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用
  • 由于同一进程的多个线程共享同一块存储空间﹐在带来方便的同时,也带来了访问冲突问题﹐为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源﹐其他线程必须等待,使用后释放锁即可.存在以下问题:
    • 一个线程持有锁会导致其他所有需要此锁的线程挂起;
    • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题.

我们可以通过private关键字来保证数据对象只能被方法访问﹐所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,目前多线程同步有常用的两种方法,分别是同步代码块同步方法。

 6.2.1 同步代码块

当多个线程使用同一个资源时,可以将处理共享资源的代码放置在一个代码块中,使用synchronized关键字来修饰,被称作同步代码块

语法格式如下:

synchronized(lock){
    操作共享资源代码块
}

上述代码中,lock 是一个锁对象称之为同步监视器,它是同步代码块的关键。当线程执行同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置为0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位被置为1,新线程才能进人同步代码块执行其中的代码。循环往复,直到共享资源被处理完为止。

6.2.2 同步方法

在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。具体语法格式如下:

public synchronized void method(int args){
}

synchronized方法控制对“对象”的访问,被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行方法。

缺陷:若将一个大的方法申明为synchronized将会影响效率。

案例:将6.1中的案例修改正确

购票案例

public class MyThread {
    public static void main(String[] args) {
    Site site = new Site();
    Thread t1 = new Thread(site,"林枫");
    Thread t2= new Thread(site,"叶凡");
    Thread t3 = new Thread(site,"石昊");
    t1.start();
    t2.start();
    t3.start();
    }
}
class Site implements Runnable{
    //定义票的数量
    int count = 10;
    //定义购买了第几张票
    int sum = 0;

    @Override
    public void run() {

        while (true){
            synchronized (this){
            if (count==0)
                break;

                count--;
                sum++;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"购买了第"+sum+"张票,剩余"+count+"张票");

            }

        }
    }
}

运行结果:

银行取钱:给账户上锁

//不安全取钱
//两个人去银行取钱,账户
public class UnsafeBank {
    public static void main(String[] args) {
        //账户
        Account account = new Account(1000,"生活费");
        Drawing you = new Drawing(account,500,"你");
        Drawing son = new Drawing(account,1000,"儿子");
        you.start();
        son.start();
    }
}
//账户
class Account{
    int money; //余额
    String name;

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}
//银行:模拟取款
class Drawing extends Thread{
    Account account;//账户
    int drawingMoney;
    //现在手里还有多少钱
    int nowMoney;
    public Drawing(Account account,int drawingMoney,String name){
        super(name);
        this.account=account;
        this.drawingMoney=drawingMoney;

    }
    //取钱
    //synchronized默认锁this.
    @Override
    public  void run(){
        synchronized (account){
            //判断有没有钱
            if (account.money-drawingMoney<0){
                System.out.println(Thread.currentThread().getName()+"钱不够取不了");
                return;
            }
            try {//sleep放大问题的发生性
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //卡里的余额=余额-你取的钱
            account.money=account.money-drawingMoney;
            //你手里的钱
            nowMoney=nowMoney+drawingMoney;
            System.out.println(account.name+"余额为:"+account.money);
            //Thread.currentThread().getName()=this.getName()
            System.out.println(this.getName()+"手里的钱:"+nowMoney);
        }

    }

}

运行结果:

 线程不安全的集合

import java.util.ArrayList;
import java.util.List;

//线程不安全的集合
public class UnsafeList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized(list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

运行结果:

6.2.3 拓展:CopyOnWriteArrayList

 java.util.concurrent (缩写 JUC)并发编程包是专门为 Java 并发编程设计的,里面有一个关于List安全的集合

案例:

import java.util.concurrent.CopyOnWriteArrayList;
public class TestJUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

运行结果:

6.2.4 死锁

多个线程各自占有一些共享资源﹐并且互相等待其他线程占有的资源才能运行﹐而导致两个或者多个线程都在等待对方释放资源﹐都停止执行的情形.某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。

案例:

//死锁:多个线程抱着对方需要的资源,然后形成僵持。
public class TestLock {
    public static void main(String[] args) {
        Makeup g1 = new Makeup(0,"小白");
        Makeup g2 = new Makeup(1,"小灰");
        g1.start();
        g2.start();
    }
}
//口红
class Lipstick{

}
//镜子
class Mirror{

}
class Makeup extends Thread {
    //需要的资源只有一份,用static来保证只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
    int choice; //选择
    String girlName;//使用化妆品的人

    Makeup(int choice, String girlName) {
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        //化妆
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //化妆,互相持有对方的锁,就是需要拿到对方的资源
    private void makeup() throws InterruptedException {
        if (choice == 0) {
            synchronized (lipstick) {//获得口红的锁
                System.out.println(this.girlName + "获得口红的锁");
                Thread.sleep(1000);
               
                } synchronized (mirror) {//一秒钟后获得镜子的锁
                System.out.println(this.girlName + "获得镜子的锁");
            }
        } else {
            synchronized (mirror) {//获得镜子的锁
                System.out.println(this.girlName + "获得口红的锁");
                Thread.sleep(2000);
                
                }synchronized (lipstick) {//一秒钟后获得口红的锁
                System.out.println(this.girlName + "获得镜子的锁");
            }
        }
    }
}

运行结果:

 产生死锁的四个必要条件:
1.互斥条件:一个资源每次只能被一个进程使用。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生。

6.3 多线程通信

为了更好地理解线程间的通信,我们可以模拟这样的一种应用场景, 生产者将产品交给店员,而消费者从店员处取走产品,店员一次只能持有固定数量的产品,如果生产者生产了过多的产品,店员会叫生产者等一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。在这个应用场景中,我们可以得出线程间通信,指的一个线程完成了自己的任务时,要通知另外一个线程去完成另外一个任务。要完成线程间通信,就需要控制多个线程按照一定的顺序轮流执行。
在Object类中提供了wait (), notify (), notifyAll ()方法用于解决线程间的通信问题,由于Java中所有类都是Object类的子类或间接子类,因此任何类的实例对象都可以直接使用这些方法。接下来通过详细说明这几个方法的作用。

多线程通信常用方法
方法名称 功能描述
void wait() 使当前线程放弃同步锁并进入等待,直到其他线程进人此同步锁,调用notify ( )方法,或notifyAll ()方法唤醒该线程为止
vold notify ( ) 唤醒此同步锁上等待的第-一个调用wait () 方法的线程
void notifyAll () 唤醒此同步锁上调用wait()方法的所有线程


上表中列出了3个与线程通信相关的方法,其中wait ( )方法用于使当前线程进人等待状态,notify ( )和notifyAll ()方法用于唤醒当前处于等待状态的线程。需要注意的是,wait ()、 notify () 和notifyAlI () 这三个方法的调用者都应该是同步锁对象,如果这三个方法的调用者不是同步锁对象,Java 虚拟机就会抛出IllegalMonitorStateException 异常。

接下来通过使用wait()和notify()方法,完成上述的应用场景的类似的程序设计,就是要求个生存者生产-一个产品之后通知消费者消费, 消费者消费了一个产品,通知生存者生产一个产品。代码如下。
 

public  class Demo01{
    public static void main(String[] args) {
        Product p = new Product(); //产品
        //创建生产对象
        Producer producer = new Producer(p);
        //创建消费者
        Customer customer = new Customer(p);
        //调用start方法开启线程
        producer.start();
        customer.start();
    }

}
//产品类
class Product {
    String name; //名字
    double price;//价格
    boolean flag = false; //产品是否生产完毕的标识,默认情况是没有生产完成
}
//生产者
class Producer extends Thread{
    Product p; //产品
    public Producer(Product p){
        this.p = p;
    }
    @Override
    public void run(){
        int i = 0;
        while (true){
            synchronized (p){
                if (p.flag==false){
                    if (i%2==0){
                        p.name = "苹果";
                        p.price = 6.5;
                    }else {
                        p.name = "香蕉";
                        p.price = 2.0;
                    }
                    System.out.println("生产者生产出了:"+p.name+"价格是:"+p.price);
                    p.flag=true;
                    i++;
                    p.notify();//换新消费者去消费
                }else {
                    //已经生产完毕,等待消费者先去消费
                    try{
                        p.wait(); //生产者等待
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
//消费者
class Customer extends Thread{
    Product p;
    public Customer(Product p){
        this.p = p;
    }
    @Override
    public void run(){
        while (true){
            synchronized (p){
                if (p.flag==true){//产品已经生产完毕
                    System.out.println("消费者消费了:"+p.name+"价格:"+p.price);
                    p.flag = false;
                    p.notify();//唤醒生产者去生产
                }else{
                    //产品还没有生产,应该等待生产者先生产
                        try {
                            p.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();

                    }
                }
            }
        }
    }

}

7. 线程池

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
  • 好处:
    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理(....)
      • corePoolSize:核心池的大小maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后会终止
  • JDK 5.0起提供了线程池相关API: ExecutorService和Executors
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute(Runnable command)∶执行任务/命令,没有返回值,一般用来执行Runnable
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行
      Callable
    • void shutdown()∶关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

案例:

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

//测试线程池
public class TestPool {
    public static void main(String[] args) {
        //创建服务,创建线程池
        //newFixedThreadPool 参数为:线程池大小
        ExecutorService service = Executors.newFixedThreadPool(10);
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        //关闭连接
        service.shutdown();
    }
}
class MyThread implements Runnable{
    @Override
    public void run() {
            System.out.println(Thread.currentThread().getName());
    }
}

运行结果:

8. 练习

8.1 龟兔赛跑

public class Race implements Runnable{
    //胜利者
    private static String winner;
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
        //模拟兔子休息
        if(Thread.currentThread().getName().equals("兔子")&& i%10==0){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

            //判断比赛是否结束
            boolean flag = gameOver(i);
            //如果比赛结束了,就停止程序
            if (flag){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"-->跑了"+i+"步");
        }
    }
    //判断是否完成比赛
    private boolean gameOver(int steps){
        //判断是否有胜利者
        if(winner!=null){//存在胜利者
            return true;

        }{
            if (steps==100){
                winner = Thread.currentThread().getName();
                System.out.println("winner is "+winner);
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();
        new Thread(race,"兔子").start();
        new Thread(race,"乌龟").start();
    }
}

 8.2模拟多人爬山

要求:每个线程代表一个人,可设置每人爬山速度,没爬完一百米显示信息,爬到终点时给出相应提示。

package Demo10.demo01;

public class MyThread {
    public static void main(String[] args) {
        ClimbThread stripling = new ClimbThread("年轻人",500,1);
        ClimbThread oldMan = new ClimbThread("老年人",1500,1);
        System.out.println("*******开始爬山*******");
        stripling.start();
        oldMan.start();
    }

}
class ClimbThread extends Thread{
    private int time; //爬一百米时间
    private int num = 0; //爬多少个一百米
    public ClimbThread(String name,int time,int kilometer){
        super(name);
        this.time = time;
        this.num = kilometer * 1000/100;
    }
    public void run(){
        while (num>0){
            try {
                Thread.sleep(this.time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"爬完一百米!");
            num--;
        }
    }
}

8.3 模拟叫号看病

要求:某科室一天需看普通号20个,特需号10个特需号看病时间是普通号的2倍,开始时普通号和特需号并行叫号,叫到特需号的概率比普通号高,当普通号叫完第10号时,要求先看完全部特需号,再看普通号,使用多线程模拟这一过程

public class MyThread {
    public static void main(String[] args) {
        //开辟线程模拟特区号叫号
        Thread thread = new Thread(new CureThread());
        thread.setPriority(Thread.MAX_PRIORITY);
        thread.start();
        //主线程模拟普通医生叫号
        for (int i = 0; i < 20; i++) {
            System.out.println("普通号:"+(i+1)+"号病人在看病!");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i==9){
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
class CureThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("特需号:"+(i+1)+"号病人在看病!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

慢慢摸索的过程就是你学习的收获的过程。学习是一条令人时而喜极若狂、时而郁郁寡欢的道路,成长路上让我们携手共同进步。


 

猜你喜欢

转载自blog.csdn.net/m0_52896041/article/details/127466234
今日推荐