Java的第十三篇文章——JAVA多线程

目录

学习目标

1. 线程的基本概念

1.1 进程

1.2 线程

2. Java实现线程程序

2.1 java.lang.Thread类

2.2 线程的内存图

2.3 Thread类的方法

3. Java实现线程程序

3.1 java.lang.Runnable接口

3.2 实现接口的好处

4. 线程安全

4.1 售票例子

4.2 同步代码块

4.3 同步方法

5. 死锁

6. JDK5新特性Lock锁

7. 生产者与消费者例题

7.1 安全问题产生

7.2 线程方法sleep和wait的区别

7.3 生产者和消费者案例的性能问题

7.4 Lock接口深入

7.5 生产者与消费者改进薇Lock接口

7.6 Lock锁的实现原理

8. 单例设计模式 

8.1 单例模式

8.2 懒汉式的安全问题

8.2 关键字 volatile

9. 线程池ThreadPool

9.1 Executors 类

9.2 Callable 接口

10. ConcurrentHashMap(需进一步了解)

11. 线程的状态图——生命周期


学习目标

  • 线程概念

  • Java实现多线程程序一

  • Thread类的方法

  • Java实现多线程程序二

  • 线程安全问题

  • 同步synchronized使用

  • 锁对象的选择

  • 死锁案例

  • 生产者与消费者

  • JDK5特性JUC

  • 单例模式

  • 关键字volatile

  • 线程池

  • ConcurrentHashMap

1. 线程的基本概念

1.1 进程

任何的软件存储在磁盘中,运行软件的时候,OS(操作系统)使用IO技术,将磁盘中的软件的文件加载到内存,程序才能运行。

进程的概念:应用程序(typerpa、word、IDEA)运行的时候进入到内存,程序在内存中占用的内存空间(进程)。

1.2 线程

线程(Thread):在内存和CPU之间,建立一条连接通路,CPU可以到内存中取出数据进行计算,这个连接的通路,就是线程

一个内存资源:一个独立的进程,进程中可以开启多个线程 (多条通路) 。

并发:同一个时刻多个线程同时操作了同一个数据。

并行:同一个时刻多个线程同时执行不同的程序。

2. Java实现线程程序

今天之前的所有程序都有一个共性:main启动之后,一条线走到底 (单线程)

2.1 java.lang.Thread类

第一种方法:继承的方式实现多线程。

一切都是对象,线程也是对象,Thread类是线程对象的描述类

  • 实现线程程序的步骤:

    • 定义类继承Thread

    • 子类重写方法run

    • 创建子类对象

    • 调用子类对象的方法start()启动线程

//- 定义类继承Thread
//- 子类重写方法run
public class SubThread extends Thread {
    public void run(){
        for(int x = 0 ; x < 50 ;x++)
            System.out.println("run..."+x);
    }
}
public static void main(String[] args) {
    //创建线程程序
    SubThread subThread = new SubThread();
    //调用子类对象的方法start()启动线程
    //启动线程,JVM调用方法run
    subThread.start();
    for(int x = 0 ; x < 50 ;x++)
    	System.out.println("main..."+x);
}

(1)调用子类对象的方法start()启动线程。我们只需要调用start()方法,run()方法不是我们自己调用的。
(2)启动线程,JVM调用方法run。

2.2 线程的内存图

Java的第十三篇文章——JAVA多线程_第1张图片

2.3 Thread类的方法

  • Thread类的方法 getName()返回线程的名字,返回值是String类型

public class ThreadName extends Thread {
    public void run (){
        System.out.println("线程名字:: "+ super.getName());
    }
}
   public static void main(String[] args) {
        ThreadName threadName = new ThreadName();
        //threadName.setName("旺财");
        threadName.start();

        ThreadName threadName1 = new ThreadName();
        //threadName1.setName("小强");
        threadName1.start();
    }
  • Thread类静态方法 : Thread currentThread()

    • 静态调用,作用是返回当前的线程对象

    • "当前" , 当今皇上——本地主机

//获取当前线程对象,拿到运行main方法的线程对象
Thread thread =  Thread.currentThread();
System.out.println("name::"+thread.getName());
  • Thread类的方法 join()

    • 解释:执行join()方法的线程,他不结束,其它线程运行不了

    public static void main(String[] args) throws InterruptedException {
        JoinThread t0 = new JoinThread();
        JoinThread t1 = new JoinThread();

        t0.start();
        t0.join();
        t1.start();
    }
  • Thread类的方法 static yield()

    • 线程让步,线程把执行权让出

    public void run() {
        for(int x = 0 ; x < 50 ;x++){
            Thread.yield();
            System.out.println(Thread.currentThread().getName()+"x.."+x);
        }
    }

3. Java实现线程程序

3.1 java.lang.Runnable接口

第二种实现多线程的方法:用接口来实现

  • 实现线程程序的步骤 :

    • 定义类实现接口

    • 重写接口的抽象方法run()

    • 创建Thread类对象

      • Thread类构造方法中,传递Runnable接口的实现类对象

    • 调用Thread对象方法start()启动线程

//- 定义类实现接口
// - 重写接口的抽象方法run()
public class SubRunnable implements Runnable{
    @Override
    public void run() {
        for(int x = 0 ; x < 50 ;x++){
            System.out.println(Thread.currentThread().getName()+"x.."+x);
        }
    }
}
    public static void main(String[] args) {
        //创建接口实现类对象
        Runnable r = new SubRunnable();
        //创建Thread对象,构造方法传递接口实现类
        Thread t0 = new Thread(r);
        t0.start();

        for(int x = 0 ; x < 50 ;x++){
            System.out.println(Thread.currentThread().getName()+"x.."+x);
        }
    }

3.2 实现接口的好处

接口实现好处是设计上的分离效果:线程要执行的任务和线程对象本身是分离的

继承Thread重写方法run():Thread是线程对象,run()是线程要执行的任务。

实现Runnable接口:方法run在实现类,和线程无关,创建Thread类传递接口的实现类对象,线程的任务和Thread没有联系,,解开耦合性。

4. 线程安全

出现线程安全的问题需要一个前提:多个线程同时操作同一个资源。

线程执行调用方法run,同一个资源是堆内存的。

4.1 售票例子

火车票的票源是固定的,购买渠道在火车站买,n多个窗口。

public class Ticket implements Runnable {
    //定义票源
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                try {
                    Thread.sleep(10);
                } catch (Exception e) {}
                System.out.println(Thread.currentThread().getName() + " 出售第" + tickets + "张");
                tickets--;
            }else{
                break;
            }
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        //这个属于一个种子,被三个线程共享
        Ticket ticket = new Ticket();
        //创建3个窗口,3个线程
        Thread t0 = new Thread(ticket);
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);

        t0.start();
        t1.start();
        t2.start();

    }
}

Java的第十三篇文章——JAVA多线程_第2张图片

我们可以发现发生了线程安全问题:多个线程抢同一张票源。

 解决线程的安全问题:当一个线程没有完成全部操作的时候,其它线程不能操作。

Java的第十三篇文章——JAVA多线程_第3张图片

4.2 同步代码块

同步代码块可以解决线程安全问题:格式 synchronized关键字。

synchronized(任意对象){
    //线程操作的共享资源
}

任意对象:在同步中这个对象称为对象锁,简称锁,官方的稳定称为对象监视器。

同步代码块,如何保证线程的安全性。

  • 同步代码块的执行原理:关键点就是对象锁

    • 线程执行到同步,判断锁是否存在

      • 如果锁存在,获取到锁,进入到同步中执行

      • 执行完毕,线程出去同步代码块,将锁对象归还

    • 线程执行到同步,判断锁所否存在

      • 如果锁不存在,线程只能在同步代码块这里等待,锁的到来

Java的第十三篇文章——JAVA多线程_第4张图片

使用同步:线程要先判断锁,然后获取锁,出去同步要释放锁,增加了许多步骤,因此线程安全运行速度慢,牺牲性能,不能牺牲数据安全。

4.3 同步方法

当一个方法中,所有代码都是线程操作的共享内容,可以在方法的定义上添加同步的关键字 synchronized ,同步的方法,或者称为同步的函数。

  • 同步方法中有对象锁吗?有且是this对象

  • 静态同步方法中有对象锁吗?锁对象是本类.class属性。 这个属性表示这个类的class文件的对象。

    @Override
    public void run() {
        while (true)
          sale();
    }

private static synchronized void sale(){
    //  synchronized (Ticket.class) {
    if (tickets > 0) {
    try {
        Thread.sleep(20);//线程休眠,暂停执行
        } catch (Exception ex) {
    }
    System.out.println(Thread.currentThread().getName() + " 出售第" + tickets + "张");
    tickets--;
    }
//  }
}

5. 死锁

死锁程序:多个线程同时争夺同一个锁资源,出现程序的假死现象。

面试点:考察开发人员是否充分理解同步代码的执行原理

同步代码块:线程判断锁,获取锁,释放锁,不出代码,锁不释放

Java的第十三篇文章——JAVA多线程_第5张图片

  • 死锁代码

/**
 * 实现死锁程序
 */
public class ThreadDeadLock implements Runnable{

    private boolean flag ;

    public ThreadDeadLock(boolean flag){
        this.flag = flag;
    }

    @Override
    public void run() {
        while (true){
            //同步代码块的嵌套
            if (flag){
                //先进入A锁同步
                synchronized (LockA.lockA){
                    System.out.println("线程获取A锁");
                    //在进入另一个同步B锁
                    synchronized (LockB.lockB){
                        System.out.println("线程获取B锁");
                    }
                }
            }else {
                //先进入B锁同步
                synchronized (LockB.lockB){
                    System.out.println("线程获取B锁");
                    //再进入另一个同步锁A锁
                    synchronized (LockA.lockA){
                        System.out.println("线程获取A锁");
                    }
                }
            }
        }
    }
}
public class LockA {
    public static LockA lockA = new LockA();
}
public class LockB {
    public static LockB lockB = new LockB();
}
    public static void main(String[] args) {
        ThreadDeadLock threadDeadLock = new ThreadDeadLock(true);
        ThreadDeadLock threadDeadLock2 = new ThreadDeadLock(false);

        new Thread(threadDeadLock).start();
        new Thread(threadDeadLock2).start();
    }

6. JDK5新特性Lock锁

JDK5新的特性:java.util.concurrent.locks包。定义了接口Lock。

Lock接口替代了synchronized,可以更加灵活

  • Lock接口的方法

    • void lock() 获取锁

    • void unlock()释放锁

  • Lock接口的实现类ReentrantLock

/**
 *  优化为juc包的接口Lock
 */
public class Ticket implements Runnable {

    //定义票源
    private  int tickets = 100;
    //获取Lock接口的实现类对象
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true)
          sale();
    }

    private void sale(){
        //获取锁
        lock.lock();
        if (tickets > 0) {
            try {
                Thread.sleep(20);//线程休眠,暂停执行
            } catch (Exception ex) {
            }
            System.out.println(Thread.currentThread().getName() + " 出售第" + tickets + "张");
            tickets--;
        }
        //释放锁
        lock.unlock();
    }
}

7. 生产者与消费者例题

创建2个线程,一个线程表示生产者,另一个线程表示消费者

Java的第十三篇文章——JAVA多线程_第6张图片

/**
 * 定义资源对象
 *   成员 : 产生商品的计数器
 *          标志位
 */
public class Resource {
    int count ;
    boolean flag ;
}
/**
 * 生产者线程
 *   资源对象中的变量++
 */
public class Produce implements Runnable{

    private Resource r ;

    public Produce(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true){
            synchronized (r) {
                //判断标志位,是否允许生产
                //flag是true,生产完成,等待消费
                if (r.flag )
                    //无限等待
                   try{ r.wait();
                   }catch (Exception ex){}
                r.count++;
                System.out.println("生产第" + r.count + "个");
                //修改标志位,已经生产了,需要消费
                r.flag = true;
                //唤醒消费者线程
                r.notify();
            }
        }
    }
}
/**
 * 消费者线程
 *   资源对象中的变量输出打印
 */
public class Customer implements Runnable{
    private Resource r ;

    public Customer(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true){
            synchronized (r) {
                //是否要消费,判断标志位 ,允许消费才能执行
                if (!r.flag )
                    //消费完成,不能再次消费,等待生产
                    try{r.wait();}catch (Exception ex){}
                System.out.println("消费第" + r.count);
                    //消费完成后,修改标志位,变成已经消费
                r.flag = false;
                //唤醒生产线程
                r.notify();
            }
        }
    }
}
public static void main(String[] args) {
    Resource r = new Resource();
    //接口实现类,生产的,消费的
    Produce produce = new Produce(r);
    Customer customer = new Customer(r);
    //创建线程
    new Thread(produce).start();
    new Thread(customer).start();
}
  • 线程通信的方法 wait() notify()

    • 方法的调用必须写在同步中

    • 调用者必须是作为锁的对象

    • wait(),notify()为什么要定义在Object类

      • 同步中的锁,是任意对象,任何类都继承Object

结果如下:

改为使用同步方法实现该功能

public class Resource {
    int count;
    boolean flag = false;

    public synchronized void getCustomer() {
        while (true) {
            if (!this.flag) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                System.out.println("消费者消费了第:" + this.count);
                this.flag = false;
            }
            this.notify();
        }
    }

    public synchronized void getProduce() {
        while (true) {
            if (this.flag) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                this.count++;
                System.out.println("生产者生产了第:" + this.count + "个");
                flag = true;
            }
            this.notify();
        }
    }
}
public class Customer implements Runnable {
    private Resource r;

    public Customer(Resource r) {
        this.r = r;
    }


    @Override
    public void run() {
        r.getCustomer();
    }
}
public class Produce implements Runnable {
    private Resource r;

    public Produce(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        r.getProduce();
    }
}

7.1 安全问题产生

  • 线程本身就是一个新创建的方法栈内存 (CPU进来读取数据)

  • 线程的notify(),唤醒第一个等待的线程

    • 解决办法:全部唤醒用 notifyAll() 方法

  • 被唤醒线程,已经进行过if判断,一旦醒来继续执行

    • 线程被唤醒后,不能立刻就执行,再次判断标志位,利用循环

    • while(标志位) 标志位是true,永远也出不去

public class Resource {
    int count;
    boolean flag = false;

    public synchronized void getCustomer(){
        while(!flag)
            try{
                this.wait();
            }catch (Exception ex){
                ex.printStackTrace();
            }
        System.out.println(Thread.currentThread().getName()+"消费者消费了第:" + this.count);
        this.flag = false;
        this.notifyAll();
    }

    public synchronized void getProduce(){
        while(flag)
            try{
                this.wait();
            }catch (Exception ex){
                ex.printStackTrace();
            }
        this.count++;
        System.out.println(Thread.currentThread().getName()+"生产者生产了第:" + this.count + "个");
        this.flag = true;
        this.notifyAll();
    }
public class Produce implements Runnable {
    private Resource r;

    public Produce(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            r.getProduce();
        }
    }
}
public class Customer implements Runnable {
    private Resource r;

    public Customer(Resource r) {
        this.r = r;
    }


    @Override
    public void run() {
        while(true){
            r.getCustomer();
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Resource resource = new Resource();
        Customer customer = new Customer(resource);
        Produce produce = new Produce(resource);

        new Thread(customer).start();
        new Thread(customer).start();
        new Thread(customer).start();
        new Thread(produce).start();
        new Thread(produce).start();
        new Thread(produce).start();
    }
}

结果如下: 

Java的第十三篇文章——JAVA多线程_第7张图片

Java的第十三篇文章——JAVA多线程_第8张图片

7.2 线程方法sleep和wait的区别

  • sleep在休眠的过程中,同步锁不会丢失,不释放

  • wait()等待的时候,发布监视器的所属权,释放锁。唤醒后要重新获取锁,才能执行

7.3 生产者和消费者案例的性能问题

wait()方法和notify()方法,本地方法调用OS的功能,和操作系统交互,JVM找OS,把线程停止。频繁等待与唤醒,导致JVM和OS交互的次数过多。

notifyAll()唤醒全部的线程,也浪费线程资源,为了一个线程,不得以唤醒的了全部的线程。

7.4 Lock接口深入

Lock接口替换了同步synchronized,提供了更加灵活,性能更好的锁定操作

  • Lock接口中方法:newCondition() 方法的返回值是接口:Condition

  • 用集合的方式去管理线程

Java的第十三篇文章——JAVA多线程_第9张图片

7.5 生产者与消费者改进薇Lock接口

  • Condition接口 (线程的阻塞队列)

    • 进入队列的线程,释放锁

    • 出去队列的线程,再次的获取锁

    • 接口的方法:await() 线程释放锁,进入队列

    • 接口的方法:signal() 线程出去队列,再次获取锁,此方法是唤醒一个线程

线程的阻塞队列,依赖Lock接口创建

/**
 *  改进为高性能的Lock接口和线程的阻塞队列
 */
public class Resource {
   private int count ;
   private boolean flag ;
   private Lock lock = new ReentrantLock();//Lock接口实现类对象

    //Lock接口锁,创建出2个线程的阻塞队列
    private Condition prod = lock.newCondition();//生产者线程阻塞队列
    private Condition cust = lock.newCondition();//消费者线程阻塞队列

   //消费者调用
   public  void getCount() {
       lock.lock();//获取锁
         //flag是false,消费完成,等待生产
         while (!flag)
            //无限等待,消费线程等待,执行到这里的线程,释放锁,进入到消费者的阻塞队列
             try{cust.await();}catch (Exception ex){}

         System.out.println("消费第"+count);
            //修改标志位,为消费完成
         flag = false;
         //唤醒生产线程队列中的一个
         prod.signal();
         lock.unlock();//释放锁
   }
   //生产者调用
   public  void setCount() {
       lock.lock();//获取锁
         //flag是true,生产完成,等待消费
       while (flag)
            //无限等待,释放锁,进入到生产线程队列
            try{prod.await();}catch (Exception ex){}
         count++;
         System.out.println("生产第"+count+"个");
         //修改标志位,为生产完成
         flag = true;
         //唤醒消费者线程阻塞队列中年的一个
         cust.signal();
       lock.unlock();//释放锁
   }
}

7.6 Lock锁的实现原理

使用技术不开源,技术的名称叫做轻量级锁

使用的是CAS锁 (Compare And Swap) 自旋锁

JDK限制:当竞争的线程大于等于10,或者单个线程自旋超过10次的时候

JDK强制CAS锁取消,升级为重量级锁 (OS锁定CPU和内存的通信总线)

Java的第十三篇文章——JAVA多线程_第10张图片

8. 单例设计模式 

设计模式:不是技术,是以前的人开发人员,为了解决某些问题实现的写代码的经验。

所有的设计模式核心的技术,就是面向对象。

Java的设计模式有23种,分为3个类别,创建型、行为型和功能型三类

8.1 单例模式

要求:保证一个类的对象在内存中的唯一性

第一种为饿汉式

  • 私有修饰构造方法

  • 自己创建自己的对象

  • 方法get,返回本类对象

/**
 * - 私有修饰构造方法
 * - 自己创建自己的对象
 * - 方法get,返回本类对象
 */
public class Single {
    private Single(){}
	//饿汉式
    private static Single s = new Single(); // 自己创建自己的对象

//    方法get,返回本类对象
    public static Single getInstance(){
        return s;
    }
}
 public static void main(String[] args) {
        //静态方法,获取Single类的对象
        Single instance = Single.getInstance();
        System.out.println("instance = " + instance);
 }

第二种为懒汉式

  • 私有修饰构造方法

  • 创建本类的成员变量,不new对象

  • 方法get,返回本类对象

/**
 * - 私有修饰构造方法
 * - 创建本类的成员变量, 不new对象
 * - 方法get,返回本类对象
 */
public class Single {
    private Single(){}
	//懒汉,对象的延迟加载
    private static Single s = null;

    public static Single getInstance(){
        //判断变量s,是null就创建
        if (s == null) {
            s = new Single();
        }
        return s;
    }
}

8.2 懒汉式的安全问题

注:一个线程判断完变量 s=null,还没有执行new对象,被另一个线程抢到CPU资源,同时有2个线程都进行判断变量,对象创建多次。

    public static Single getInstance(){
        synchronized (Single.class) {
            //判断变量s,是null就创建
            if (s == null) {
                s = new Single();
            }
        }
        return s;
    }

性能问题:第一个线程获取锁,创建对象,返回对象。第二个线程调用方法的时候,变量s已经有对象了,根本就不需要在进同步,不要在判断空,直接return才是最高效的。双重的if判断,提高效率 Double Check Lock。

private static volatile Single s = null; 
public static Single getInstance(){
        //再次判断变量,提高效率
        if(s == null) {
            synchronized (Single.class) {
                //判断变量s,是null就创建
                if (s == null) {
                    s = new Single();
                }
            }
        }
        return s;
}

8.2 关键字 volatile

成员变量修饰符,不能修饰其它内容。

  • 关键字作用 :

    • 保证被修饰的变量,在线程中的可见性

    • 防止指令重排序

      • 单例的模式,不使用该关键字,可能线程会拿到一个尚未初始化完成的对象(半初始化)

如果我将以下代码中的volatile关键字去掉,程序就不会结束,陷入死循环。因为线程与线程之前的变量是不可见的,一个线程修改了变量,另一个线程是看不到。所以如以下例子,如果没有volatile关键字,main线程即使修改了flag变量,MyRunnable线程也是看不到的修改信息的。

public class MyRunnable implements Runnable {
    private volatile boolean flag = true;

    @Override
    public void run() {
        m();
    }

    private void m(){
        System.out.println("开始执行");
        while (flag){

        }
        System.out.println("结束执行");
    }


    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
public class MyRunnable implements Runnable {
    private volatile boolean flag = true;

    @Override
    public void run() {
        m();
    }

    private void m(){
        System.out.println("开始执行");
        while (flag){

        }
        System.out.println("结束执行");
    }


    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

9. 线程池ThreadPool

线程的缓冲池,目的就是提高效率。new Thread().start(),线程是内存中的一个独立的方法栈区,JVM没有能力开辟内存空间,和OS交互,会影响程序的速度。

9.1 Executors 类

  • 静态方法static newFixedThreadPool(int 线程的个数)

    • 方法的返回值ExecutorService接口的实现类,管理池子里面的线程

  • ExecutorService接口的方法

    • submit (Runnable r)提交线程执行的任务(此方法会去线程池拿线程,然后调用该线程的start方法)

9.2 Callable 接口

实现多线程的程序:接口特点是有返回值,可以抛出异常 (Runnable没有)

抽象的方法只有一个 call()

启动线程,线程调用重写方法call()

  • ExecutorService接口的方法

    • submit (Callable c)提交线程执行的任务

    • Future submit()方法提交线程任务后,方法有个返回值 Future接口类型

    • Future接口,获取到线程执行后的返回值结果

public class MyCall implements Callable {
    public String call() throws Exception{
        return "返回字符串";
    }
}
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建线程池,线程的个数是2个
       ExecutorService  es =  Executors.newFixedThreadPool(2);
       //线程池管理对象service,调用方法啊submit提交线程的任务
        //提交线程任务,使用Callable接口实现类
        Future future = es.submit(new MyCall());//返回接口类型 Future
        //接口的方法get,获取线程的返回值
        String str = future.get();
        System.out.println("str = " + str);

//        es.submit(my);
//        es.submit(my);
//        es.submit(my);
       // es.shutdown();//销毁线程池
    }

10. ConcurrentHashMap(需进一步了解)

ConcurrentHashMap类本质上Map集合,键值对的集合。使用方式和HashMap没有区别。

凡是对于此Map集合的操作,不去修改里面的元素,不会锁定。

11. 线程的状态图——生命周期

在某一个时刻,线程只能处于其中的一种状态。这种线程的状态反应的是JVM中的线程状态和OS无关。

Java的第十三篇文章——JAVA多线程_第11张图片

Java的第十三篇文章——JAVA多线程_第12张图片

你可能感兴趣的:(后端开放(JavaSE),java,开发语言)