关于Java的多线程实现

多线程介绍

进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

进程

线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

线程

什么是多线程呢?即就是一个程序中有多个线程在同时执行。

通过下图来区别单线程程序与多线程程序的不同:

  • 单线程程序:即,若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。如,去网吧上网,网吧只能让一个人上网,当这个人下机后,下一个人才能上网。
  • 多线程程序:即,若有多个任务可以同时执行。如,去网吧上网,网吧能够让多个人同时上网。

多线程

程序运行原理

  • 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
  • 抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

抢占、优先级

抢占式调度详解

大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着

抢占式调度

实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。

其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

线程的状态

线程可以处于下列状态之一:

  • NEW:至今尚未启动的线程处于这种状态
  • RUNNABLE:正在 Java 虚拟机中执行的线程处于这种状态
  • BLOCKED:受阻塞并等待某个监视器锁的线程处于这种状态
  • WAITING:无限期地等待另一个线程来执行某一特定操作的线程处于这种状态
    TIMED_WATING:等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。TERHINATED已退出的线程处于这秘状态

在给定时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所有操作系统线程状态

线程的状态

主线程

当我们在dos命令行中输入java空格类名回车后,启动JVM,并且加载对应的class文件。虚拟机并会从main方法开始执行我们的程序代码,一直把main方法的代码执行结束。如果在执行过程遇到循环时间比较长的代码,那么在循环之后的其他代码是不会被马上执行的。如下代码演示:

class Demo{
    
    
	String name;
	Demo(String name){
    
    
		this.name = name;
	}
	void show()	{
    
    
		for (int i=1;i<=10000 ;i++ )		{
    
    
			System.out.println("name="+name+",i="+i);
		}
	}
}
class TestDemo {
    
    
	public static void main(String[] args) 	{
    
    
	    Demo d = new Demo("小强");
         Demo d2 = new Demo("旺财");
		d.show();
		d2.show();
		System.out.println("Hello World!");
	}
}

若在上述代码中show方法中的循环执行次数很多,这时在d.show();下面的代码是不会马上执行的,并且在dos窗口会看到不停的输出name=小强,i=值,这样的语句。为什么会这样呢?

原因是:jvm启动后,必然有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为主线程。当程序的主线程执行时,如果遇到了循环而导致程序在指定位置停留时间过长,则无法马上执行下面的程序,需要等待循环结束后能够执行。

那么,能否实现一个主线程负责执行其中一个循环,再由另一个线程负责其他代码的执行,最终实现多部分代码同时执行的效果?

能够实现同时执行,通过Java中的多线程技术来解决该问题

Thread类

如何创建线程呢?通过API中搜索,查到Thread类。通过阅读Thread类中的描述。Thread是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。

Thread类

构造方法

常用方法

创建新执行线程有两种方法。

  1. 将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。创建对象,开启线程。run方法相当于其他线程的main方法。
  2. 声明一个实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

创建线程方式一继承Thread类

创建线程的步骤:

  1. 定义一个类继承Thread
  2. 重写run方法
  3. 创建子类对象,就是创建线程对象
  4. 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法
/**
 * 这是自定义线程类,继承Thread类
 * 重写run方法
 */
public class SubThread extends Thread {
    
    
    /**
     * 重写run方法,完成该线程执行的逻辑
     */
    @Override
    public void run() {
    
    
        for (int i=0; i<50; i++) {
    
    
            System.out.println(getName()+"循环第"+i+"次");
        }
    }
}
/**
 * 测试类,创建和启动一个线程
 *  创建Theard子类对象
 *  子类对象调用方法start(),让线程程序执行JVM调用线程中的run
 */
public class ThreadDemo {
    
    
    public static void main(String[] args) {
    
    
        //创建自定义线程对象
        SubThread st = new SubThread();
        //开启新线程
        st.start();

        //在主方法中执行for循环
        for (int i=0; i<50; i++) {
    
    
            System.out.println("main循环第"+i+"次");
        }
    }
}

线程对象调用 run方法和调用start方法区别:

线程对象调用run方法不开启线程。仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行。

线程随机性

1、我们为什么要继承Thread类,并调用其的start方法才能开启线程呢?

那是因为Thread类用来描述线程,具备线程应该有功能。

2、为什么不直接创建Thread类的对象呢?

Thread t1 = new Thread();
t1.start();

这样做没有错,但是该start调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是这个run方法中并没有定义我们需要让线程执行的代码。对于之前所讲的主线程,它的任务定义在main函数中。自定义线程需要执行的任务都定义在run方法中。Thread类run方法中的任务并不是我们所需要的,只有重写这个run方法。既然Thread类已经定义了线程任务的编写位置(run方法),那么只要在编写位置(run方法)中定义任务代码即可。所以进行了重写run方法动作。

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈,当执行线程的任务结束了,线程自动在栈内存中释放了,但是当所有的执行线程都结束了,那么进程就结束了。

多线程的内存图解

获取线程名称

开启的线程都会有自己的独立运行栈内存,那么这些运行的线程的名字是什么呢?该如何获取呢?既然是线程的名字,按照面向对象的特点,是哪个对象的属性和谁的功能,那么我们就去找那个对象就可以了。查阅Thread类的API文档发现有个方法是获取当前正在运行的线程对象。还有个方法是获取当前线程对象的名称。

API说明

/**
 * 这是自定义线程类,继承Thread类
 * 重写run方法
 *
 * 获取线程名字的方法 String getName()
 */
public class SubThread extends Thread {
    
    
    /**
     * 重写run方法,完成该线程执行的逻辑
     */
    @Override
    public void run() {
    
    
        System.out.println(super.getName());
    }
}
/**
 * 测试类,创建和启动一个线程
 * 每个线程都有自己的名字
 *  运行方法main主线程,默认名字就是"main"
 *  其他新建的子线程,默认名字是"Thread-x"
 *
 * JVM开启主线程,运行方法main,主线程也是线程必然也是继承于Thread类
 * Theard类当中,静态方法static Thread currentThread()返回正在执行的线程对象
 */
public class ThreadDemo {
    
    
    public static void main(String[] args) {
    
    
        //创建自定义线程对象
        SubThread st = new SubThread();
        //开启新线程
        st.start();

        //通用获取线程名称的方式
        System.out.println(Thread.currentThread().getName());
    }
}

设置线程名称

/**
 * 这是自定义线程类,继承Thread类
 * 重写run方法
 *
 * 设置线程名字的方法 setName(String name)
 */
public class SubThread extends Thread {
    
    
    public SubThread(){
    
    }
    public SubThread(String threadName){
    
    
        setName(threadName);
    }

    /**
     * 重写run方法,完成该线程执行的逻辑
     */
    @Override
    public void run() {
    
    
        System.out.println(super.getName());
    }
}
/**
 * 测试类,创建和启动一个线程
 * 每个线程都有自己的名字
 *  运行方法main主线程,默认名字就是"main"
 *  其他新建的子线程,默认名字是"Thread-x"
 *
 * JVM开启主线程,运行方法main,主线程也是线程必然也是继承于Thread类
 * Theard类当中,静态方法static Thread currentThread()返回正在执行的线程对象
 */
public class ThreadDemo {
    
    
    public static void main(String[] args) {
    
    
        //创建自定义线程对象
        SubThread st = new SubThread("new-thread-01");
        //开启新线程
        st.start();

        //通用获取线程名称的方式
        System.out.println(Thread.currentThread().getName());
    }
}

创建线程方式—实现Runnable接口

创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

为何要实现Runnable接口,Runable是啥玩意呢?

查看Runnable接口说明文档:Runnable接口用来指定每个线程要执行的任务。包含了一个 run 的无参数抽象方法,需要由接口实现类重写该方法。

Runnable接口

构造方法

常用方法

创建线程的步骤:

  1. 定义类实现Runnable接口;
  2. 覆盖接口中的run方法;
  3. 创建Thread类的对象;
  4. 将Runnable接口的子类对象作为参数传递给Thread类的构造函数;
  5. 调用Thread类的start方法开启线程;
/**
 * 实现线程程序的另一种方式,接口实现
 * 实现接口Runnable,重写run方法
 */
public class SubRunnable implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for(int i=0; i<5; i++) {
    
    
            System.out.println(i);
        }
    }
}
/**
 * 测试类,创建和启动一个线程
 * 创建Thread类对象,在构造方法当中传递Runnable接口实现类
 * 调用Thread类方法start()启动
 */
public class ThreadDemo {
    
    
    public static void main(String[] args) {
    
    
        SubRunnable sr = new SubRunnable();
        new Thread(sr).start();
    }
}

为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?

实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。

实现Runnable的好处:

第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。

线程的匿名内部类使用

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作

/**
 * 使用匿名内部类,实现多线程程序
 * 前提:继承或者接口的实现
 * new 父类或者接口(){
 *     重写抽象的方法
 * }
 */
public class ThreadDemo{
    
    
    public static void main(String[] args) {
    
    
        //继承方式 XXX extends Thread{public void run(){ ... }}
        new Thread(){
    
    
            @Override
            public void run() {
    
    
                System.out.println(getName());
            }
        }.start();

        //实现接口 XXX implements Runnable(){public void run(){ ... }}
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println(Thread.currentThread().getName());
            }
        }){
    
    }.start();
    }
}

线程池

线程池概念

线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

线程池原理

我们详细的解释一下为什么要使用线程池?

在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

使用线程池方式–Runnable接口

通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。

  • Executors:线程池创建工厂类
    • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象
  • ExecutorService:线程池类
    • Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
  • Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

使用线程池中线程对象的步骤:

  1. 创建线程池对象
  2. 创建Runnable接口子类对象
  3. 提交Runnable接口子类对象
  4. 关闭线程池
/**
 * JDK5的新特性 线程池技术
 * 实现一个线程池,使用util包下的concurrent工厂类Executors中的静态方法创建线程对象,并设定线程的个数
 *  static ExecutorService newFixedThreadPool(int 个数) 返回线程池对象
 *  返回的是ExecutorService接口的实现类(线程池的对象)
 *  接口实现类对象,调用方法submit(Runnable r)提交一个线程执行的任务
 */
public class ThreadPoolDemo {
    
    
    public static void main(String[] args) {
    
    
        //调用工厂类的静态方法,创建线程池对象
        //返回的是线程池对象,即ExecutorService接口
        ExecutorService es = Executors.newFixedThreadPool(2);
        //调用接口实现类对象es中的方法submit提交执行任务
        //将Runnable的实现类对象传递即可
        es.submit(
                new Runnable() {
    
    
                    @Override
                    public void run() {
    
    
                        System.out.println(Thread.currentThread().getName()+"正在执行新任务");
                    }
                }
        );

        //销毁线程池的方法(一般不会使用)
        es.shutdown();
    }
}

使用线程池方式—Callable接口

  • Callable接口:与Runnable接口功能相似,用来指定线程的任务。其中的call()方法,用来返回线程任务执行完毕后的结果,call方法可抛出异常。
  • ExecutorService:线程池类
    • Future submit(Callable task):获取线程池中的某一个线程对象,并执行线程中的call()方法
  • Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

使用线程池中线程对象的步骤:

  1. 创建线程池对象
  2. 创建Callable接口子类对象
  3. 提交Callable接口子类对象
  4. 关闭线程池
import java.util.concurrent.*;

/**
 * JDK5的新特性 线程池技术 实现Callable接口
 * 实现步骤:
 *  Executor工厂类静态方法newFixedThreadPool创建线程池对象
 *  线程池实现对象ExecutorService接口实现类,调用submit方法提交线程
 *  submit(Callable c)
 */
public class ThreadPoolDemo {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        ExecutorService es = Executors.newFixedThreadPool(2);
        //提交线程任务的方法submit将返回一个Future接口的实现类
        Future<String> f = es.submit(
                /**
                 * Callable接口实现类,作为线程提交任务出现
                 * 使用方法返回值
                 */
                new Callable<String>() {
    
    
                    @Override
                    public String call() throws Exception {
    
    
                        return Thread.currentThread().getName()+"正在执行新任务";
                    }
                }
        );

        //获取返回值
        System.out.println(f.get());
    }
}

案例:多线程实现异步计算

import java.util.concurrent.Callable;

/**
 * Callable接口的实现类
 */
public class CountDemo implements Callable<Integer> {
    
    
    private int a;
    private int b;

    public CountDemo(int a, int b) {
    
    
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer call() throws Exception {
    
    
        int result = 0;
        for (int i=a; i<=b; i++){
    
    
            result=result+i;
        }
        return result;
    }
}
import java.util.concurrent.*;

/**
 * 使用多线程技术进行求和计算
 * 要求两个线程处理,一个线程计算1...100的和,另一个计算101...200的和
 */
public class ThreadPoolDemo {
    
    

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        ExecutorService es = Executors.newFixedThreadPool(2);

        //线程一:计算1...100的和
        Future<Integer> result1 = es.submit(new CountDemo(1,100));
        //线程二:计算101...200的和
        Future<Integer> result2 = es.submit(new CountDemo(101,200));

        int finalResult = result1.get() + result2.get();
        System.out.println("最终结果为:"+ finalResult);
    }
}

自定义线程池:ThreadPoolExecutor

内置的线程池, 不推荐生产使用:
Executors.newCachedThreadPool();
Executors.newFixedThreadPool(10);
Executors.newScheduledThreadPool(10);
Executors.newSingleThreadExecutor();

上述的方法不推荐在生产当中使用,在生产场景下必须使用ThreadPoolExecutor构建线程池,这样可以明确线程池的运行规则,创建符合自己业务场景需要的线程池,避免资源耗尽的风险。以下是一个简单的示例:

构造函数

构造方法

  • corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去。
  • maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量。
  • keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被摧毁。
  • unit:keepAliveTime的单位。
  • workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;分为直接提交队列,有界任务队列,无界任务队列,优先任务队列。
  • threadFactory:线程工厂,用于创建线程。
  • handler:拒绝策略,当任务太多来不及处理的时候,会用这种策略拒绝任务。

corePoolSize和maximumPoolSize

ThreadPoolExecutor executorPool = new ThreadPoolExecutor(
                5,	//corePoolSize
                10,	//maximumPoolSize
                3, 	//keepAliveTime
                TimeUnit.SECONDS,	//unit
                new ArrayBlockingQueue<Runnable>(50)
        );

上边代码意思是有5个核心线程数,10个最大线程数,任务队列是50个线程。

在运行时,JVM首先为前5个新任务创建新线程,此时再来任务就放入任务队列中,直到任务队列已放满,此时再来新任务,JVM就会创建新线程,直到此时线程池中达到10个线程了就停止创建,即达到了最大线程数,此时再来新任务就会使用配置的拒绝策略新任务的提交。

workQueue任务队列

分为直接提交队列,有界任务队列,无界任务队列,优先任务队列。

直接提交队列

设置SynchronousQueue队列,SynchronousQueue是一个特殊的BlockingEueue,它没有容量,每执行一个插入操作就会阻塞,需要再执行一个删除操作才能被唤醒,反之每一个删除操作也都要等待对应的插入操作。

import java.util.concurrent.*;
public class ThreadPool {
    
    
    private static ExecutorService pool;

    public static void main(String[] args) {
    
    
        pool = new ThreadPoolExecutor(1, 2,
                1000,
                TimeUnit.MICROSECONDS,
                new SynchronousQueue<Runnable>(),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 3; i++) {
    
    
            pool.execute(
                    new Runnable() {
    
    
                        @Override
                        public void run() {
    
    
                            System.out.println(Thread.currentThread().getName());
                        }
                    }
            );

        }

        pool.shutdown();
    }
}


------输出结果---------
pool-1-thread-2
pool-1-thread-1
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.if010.thread.ThreadPool$1@5e2de80c rejected from java.util.concurrent.ThreadPoolExecutor@1d44bcfa[Running, pool size = 2, active threads = 1, queued tasks = 0, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at com.if010.thread.ThreadPool.main(ThreadPool.java:16)

从输出结果当中看到,当任务队列为SynchronousQueue时,创建的线程数量大于maximumPoolSize时,直接执行了拒绝策略抛出异常。

使用SynchronousQueue队列,提交的任务不会被保存,会马上提交执行。如果用于执行任务的线程数量小于maximumPoolSize时,则尝试创建新的线程,如果达到maximumPoolSize设置的最大值,则会根据设置的handler执行拒绝策略。

有界任务队列

使用ArrayBlockingQueue实现

import java.util.concurrent.*;
public class ThreadPool {
    
    
    private static ExecutorService pool;

    public static void main(String[] args) {
    
    
        pool = new ThreadPoolExecutor(1, 2,
                1000,
                TimeUnit.MICROSECONDS,
                new ArrayBlockingQueue<Runnable>(10),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 3; i++) {
    
    
            pool.execute(
                    new Runnable() {
    
    
                        @Override
                        public void run() {
    
    
                            System.out.println(Thread.currentThread().getName());
                        }
                    }
            );

        }

        pool.shutdown();
    }
}

------输出结果-------
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1

使用ArrayBlockingQueue有界任务队列,当有新的任务需要执行的时候,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,会将新的任务加入到等待队列中。当等待队列满了的时候,也会继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量时,则会执行拒绝策略。在这种情况下,线程数量的上限与有界任务队列的状态有直接的关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在corePoolSize以下,反之当任务队列满了时,则会以maximumPoolSize为最大线程数上限。

无界任务队列

使用LinkedBlockingQueue实现。

import java.util.concurrent.*;
public class ThreadPool {
    
    
    private static ExecutorService pool;

    public static void main(String[] args) {
    
    
        pool = new ThreadPoolExecutor(1, 2,
                1000,
                TimeUnit.MICROSECONDS,
                new LinkedBlockingDeque<Runnable>(),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 3; i++) {
    
    
            pool.execute(
                    new Runnable() {
    
    
                        @Override
                        public void run() {
    
    
                            System.out.println(Thread.currentThread().getName());
                        }
                    }
            );

        }

        pool.shutdown();
    }
}

------输出结果-------
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1

使用无界任务队列,线程池的任务队列可以无限制的添加新任务,而线程池创建的最大线程数量就是corePoolSize设置的数量,也就是说此时maximumPoolSize参数是无效的。当有新的任务加入时,则会直接进入等待队列,所以你一定要注意任务提交与处理直接的协调,要防止等待队列中的任务由于无法及时处理而一直增长,导致资源耗尽。

优先任务队列

使用PriorityBlockingQueue实现。

import java.util.concurrent.*;
public class ThreadPool {
    
    
    private static ExecutorService pool;

    public static void main(String[] args) {
    
    
        pool = new ThreadPoolExecutor(1, 2,
                1000,
                TimeUnit.MICROSECONDS,
                new PriorityBlockingQueue<Runnable>(),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 10; i++) {
    
    
            pool.execute(new ThreadTask(i));
        }

        pool.shutdown();
    }
}

class ThreadTask implements Runnable, Comparable<ThreadTask> {
    
    
    private int priority;

    public int getPriority() {
    
    
        return priority;
    }

    public void setPriority(int priority) {
    
    
        this.priority = priority;
    }

    public ThreadTask() {
    
    
    }

    public ThreadTask(int priority) {
    
    
        this.priority = priority;
    }

    //当前对象与其它对象比较,当优先级大时返回-1,优先级小时返回1
    //priority值越小优先级越高
    @Override
    public int compareTo(ThreadTask o) {
    
    
        return this.priority > o.priority ? -1 : 1;
    }

    @Override
    public void run() {
    
    
        try {
    
    
            //阻塞线程,使后续任务进入缓存队列
            Thread.sleep(1000);
            System.out.println("当前线程优先级:" + this.priority + ",线程名字:" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

------输出结果-------
当前线程优先级:0,线程名字:pool-1-thread-1
当前线程优先级:9,线程名字:pool-1-thread-1
当前线程优先级:8,线程名字:pool-1-thread-1
当前线程优先级:7,线程名字:pool-1-thread-1
当前线程优先级:6,线程名字:pool-1-thread-1
当前线程优先级:5,线程名字:pool-1-thread-1
当前线程优先级:4,线程名字:pool-1-thread-1
当前线程优先级:3,线程名字:pool-1-thread-1
当前线程优先级:2,线程名字:pool-1-thread-1
当前线程优先级:1,线程名字:pool-1-thread-1

除了第一个任务直接创建线程执行外,其它的任务都被放入了优先任务队列,按照优先级进行重新排序执行,且线程池的线程数一直为corePoolSize,也就是一个,说明此时maximumPoolSize设置无效。

也就是说PriorityBlockingQueue是一个特殊的无界队列,无论添加了多少个任务,线程池创建的线程数也不会超过corePoolSize设置的数量。

threadFactory

线程池中线程是通过ThreadPoolExecutor中的ThreadFactory线程工厂创建。通过自定义ThreadFactory可以按需要对线程池中创建的线程进行一些特殊设置,比如命名,优先级。

import java.util.concurrent.*;
public class ThreadPool {
    
    
    private static ExecutorService pool;

    public static void main(String[] args) {
    
    
        pool = new ThreadPoolExecutor(2, 4,
                1000,
                TimeUnit.MICROSECONDS,
                new ArrayBlockingQueue<Runnable>(5),
                new ThreadFactory() {
    
    
                    @Override
                    public Thread newThread(Runnable r) {
    
    
                        System.out.println("线程 " + r.hashCode() + " 创建");
                        //线程命名
                        Thread th = new Thread(r, "线程名字 " + r.hashCode());
                        return th;
                    }
                },
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
    
    
            pool.execute(
                    new Runnable() {
    
    
                        @Override
                        public void run() {
    
    
                            //输出执行线程的名称
                            System.out.println("执行中的线程名字:" + Thread.currentThread().getName());
                        }
                    }
            );
        }

        pool.shutdown();
    }
}

------输出结果-------
线程 1627674070 创建
线程 1360875712 创建
执行中的线程名字:线程名字 1627674070
线程 1625635731 创建
执行中的线程名字:线程名字 1360875712
执行中的线程名字:线程名字 1627674070
执行中的线程名字:线程名字 1360875712
执行中的线程名字:线程名字 1627674070
执行中的线程名字:线程名字 1627674070
执行中的线程名字:线程名字 1625635731
执行中的线程名字:线程名字 1360875712
执行中的线程名字:线程名字 1625635731
执行中的线程名字:线程名字 1627674070

handler

为防止资源被耗尽,任务队列都会选择创建有界任务队列,但是这种模式下如果出现任务队列已满并且线程池创建的线程数已达到最大线程数时,就需要指定ThreadPoolExecutor的RejectedExecutionHandler参数提供拒绝策略,来处理线程池超载情况。

ThreadPoolExecutor自带的拒绝策略如下:

  1. AbortPolicy策略:该策略直接抛出异常,阻止系统正常工作
  2. CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程中运行
  3. DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的
  4. DiscardPolicy策略:该策略会丢弃无法处理的任务,不予任何处理

以上内置的策略都实现了RejectedExecutionHandler接口,当然也可以自定义拒绝策略

import java.util.concurrent.*;
public class ThreadPool {
    
    
    private static ExecutorService pool;

    public static void main(String[] args) {
    
    
        pool = new ThreadPoolExecutor(1, 2,
                1000,
                TimeUnit.MICROSECONDS,
                new ArrayBlockingQueue<Runnable>(5),
                Executors.defaultThreadFactory(),
                new RejectedExecutionHandler() {
    
    
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    
    
                        System.out.println(r.toString() + "执行了拒绝策略");
                    }
                });

        for (int i = 0; i < 10; i++) {
    
    
            pool.execute(
                    new Runnable() {
    
    
                        @Override
                        public void run() {
    
    
                            try {
    
    
                                //让线程阻塞,使后续任务进入后续队列
                                Thread.sleep(1000);
                                //输出执行线程的名称
                                System.out.println("执行中的线程名字:" + Thread.currentThread().getName());
                            } catch (InterruptedException e) {
    
    
                                e.printStackTrace();
                            }
                        }
                    }
            );
        }

        pool.shutdown();
    }
}

------输出结果-------
com.if010.thread.ThreadPool$2@60e53b93执行了拒绝策略
com.if010.thread.ThreadPool$2@5e2de80c执行了拒绝策略
com.if010.thread.ThreadPool$2@1d44bcfa执行了拒绝策略
执行中的线程名字:pool-1-thread-1
执行中的线程名字:pool-1-thread-2
执行中的线程名字:pool-1-thread-1
执行中的线程名字:pool-1-thread-2
执行中的线程名字:pool-1-thread-2
执行中的线程名字:pool-1-thread-1
执行中的线程名字:pool-1-thread-2

当任务加入了休眠阻塞,执行需要花费一定时间,导致会有一定的任务被丢弃,从而执行自定义的拒绝策略。

ThreadPoolExecutor扩展

  • beforeExecute:线程池中的任务运行前执行
  • afterExecute:线程池中的任务运行完毕后执行
  • terminated:线程池退出后执行

通过这三个接口,可以监控每个任务的开始和结束时间

import java.util.concurrent.*;
public class ThreadPool {
    
    
    private static ExecutorService pool;

    public static void main(String[] args) {
    
    
        pool = new ThreadPoolExecutor(2, 4,
                1000,
                TimeUnit.MICROSECONDS,
                new ArrayBlockingQueue<Runnable>(5),
                new ThreadFactory() {
    
    
                    @Override
                    public Thread newThread(Runnable r) {
    
    
                        System.out.println("线程 " + r.hashCode() + " 创建");
                        //线程命名
                        Thread th = new Thread(r, "线程 " + r.hashCode());
                        return th;
                    }
                },
                new ThreadPoolExecutor.CallerRunsPolicy()) {
    
    
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
    
    
                System.out.println("准备执行 " + ((ThreadTask) r).getTaskName());
            }

            @Override
            protected void afterExecute(Runnable r, Throwable t) {
    
    
                System.out.println("执行完毕 " + ((ThreadTask) r).getTaskName());
            }

            @Override
            protected void terminated() {
    
    
                System.out.println("线程池退出");
            }
        };

        for (int i = 0; i < 10; i++) {
    
    
            pool.execute(new ThreadTask("Task" + i));
        }

        pool.shutdown();
    }
}

class ThreadTask implements Runnable {
    
    
    private String taskName;

    public String getTaskName() {
    
    
        return taskName;
    }

    public void setTaskName(String taskName) {
    
    
        this.taskName = taskName;
    }

    public ThreadTask(String taskName) {
    
    
        this.taskName = taskName;
    }

    @Override
    public void run() {
    
    
        //输出执行线程的名称
        System.out.println("任务名称:" + this.getTaskName() + " 线程名称" + Thread.currentThread().getName());
    }
}

------输出结果-------
线程 1627674070 创建
线程 1360875712 创建
准备执行 Task0
任务名称:Task0 线程名称线程 1627674070
线程 1625635731 创建
准备执行 Task1
执行完毕 Task0
任务名称:Task1 线程名称线程 1360875712
准备执行 Task2
线程 1580066828 创建
任务名称:Task2 线程名称线程 1627674070
执行完毕 Task2
执行完毕 Task1
准备执行 Task8
任务名称:Task8 线程名称线程 1580066828
执行完毕 Task8
准备执行 Task5
任务名称:Task5 线程名称线程 1580066828
执行完毕 Task5
准备执行 Task7
任务名称:Task7 线程名称线程 1625635731
执行完毕 Task7
准备执行 Task6
准备执行 Task4
准备执行 Task3
任务名称:Task3 线程名称线程 1360875712
任务名称:Task4 线程名称线程 1627674070
执行完毕 Task4
任务名称:Task6 线程名称线程 1580066828
执行完毕 Task6
准备执行 Task9
任务名称:Task9 线程名称线程 1625635731
执行完毕 Task9
执行完毕 Task3
线程池退出

对于这三个方法的重写,可以对线程池中线程的运行状态进行监控,在其执行前后打印相关信息。使用shutdown方法可以比较安全的关闭线程池,当调用该方法后,线程池不再接受后续添加的任务,但是此时线程池不会马上退出,而是等到添加到线程池中的任务都已经完成处理后,才会退出。

线程分配流程

线程分配流程

线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

我们通过一个案例,演示线程的安全问题

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “功夫熊猫3”,本次电影的座位共10个(本场电影只能卖10张票)。

/**
 * 模拟电影院的售票窗口,实现多个窗口同时卖"功夫熊猫3"这场电影票(多个窗口一起卖这10张票)
 * 窗口,线程对象来模拟
 * 票,Runnable接口子类来模拟
 */
public class MovieTestDemo {
    
    
    public static void main(String[] args) {
    
    
        //创建Runnable接口实现类对象
        Runnable t = new Tickets();
        //创建3个Thread类对象,传递Runnable接口实现类
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

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

class Tickets implements Runnable{
    
    
    //定义出10张票
    private int tickets = 10;

    @Override
    public void run() {
    
    
        while (true){
    
    
            //对票数进行判断,大于0则可以出售,变量--操作
            if (tickets > 0) {
    
    
                try {
    
    
                    Thread.sleep(10);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + " 出售第" + tickets-- + "张票");
            } else {
    
    
                System.out.println("售票结束");
                break;
            }
        }
    }
}

---------输出结果------
Thread-1 出售第10张票
Thread-0 出售第10张票
Thread-2 出售第9张票
Thread-0 出售第8张票
Thread-1 出售第7张票
Thread-2 出售第6张票
Thread-1 出售第5张票
Thread-0 出售第4张票
Thread-2 出售第3张票
Thread-1 出售第2张票
Thread-0 出售第1张票
售票结束
Thread-2 出售第0张票
售票结束
Thread-1 出售第-1张票
售票结束

运行结果发现:上面程序出现了问题

  • 票出现了重复的票
  • 错误的票 0、-1

其实,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程安全处理Synchronized

java中提供了线程同步机制,它能够解决上述的线程安全问题。

线程同步的方式有两种:

  1. 同步代码块
  2. 同步方法

同步代码块执行原理图

同步代码块

同步代码块: 在代码块声明上 加上synchronized

synchronized (锁对象) {
    
    
	可能会产生线程安全问题的代码
}

同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。

使用同步代码块,对电影院卖票案例中Ticket类进行如下代码修改:

/**
 * 模拟电影院的售票窗口,实现多个窗口同时卖"功夫熊猫3"这场电影票(多个窗口一起卖这10张票)
 * 窗口,线程对象来模拟
 * 票,Runnable接口子类来模拟
 */
public class MovieTestDemo {
    
    
    public static void main(String[] args) {
    
    
        //创建Runnable接口实现类对象
        Runnable t = new Tickets();
        //创建3个Thread类对象,传递Runnable接口实现类
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

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

class Tickets implements Runnable{
    
    
    //定义出10张票
    private int tickets = 10;
    Object lock = new Object();

    @Override
    public void run() {
    
    
        while (true){
    
    
            synchronized (lock) {
    
    
                //对票数进行判断,大于0则可以出售,变量--操作
                if (tickets > 0) {
    
    
                    try {
    
    
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + " 出售第" + tickets-- + "张票");
                } else {
    
    
                    System.out.println("售票结束");
                    break;
                }
            }
        }
    }
}

同步方法

同步方法:在方法声明上加上synchronized

public synchronized void method(){
    
    
   	//可能会产生线程安全问题的代码
}

同步方法中的锁对象是 this

public void method(){
    
    
    synchronized(this) {
    
    
        //可能会产生线程安全问题的代码
    }
}

使用同步方法,对电影院卖票案例中Ticket类进行如下代码修改:

/**
 * 模拟电影院的售票窗口,实现多个窗口同时卖"功夫熊猫3"这场电影票(多个窗口一起卖这10张票)
 * 窗口,线程对象来模拟
 * 票,Runnable接口子类来模拟
 */
public class MovieTestDemo {
    
    
    public static void main(String[] args) {
    
    
        //创建Runnable接口实现类对象
        Runnable t = new Tickets();
        //创建3个Thread类对象,传递Runnable接口实现类
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

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

class Tickets implements Runnable{
    
    
    //定义出10张票
    private int tickets = 10;
  
    @Override
    public void run() {
    
    
        while (true){
    
    
            if (buyTickets() == false){
    
    
                System.out.println("售票结束");
                return;
            }
        }
    }

    public synchronized boolean buyTickets(){
    
    
        if (tickets > 0) {
    
    
            try {
    
    
                Thread.sleep(10);
            } catch (InterruptedException e) {
    
    
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + " 出售第" + tickets-- + "张票");
            return true;
        } else {
    
    
            return false;
        }
    }
}

静态同步方法: 在方法声明上加上static synchronized,

public static synchronized void method(){
    
    
    //可能会产生线程安全问题的代码
}

静态同步方法中的锁对象是 类名.class

public static void method(){
    
    
    synchronized(MovieTestDemo.class) {
    
    
        //可能会产生线程安全问题的代码
    }
}

死锁

同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。

线程死锁的原理

synchronzied(A){
    
    
    synchronized(B){
    
    
        //肯能产生线程安全问题的代码
    }
}

实现死锁:

public class TestLock {
    
    
    public static void main(String[] args) {
    
    
        LockDemo lockDemo = new LockDemo();
        Thread t0 = new Thread(lockDemo);
        Thread t1 = new Thread(lockDemo);

        t0.start();t1.start();
    }
}

class LockDemo implements Runnable{
    
    
    private int i=0;
    @Override
    public void run() {
    
    
        while (true){
    
    
            if (i%2 == 0){
    
    
                //先拿A锁,再那B锁
                synchronized (LockA.class){
    
    
                    System.out.println(Thread.currentThread().getName()+"号玩家在 if 已拿到 A 锁");
                    synchronized (LockB.class){
    
    
                        System.out.println(Thread.currentThread().getName()+"号玩家在 if 已拿到 B 锁");
                    }
                }
            } else {
    
    
                //先拿B锁,再那A锁
                synchronized (LockB.class){
    
    
                    System.out.println(Thread.currentThread().getName()+"号玩家在 else 已拿到 B 锁");
                    synchronized (LockA.class){
    
    
                        System.out.println(Thread.currentThread().getName()+"号玩家在 else 已拿到 A 锁");
                    }
                }
            }
            i++;
        }
    }
}

class LockA{
    
    
    private LockA(){
    
    }
    public static final LockA locka = new LockA();
}

class LockB{
    
    
    private LockB(){
    
    }
    public static final LockB lockb = new LockB();
}

Lock接口

查阅API,查阅Lock接口描述,Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。

Lock接口中的常用方法

Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。

我们使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 使用JDK1.5之后的接口Lock,替换同步代码块,实现线程的安全性
 * Lock接口方法:
 *  lock() 获取锁
 *  unlock() 释放锁
 * 实现类ReentrantLock
 */
public class MovieTestDemo {
    
    
    public static void main(String[] args) {
    
    
        //创建Runnable接口实现类对象
        Runnable t = new Tickets();
        //创建3个Thread类对象,传递Runnable接口实现类
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

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

class Tickets implements Runnable{
    
    
    //定义出售的票源
    private int tickets = 10;

    //在类成员位置,创建Lock接口实现类对象
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
    
    
        while (true){
    
    
            //调用Lock接口方法lock获取锁
            lock.lock();
            try {
    
    
                if (tickets > 0) {
    
    
                    try {
    
    
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + " 出售第" + tickets-- + "张票");
                } else {
    
    
                    System.out.println("售票结束");
                    return;
                }
            } catch (Exception e){
    
    

            } finally {
    
    
                //释放锁,调用Lock接口方法unlock
                lock.unlock();
            }
        }
    }
}

等待唤醒机制

在开始讲解等待唤醒机制之前,有必要搞清一个概念——线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

等待唤醒机制所涉及到的方法:

  • wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
  • notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
  • notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。

其实,所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。

仔细查看JavaAPI之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?

因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。

方法

接下里,我们先从一个简单的示例入手:

线程通信示例

如上图说示,输入线程向Resource中输入name ,sex , 输出线程从资源中输出,先要完成的任务是:

  1. 当input发现Resource中没有数据时,开始输入,输入完成后,叫output来输出。如果发现有数据,就wait();
  2. 当output发现Resource中没有数据时,就wait(); 当发现有数据时,就输出,然后,叫醒input来输入数据。

下面代码,模拟等待唤醒机制的实现:

/**
 * 定义资源类,有两个成员变量name,sex
 * 同时有两个线程,对资源中的变量操作
 * 线程一对name,age赋值
 * 线程二对name,age输出
 */
public class Resource {
    
    
    public String name;
    public String sex;
    public boolean flag = false;
}

/**
 * 输入的线程,对资源对象Resource中的成员变量赋值
 */
class Input implements Runnable{
    
    
    private Resource r;

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

    @Override
    public void run() {
    
    
        int i = 0;
        while (true) {
    
    
            synchronized (r) {
    
    
                if (r.flag){
    
    
                    try {
    
    
                        r.wait();
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                } else {
    
    
                    if (i % 2 == 0) {
    
    
                        r.name = "小王";
                        r.sex = "男";
                    } else {
    
    
                        r.name = "小林";
                        r.sex = "女";
                    }
                    i++;
                    r.flag = true;
                    r.notify();
                }
            }
        }
    }
}

/**
 * 输出线程,对Resource的成员变量进行打印输出
 */
class Output implements Runnable{
    
    
    private Resource r;

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

    @Override
    public void run() {
    
    
        while (true){
    
    
            synchronized (r) {
    
    
                if (r.flag) {
    
    
                    System.out.println(r.name + "\t" + r.sex);
                    r.flag = false;
                    r.notify();
                } else {
    
    
                    try {
    
    
                        r.wait();
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}

/**
 * 开启输入线程和输出线程
 */
class TestDome{
    
    
    public static void main(String[] args) {
    
    
        Resource r = new Resource();
        Input in = new Input(r);
        Output out = new Output(r);

        Thread t_in = new Thread(in);
        Thread t_out = new Thread(out);

        t_in.start();t_out.start();
    }
}

这里要注意的是wait和notify需要使用锁对象进行调用,否则会报以下错误:

Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.if010.thread.Output.run(Resource.java:73)
	at java.lang.Thread.run(Thread.java:750)

猜你喜欢

转载自blog.csdn.net/qq_32262243/article/details/132009122