美文网首页
Java多线程

Java多线程

作者: 小王_min | 来源:发表于2020-10-10 14:25 被阅读0次

    Java 多线程编程

    Java给多线程编程提供了内置的支持。一个多线程程序包含两个或多个能并发运行的部分。程序的每一部分都称作一个线程,并且每个线程定义了一个独立的执行路径。

    多线程是多任务的一种特别的形式。多线程比多任务需要更小的开销。

    这里定义和线程相关的另一个术语:进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守候线程都结束运行后才能结束。

    多线程能满足程序员编写非常有效率的程序来达到充分利用CPU的目的,因为CPU的空闲时间能够保持在最低限度。


    一个线程的生命周期

    线程的生命周期包含出生状态、就绪状态、运行状态、等待状态、休眠状态、阻塞状态和死亡状态7种状态。当用户创建线程时线程处于出生状态;在用户调用start()方法后线程处于就绪状态;当线程得到资源后进入运行状态;当在运行态调用wait()方法时线程处于等待状态,此时必须调用notify()方法才能被唤醒,notifyAll()可以唤醒所有处于等待状态下的线程;当线程调用sleep()方法时会进入休眠状态;如果一个线程在运行状态下发出输入/输出请求,该线程将进入阻塞状态,在其等待输入/输出结束时线程进入就绪状态;当线程的run()方法执行完毕时线程进入死亡状态。(具体内容可参考操作系统)

    下图为线程生命周期状态图,可对照上述文字加以理解。

    image.png
    • 新建状态:当一个Thread类或其子类的对象被声明和创建时,新生的线程对象处于新建状态。

    • 就绪状态:处于新建状态的线程被start后,将进入线程队列等待CPU时间片,此时它已具备运行的条件,只是没分配到CPU资源。

    • 运行状态:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法得以运行。

    • 阻塞状态:在某种特殊情况下,被人为挂起或执行输入输出的IO操作是,让出CPU并临时中止自己的执行,进入阻塞状态。此状态主要分为:sleep、wait

    • 死亡状态:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。

    image.png
    • 新建状态: 一个新产生的线程从新状态开始了它的生命周期。它保持这个状态直到程序start这个线程。

    • 运行状态:当一个新状态的线程被start以后,线程就变成可运行状态,一个线程在此状态下被认为是开始执行其任务

    • 就绪状态:当一个线程等待另外一个线程执行一个任务的时候,该线程就进入就绪状态。当另一个线程给就绪状态的线程发送信号时,该线程才重新切换到运行状态。

    • 休眠状态: 由于一个线程的时间片用完了,该线程从运行状态进入休眠状态。当时间间隔到期或者等待的事件发生了,该状态的线程切换到运行状态。

    • 终止状态: 一个运行状态的线程完成任务或者其他终止条件发生,该线程就切换到终止状态。


    线程的优先级

    每一个Java线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

    Java线程优先级:(1-10)

    • MIN_PRIORITY:1

    • MAX_PRIORITY:10

    • ORM_PRIORITY:5(默认)

    关于优先级的方法:

    getPriority():返回线程优先级的值

    setPriority():改变线程的优先级

    说明:

    • 线程创建是继承父线程的优先级

    • 低优先级只是获取调度的概率低,并非一定是在高优先级线程之后才被调用,这是有jvm所决定的

    测试优先级的实例

    package com.briup.bigdata.zookeeper.code.thread;
    
    import java.util.concurrent.CountDownLatch;
    
    public class TestThreadPriority {
    
        public static void main(String[] args) throws InterruptedException {
    
            CountDownLatch cdl = new CountDownLatch(2);
    
            Thread thread1 = new Thread(() -> runFun(cdl));
    
            Thread thread2 = new Thread(() -> runFun(cdl));
    
            thread1.setPriority(Thread.MAX_PRIORITY);
    
            thread1.start();
    
            cdl.countDown();
    
            thread2.setPriority(Thread.MIN_PRIORITY);
    
            thread2.start();
    
            cdl.countDown();
    
        }
    
        private static void runFun(CountDownLatch cdl){
    
            try {
    
                cdl.await();
    
                for (int i = 0; i < 10; i++) {
    
                    System.out.println(Thread.currentThread().getName() + ":" + i);
    
                }
    
            } catch (InterruptedException e) {
    
                e.printStackTrace();
    
            }
    
        }
    
    }
    

    运行结果:

    image.png

    创建一个线程

    Java提供了三种创建线程方法:

    • 通过实现Runnable接口;

    • 通过继承Thread类本身;

    • 通过 Callable 和 Future 创建线程。


    通过实现Runnable接口来创建线程

    创建一个线程,最简单的方法是创建一个实现Runnable接口的类。

    为了实现Runnable,一个类只需要执行一个方法调用run(),声明如下:

    public void run()

    你可以重写该方法,重要的是理解的run()可以调用其他方法,使用其他类,并声明变量,就像主线程一样。

    在创建一个实现Runnable接口的类之后,你可以在类中实例化一个线程对象。

    Thread定义了几个构造方法,下面的这个是我们经常使用的:

    Thread(Runnable threadOb,String threadName);

    这里,threadOb 是一个实现Runnable 接口的类的实例,并且 threadName指定新线程的名字。

    新线程创建之后,你调用它的start()方法它才会运行。

    void start();

    实例

    下面是一个创建线程并开始让它执行的实例:

    package com.briup.bigdata.zookeeper.code.thread;
    
    public class MyThread01 implements Runnable {
    
        public static void main(String[] args) throws InterruptedException {
    
            new Thread(new MyThread01()).start();
    
            try {
    
                for(int i = 10; i > 0; i--) {
    
                    System.out.println(Thread.currentThread().getName() + ":" + i);
    
                    Thread.sleep(100);
    
                }
    
            } catch (InterruptedException e) {
    
                System.out.println(Thread.currentThread().getName() + " was interrupted.");
    
            }
    
            System.out.println("Exiting " + Thread.currentThread().getName() + " thread.");
    
        }
    
        @Override
    
        public void run() {
    
            try {
    
                for (int i = 0; i < 10; i++) {
    
                    System.out.println(Thread.currentThread().getName() + ":" + i);
    
                    Thread.sleep(1000);
    
                }
    
            } catch (InterruptedException e) {
    
                    System.out.println(Thread.currentThread().getName() + " was interrupted.");
    
            }
    
            System.out.println("Exiting " + Thread.currentThread().getName() + " thread.");
    
        }
    
    }
    

    运行结果:

    image.png

    通过继承Thread来创建线程

    创建一个线程的第二种方法是创建一个新的类,该类继承Thread类,然后创建一个该类的实例。

    继承类必须重写run()方法,该方法是新线程的入口点。它也必须调用start()方法才能执行。该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。

    实例

    package com.briup.bigdata.zookeeper.code.thread;
    
    public class MyThread02 extends Thread {
    
        public static void main(String[] args) {
    
            new MyThread02().start();
    
            try {
    
                for (int i = 0; i < 5; i++) {
    
                    System.out.println(Thread.currentThread().getName() + ":" + i);
    
                    Thread.sleep(1000);
    
                }
    
            } catch (InterruptedException e) {
    
                System.out.println(Thread.currentThread().getName() + " was interrupted.");
    
            }
    
            System.out.println("Exiting " + Thread.currentThread().getName() + " thread.");
    
        }
    
        @Override
    
        public void run() {
    
            try {
    
                for (int i = 0; i < 5; i++) {
    
                    System.out.println(Thread.currentThread().getName() + ":" + i);
    
                    Thread.sleep(1000);
    
                }
    
            } catch (InterruptedException e) {
    
                System.out.println(Thread.currentThread().getName() + " was interrupted.");
    
            }
    
            System.out.println("Exiting " + Thread.currentThread().getName() + " thread.");
    
        }
    
    }
    

    运行结果:

    image.png

    Thread 方法

    下表列出了Thread类的一些重要方法:

    序号 方法 描述
    1 public void start() 使该线程开始执行;java虚拟机调用该线程的 run 方法。
    2 public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则该方法不执行任何操作返回。
    3 public final void setName(String name) 改变线程名称,使之与参数 name 相同。
    4 public final void setPriority(int priority) 更改线程的优先级。
    5 public final void setDaemon(boolean on) 将该线程标记为守护线程,主线程结束,守护线程也跟着结束。
    6 public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。
    7 public void interrupt() 中断线程。
    8 public final boolean isAlive() 测试线程是否处于活动状态。
    9 public final void stop() 停止线程,已过时

    测试线程是否处于活动状态。 上述方法是被Thread对象调用的。下面的方法是Thread类的静态方法。

    序号 方法 描述
    1 public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
    2 public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
    3 public static boolean holdsLock(Object x) 仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
    4 public static Thread currentThread() 返回对当前正在执行的线程对象的引用。
    5 public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。

    实例

    如下的ThreadClassDemo 程序演示了Thread类的一些方法:

    package com.briup.bigdata.zookeeper.code.thread;
    
    public class ThreadClassDemo {
    
        public static void main(String[] args) throws InterruptedException {
    
            Thread thread1 = new Thread(() -> {
    
                try {
    
                    for (int i = 0; i < 5; i++) {
    
                        // yield
    
                        if (i == 3) Thread.yield();
    
                        System.out.println(Thread.currentThread().getName() + " : " + i);
    
                        Thread.sleep(1000);
    
                    }
    
                } catch (InterruptedException e){
    
                    System.out.println(Thread.currentThread().getName() + " was interrupted.");
    
                }
    
                System.out.println(Thread.currentThread().getName() + " was exit.");
    
            }, "thread1");
    
            thread1.start();
    
            for (int i = 0; i < 5; i++) {
    
                System.out.println(Thread.currentThread().getName() + " : " + i);
    
            }
    
            Thread joinThread = new Thread(() -> {
    
                for (int i = 0; i < 5; i++) {
    
                    System.out.println(Thread.currentThread().getName() + ":" + i);
    
                }
    
            }, "joinThread");
    
            joinThread.start();
    
            Thread.sleep(3000);
    
            for (int i = 5; i < 10; i++) {
    
                System.out.println(Thread.currentThread().getName() + " : " + i);
    
                Thread.yield();
    
            }
    
        }
    
    }
    

    运行结果:

    image.png

    通过 Callable 和 Future 创建线程

    • 1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

    • 2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

    • 3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

    • 4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

    实例:

    package com.briup.bigdata.zookeeper.code.thread;
    
    import java.util.concurrent.Callable;
    
    import java.util.concurrent.ExecutionException;
    
    import java.util.concurrent.FutureTask;
    
    /**
    
    * 实现Callable接口的类中的call方法是有返回值的
    
    */
    
    public class MyThread03 implements Callable<Integer> {
    
        public static void main(String[] args) {
    
            MyThread03 myThread = new MyThread03();
    
            FutureTask<Integer> ft = new FutureTask<>(myThread);
    
            for(int i = 0;i < 100;i++) {
    
                System.out.println(Thread.currentThread().getName()+":"+i);
    
                if(i==20){
    
                    Thread.yield();
    
                    new Thread(ft,"有返回值的线程").start();
    
                }
    
            }
    
            try {
    
                System.out.println("子线程的返回值:"+ft.get());
    
            } catch (InterruptedException e) {
    
                e.printStackTrace();
    
            } catch (ExecutionException e) {
    
                e.printStackTrace();
    
            }
    
        }
    
        @Override
    
        public Integer call() throws Exception {
    
            int i = 0;
    
            for (; i < 100; i++) {
    
                System.out.println(Thread.currentThread().getName() + ":" + i);
    
            }
    
            return i;
    
        }
    
    }
    

    运行结果:


    创建线程的三种方式的对比

    • 1. 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。

    • 2. 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。


    线程的几个主要概念

    在多线程编程时,你需要了解以下几个概念:

    • 线程同步

    • 线程间通信

    • 线程死锁

    • 线程控制:挂起、停止和恢复


    多线程的使用

    有效利用多线程的关键是理解程序是并发执行而不是串行执行的。例如:程序中有两个子系统需要并发执行,这时候就需要利用多线程编程。

    通过对多线程的使用,可以编写出非常高效的程序。不过请注意,如果你创建太多的线程,程序执行的效率实际上是降低了,而不是提升了。

    请记住,上下文的切换开销也很重要,如果你创建了太多的线程,CPU花费在上下文的切换的时间将多于执行程序的时间!


    线程同步

    多线程中,可能会发生两个线程抢占资源的问题,例如两个人同时过一个独木桥。所以Java提供线程同步机制来防止这些资源访问的冲突。

    问题实例:火车售票系统

    package com.briup.bigdata.zookeeper.code.thread.example;
    
    public class TicketSaleSystem implements Runnable {
    
        private int ticket = 10;
    
        @Override
    
        public void run() {
    
            for (int i = 0; i < 10; i++) {
    
                if (this.ticket > 0) {
    
                    if ("票贩子A".equals(Thread.currentThread().getName())) {
    
                        try {
    
                            Thread.sleep(10);
    
                        } catch (InterruptedException e) {
    
                            e.printStackTrace();
    
                        }
    
                    }
    
                    System.out.println(Thread.currentThread().getName() + "卖出 1 张票," + " 余量:" + --this.ticket);
    
                }
    
            }
    
        }
    
        public static void main(String[] args) {
    
            TicketSaleSystem tss = new TicketSaleSystem();
    
            Thread t1 = new Thread(tss, "票贩子A");
    
            Thread t2 = new Thread(tss, "票贩子B");
    
            Thread t3 = new Thread(tss, "票贩子C");
    
            t1.start();
    
            t2.start();
    
            t3.start();
    
        }
    
    }
    

    运行结果:

    image.png

    线程同步的几种方法:

    • 同步代码块(synchronized):synchronized(同步监视器-任意类的对象){}

    • 同步方法:public void synchronized fun(){}

    • 锁机制(Lock),JDK1.5开始

    • 信号量

    同步代码块实例:

    1.实现Runnable接口

    package com.briup.bigdata.zookeeper.code.thread.example;
    
    public class SynchronizedTicketSaleSystem01 implements Runnable {
    
        private int ticket = 10;
    
        @Override
    
        public void run() {
    
            synchronized (SynchronizedTicketSaleSystem01.class) {
    
                for (int i = 0; i < 10; i++) {
    
                    if (this.ticket > 0) {
    
                        if ("票贩子A".equals(Thread.currentThread().getName())) {
    
                            try {
    
                                Thread.sleep(10);
    
                            } catch (InterruptedException e) {
    
                                e.printStackTrace();
    
                            }
    
                        }
    
                        System.out.println(Thread.currentThread().getName() + "卖出 1 张票," + " 余量:" + --this.ticket);
    
                    }
    
                }
    
            }
    
        }
    
        public static void main(String[] args) {
    
            SynchronizedTicketSaleSystem01 tss = new SynchronizedTicketSaleSystem01();
    
            Thread t1 = new Thread(tss, "票贩子A");
    
            Thread t2 = new Thread(tss, "票贩子B");
    
            Thread t3 = new Thread(tss, "票贩子C");
    
            t1.start();
    
            t2.start();
    
            t3.start();
    
        }
    
    }
    

    运行结果:

    image.png

    2.继承Thread类

    package com.briup.bigdata.zookeeper.code.thread.example;
    
    public class SynchronizedTicketSaleSystem02 extends Thread {
    
        private static int ticket = 10;
    
        @Override
    
        public void run() {
    
            synchronized (SynchronizedTicketSaleSystem02.class) {
    
                for (int i = 0; i < 10; i++) {
    
                    if (this.ticket > 0) {
    
                        if ("票贩子A".equals(Thread.currentThread().getName())) {
    
                            try {
    
                                Thread.sleep(10);
    
                            } catch (InterruptedException e) {
    
                                e.printStackTrace();
    
                            }
    
                        }
    
                        System.out.println(this.getName() + "卖出 1 张票," + " 余量:" + --ticket);
    
                    }
    
                }
    
            }
    
        }
    
        public static void main(String[] args) {
    
            SynchronizedTicketSaleSystem02 t1 = new SynchronizedTicketSaleSystem02();
    
            t1.setName("票贩子A");
    
            SynchronizedTicketSaleSystem02 t2 = new SynchronizedTicketSaleSystem02();
    
            t2.setName("票贩子B");
    
            SynchronizedTicketSaleSystem02 t3 = new SynchronizedTicketSaleSystem02();
    
            t3.setName("票贩子C");
    
            t1.start();
    
            t2.start();
    
            t3.start();
    
        }
    
    }
    

    运行结果:

    image.png

    说明:

    • 在1中的同步代码块的同步监视器(锁)可以采用this,因为在main线程中只new了一个实现runnable接口的类,所以符合锁的要求(唯一性)

    • 在2中的同步代码块的同步监视器(锁)可以采用this,因为在main线程中new了多个继承Thread的类,所以不符合锁的要求(唯一性)

    • 在2中定义的ticket需要定义为static的,提升为类属性,不然就相当于每个线程拥有100张票,与题意不符。

    同步代码块实例:

    1.实现Runnable接口

    package com.briup.bigdata.zookeeper.code.thread.example;
    
    public class SynchronizedTicketSaleSystem03 implements Runnable {
    
        private int ticket = 10;
    
        @Override
    
        public void run() {
    
            fun();
    
        }
    
        // 同步方法
    
        private synchronized void fun(){
    
            for (int i = 0; i < 10; i++) {
    
                if (this.ticket > 0) {
    
                    if ("票贩子A".equals(Thread.currentThread().getName())) {
    
                        try {
    
                            Thread.sleep(10);
    
                        } catch (InterruptedException e) {
    
                            e.printStackTrace();
    
                        }
    
                    }
    
                    System.out.println(Thread.currentThread().getName() + "卖出 1 张票," + " 余量:" + --this.ticket);
    
                }
    
            }
    
        }
    
        public static void main(String[] args) {
    
            SynchronizedTicketSaleSystem03 tss = new SynchronizedTicketSaleSystem03();
    
            Thread t1 = new Thread(tss, "票贩子A");
    
            Thread t2 = new Thread(tss, "票贩子B");
    
            Thread t3 = new Thread(tss, "票贩子C");
    
            t1.start();
    
            t2.start();
    
            t3.start();
    
        }
    
    }
    

    运行结果:

    image.png

    2.继承Thread类

    package com.briup.bigdata.zookeeper.code.thread.example;
    
    public class SynchronizedTicketSaleSystem04 extends Thread {
    
        private static int ticket = 10;
    
        @Override
    
        public void run() {
    
            fun();
    
        }
    
        public static synchronized void fun(){
    
            for (int i = 0; i < 10; i++) {
    
                if (ticket > 0) {
    
                    if ("票贩子A".equals(Thread.currentThread().getName())) {
    
                        try {
    
                            Thread.sleep(10);
    
                        } catch (InterruptedException e) {
    
                            e.printStackTrace();
    
                        }
    
                    }
    
                    System.out.println(Thread.currentThread().getName() + "卖出 1 张票," + " 余量:" + --ticket);
    
                }
    
            }
    
        }
    
        public static void main(String[] args) {
    
            SynchronizedTicketSaleSystem04 t1 = new SynchronizedTicketSaleSystem04();
    
            t1.setName("票贩子A");
    
            SynchronizedTicketSaleSystem04 t2 = new SynchronizedTicketSaleSystem04();
    
            t2.setName("票贩子B");
    
            SynchronizedTicketSaleSystem04 t3 = new SynchronizedTicketSaleSystem04();
    
            t3.setName("票贩子C");
    
            t1.start();
    
            t2.start();
    
            t3.start();
    
        }
    
    }
    

    运行结果:

    image.png

    说明:

    • 对于这个方法中的所有代码都需要同步(处于临界区),则可以将整个方法同步

    • 对于同步方法,本质上与同步代码块一致,知识隐式地设置了同步监听器(锁)

    • 非静态同步方法的锁为this,静态同步方法的锁为class。如果是继承Thread的方法实现的多线程则需要将方法提升为静态方法。

    锁实例:Lock完全用Java写成,在java这个层面是无关JVM实现的(java1.5)

    package com.briup.bigdata.zookeeper.code.thread.example;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ThreadLock {
    
        private static int ticket = 10;
    
        private static ReentrantLock lock = new ReentrantLock();
    
        public static void main(String[] args) {
    
            Runnable r1 = () -> {
    
                while (true){
    
                    try {
    
                        lock.lock();
    
                        if (ticket > 0){
    
                            try {
    
                                Thread.sleep(10);
    
                            }catch (InterruptedException e) {
    
                                e.printStackTrace();
    
                            }
    
                            System.out.println(Thread.currentThread().getName() + ":售票,票余量:" + --ticket);
    
                        }else break;
    
                    }finally {
    
                        lock.unlock();
    
                    }
    
                }
    
            };
    
            new Thread(r1).start();
    
            new Thread(r1).start();
    
        }
    
    }
    

    synchronized和lock锁的区别:

    • Lock是一个接口,而synchronized是关键字。

    • synchronized会自动释放锁,而Lock必须手动释放锁。

    • Lock可以让等待锁的线程响应中断,而synchronized不会,线程会一直等待下去。

    • 通过Lock可以知道线程有没有拿到锁,而synchronized不能。

    • Lock能提高多个线程读操作的效率。

    • synchronized能锁住类、方法和代码块,而Lock是块范围内的


    线程死锁

    不同线程分别占用对方所需资源不主动释放,都在等待对方释放自己所需的同步资源,就会形成线程死锁。

    实例:

    package com.briup.bigdata.zookeeper.code.thread.example;
    
    import java.util.Date;
    
    public class ThreadDeadLock {
    
        private static Object o1 = new Object();
    
        private static Object o2 = new Object();
    
        public static void main(String[] args) {
    
            Runnable lockA = () -> {
    
                try {
    
                    System.out.println(new Date().toString() + " LockA 开始执行");
    
                    while (true){
    
                        synchronized (ThreadDeadLock.o1){
    
                            System.out.println(new Date().toString() + " LockA 锁住 o1");
    
                            Thread.sleep(3000);
    
                            synchronized (ThreadDeadLock.o2){
    
                                System.out.println(new Date().toString() + " LockA 锁住 o2");
    
                            }
    
                        }
    
                    }
    
                } catch (InterruptedException e) {
    
                    e.printStackTrace();
    
                }
    
            };
    
            Runnable lockB = () -> {
    
                try {
    
                    System.out.println(new Date().toString() + " LockB 开始执行");
    
                    while (true){
    
                        synchronized (ThreadDeadLock.o2){
    
                            System.out.println(new Date().toString() + " LockB 锁住 o2");
    
                            Thread.sleep(3000);
    
                            synchronized (ThreadDeadLock.o1){
    
                                System.out.println(new Date().toString() + " LockB 锁住 o1");
    
                            }
    
                        }
    
                    }
    
                } catch (InterruptedException e) {
    
                    e.printStackTrace();
    
                }
    
            };
    
            new Thread(lockA).start();
    
            new Thread(lockB).start();
    
        }
    
    }
    

    运行结果:

    image.png

    解决方法:

    • 专门的算法、原则

    • 尽量减少同步资源的定义

    • 尽量避免嵌套同步

    相关文章

      网友评论

          本文标题:Java多线程

          本文链接:https://www.haomeiwen.com/subject/ypzdpktx.html