Java并发编程 一 线程介绍、创建线程的4种方式、线程常用方法、两阶段终止状态、线程的五种/六种状态

一、线程与进程、并行并发、同步异步概念

1、进程与进程

进程: 资源分配的最小单位

  • 进程是线程的容器, 一个进程中包含多个线程, 真正执行任务的是线程

线程: 资源调度的最小单位

进程

  • 程序指令数据组成,但是这些 指令要运行,数据要读写,就必须将指令加载到cpu,数据加载至内存。在指令运行过程中还需要用到磁盘,网络等设备,进程就是用来加载指令,管理内存,管理IO的
  • 当一个指令被运行,从磁盘加载这个程序的代码到内存,这时候就开启了一个进程
  • 进程就可以视为程序的一个实例,大部分程序都可以运行多个实例进程(例如记事本,浏览器等),部分只可以运行一个实例进程(例如360安全卫士)

线程

  • 一个进程之内可以分为多个线程
  • 一个线程就是一个指令流将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为资源的最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器。

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享; 进程间通信较为复杂
    同一台计算机的进程通信称为 IPC(Inter-process communication)
  • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量线程上下文切换成本一般上要比进程上下文切换低

2、 并行与并发

并发: 在单核CPU下, 一定是并发执行的, 也就是在同一个时间段内一起执行. 实际还是串行执行, CPU的时间片切换非常快, 给人一种同时运行的感觉。

并行: 在多核CPU下, 能真正意义上实现并行执行, 在同一个时刻, 多个线程同时执行; 比如说2核cpu, 同时执行4个线程. 理论上同时可以有2个线程是并行执行的. 此时还是存在并发, 因为2个cpu也会同时切换不同的线程执行任务罢了

并发 (concurrent)

  • 微观串行, 宏观并行
  • 单核 cpu下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu 在线程间(时间片很短)的切换非常快,给人的 感觉是同时运行的 。一般会将这种线程轮流使用 CPU的做法称为并发(concurrent)
  • 线程轮流使用cput称为并发(concurrent)
    Java并发编程 一 线程介绍、创建线程的4种方式、线程常用方法、两阶段终止状态、线程的五种/六种状态_第1张图片

并行

  • 多核 cpu下,每个核(core) 都可以调度运行线程,这时候线程可以是并行的,不同的线程同时使用不同的cpu在执行。
    Java并发编程 一 线程介绍、创建线程的4种方式、线程常用方法、两阶段终止状态、线程的五种/六种状态_第2张图片

二者对比

  • 引用 Rob Pike 的一段描述:
    • 并发(concurrent): 是同一时间应对(dealing with)多件事情的能力
    • 并行(parallel): 是同一时间动手做(doing)多件事情的能力

例子

  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
  • 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待)
  • 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是 并行

3、同步和异步

调用方的角度讲

  • 如果需要等待结果返回才能继续运行的话就是同步
  • 如果不需要等待就是异步
1 设计
  • 多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这5秒cpu什么都做不了,其它代码都得暂停
2 结论
  • 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换避免阻塞主线程
  • tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程
  • UI 程序中,开线程进行其他操作,避免阻塞 UI 线程

二、线程的创建 (重点)

1、创建一个线程(非主线程)

1、通过继承Thread创建线程

public class CreateThread {
	public static void main(String[] args) {
		Thread myThread = new MyThread();
        // 启动线程
		myThread.start();
	}
}

class MyThread extends Thread {
	@Override
	public void run() {
		System.out.println("my thread running...");
	}
}
  • 使用继承方式的好处是,在run()方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法;不好的地方是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码

2、使用Runnable配合Thread (推荐)

public class Test2 {
	public static void main(String[] args) {
		//创建线程任务
		Runnable r = new Runnable() {
			@Override
			public void run() {
				System.out.println("Runnable running");
			}
		};
		//将Runnable对象传给Thread
		Thread t = new Thread(r);
		//启动线程
		t.start();
	}
}

或者

public class CreateThread2 {
   private static class MyRunnable implements Runnable {
      @Override
      public void run() {
         System.out.println("my runnable running...");
      }
   }

   public static void main(String[] args) {
      MyRunnable myRunnable = new MyRunnable();
      Thread thread = new Thread(myRunnable);
      thread.start();
   }
}
  • 通过实现Runnable接口,并且实现run()方法。在创建线程时作为参数传入该类的实例即可
方法二的简化:使用lambda表达式简化操作
  • 当一个接口带有@FunctionalInterface注解时,是可以使用lambda来简化操作的
  • 所以方法二中的代码可以被简化为
public class Test2 {
	public static void main(String[] args) {
		//创建线程任务
		Runnable r = () -> {
            //直接写方法体即可
			System.out.println("Runnable running");
			System.out.println("Hello Thread");
		};
		//将Runnable对象传给Thread
		Thread t = new Thread(r);
		//启动线程
		t.start();
	}
}
原理之 Thread 与 Runnable 的关系
  • 分析 Thread 的源码,理清它与 Runnable 的关系

小结

  • 继承Thread方式: 是把线程和任务合并在了一起
  • 实现Runnable方式: 是把线程和任务分开了
  • 用 Runnable 更容易与线程池等高级 API 配合 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

3、使用FutureTask与Thread结合

使用FutureTask可以用泛型指定线程的返回值类型(Runnable的run方法没有返回值)

public class Test3 {
	public static void main(String[] args) throws ExecutionException, InterruptedException {
        //需要传入一个Callable对象
		FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
			@Override
			public Integer call() throws Exception {
				System.out.println("线程执行!");
				Thread.sleep(1000);
				return 100;
			}
		});

		Thread r1 = new Thread(task, "t2");
		r1.start();
		//获取线程中方法执行后的返回结果
		System.out.println(task.get());
	}
}

public class UseFutureTask {
   public static void main(String[] args) throws ExecutionException, InterruptedException {
      FutureTask<String> futureTask = new FutureTask<>(new MyCall());
      Thread thread = new Thread(futureTask);
      thread.start();
      // 获得线程运行后的返回值
      System.out.println(futureTask.get());
   }
}

class MyCall implements Callable<String> {
   @Override
   public String call() throws Exception {
      return "hello world";
   }
}

4、使用线程池来创建线程

/**
 * 创建线程的方式四:使用线程池
 *
 * 好处:
 * 1.提高响应速度(减少了创建新线程的时间)
 * 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
 * 3.便于线程管理
 *      corePoolSize:核心池的大小
 *      maximumPoolSize:最大线程数
 *      keepAliveTime:线程没有任务时最多保持多长时间后会终止
 *
 *
 * 面试题:创建多线程有几种方式?四种!
 */

class NumberThread implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

public class ThreadPool {

    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();


        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

//        service.submit(Callable callable);//适合使用于Callable
        //3.关闭连接池
        service.shutdown();
    }
}

总结

  • 使用 继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递,而如果使用Runnable方式,则只能使用主线程里面被声明为final的变量。不好的地方是Java不支持多继承,如果继承了Thread类,那么子类不能再继承其他类,而Runable则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是Futuretask方式可以
  • 开发中一般使用线程池的方式

Java并发编程 一 线程介绍、创建线程的4种方式、线程常用方法、两阶段终止状态、线程的五种/六种状态_第3张图片

三、线程运行原理 (重点)

1、虚拟机栈与栈帧

  • 虚拟机栈描述的是Java方法执行的内存模型每个方法被执行的时候都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,是属于线程的私有的当Java中使用多线程时,每个线程都会维护它自己的栈帧!每个线程只能有一个活动栈帧(在栈顶),对应着当前正在执行的那个方法

2、线程上下文切换(Thread Context Switch)

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程

  • 线程的 cpu 时间片用完(每个线程轮流执行,看前面并行的概念)
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleepyieldwaitjoinparksynchronizedlock 等方法

Thread Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 线程的状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

3、Thread的常见方法

Java并发编程 一 线程介绍、创建线程的4种方式、线程常用方法、两阶段终止状态、线程的五种/六种状态_第4张图片Java并发编程 一 线程介绍、创建线程的4种方式、线程常用方法、两阶段终止状态、线程的五种/六种状态_第5张图片

3.1、调用start 与 run方法的区别

调用start()方法
public static void main(String[] args) {
    Thread thread = new Thread(){
      @Override
      public void run(){
          log.debug("我是一个新建的线程正在运行中");
          FileReader.read(fileName);
      }
    };
    thread.setName("新建线程");
    thread.start();
    log.debug("主线程");
}
  • 输出:程序在t1 线程运行run()方法里面内容的调用是异步的代码
11:59:40.711 [main] DEBUG com.concurrent.test.Test4 - 主线程
11:59:40.711 [新建线程] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中
11:59:40.732 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] start ...
11:59:40.735 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 3 ms
调用run()方法
  • 将上面代码的thread.start();改为 thread.run();输出结果如下:程序仍在 main 线程运行, run()方法里面内容的调用还是同步的
12:03:46.711 [main] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中
12:03:46.727 [main] DEBUG com.concurrent.test.FileReader - read [test] start ...
12:03:46.729 [main] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 2 ms
12:03:46.730 [main] DEBUG com.concurrent.test.Test4 - 主线程

小结

  • 直接调用 run() 是在主线程中执行了 run()没有启动新的线程
  • 使用 start()启动新的线程,通过新的线程间接执行 run()方法中的代码

3.2、 sleep 与 yield

sleep方法
  1. 调用 sleep() 会让当前线程从 Running(运行状态) 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出 InterruptedException异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
  3. 睡眠结束后的线程未必会立刻得到执行 (需要分配到cpu时间片)
  4. 建议用 TimeUnitsleep() 代替 Thread 的 sleep()来获得更好的可读性
yield方法
  1. 调用 yield 会让当前线程从Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)

小结

  • yield使cpu调用其它线程但是cpu可能会再分配时间片给该线程而sleep需要等过了休眠时间之后才有可能被分配cpu时间片

3.3、线程优先级

  • 线程优先级提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它, 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
thread1.setPriority(Thread.MAX_PRIORITY); //设置为优先级最高

3.4、 join方法

  • 主线程中调用t1.join,则主线程等待t1线程执行完之后继续执行
private static void test1() throws InterruptedException {
    log.debug("开始");
    Thread t1 = new Thread(() -> {
        log.debug("开始");
        sleep(1);
        log.debug("结束");
        r = 10;
    },"t1");
    t1.start();
    // t1.join(); 
    // 这里如果不加t1.join(), 此时主线程不会等待t1线程给r赋值, 主线程直接就输出r=0结束了
    // 如果加上t1.join(), 此时主线程会等待到t1线程执行完才会继续执行.(同步), 此时r=10;
    log.debug("结果为:{}", r);
    log.debug("结束");
}

下图, 因为开辟了t1线程. 此时程序中有两个线程; main线程和t1线程; 此时在main线程中调用t1.join, 所以main线程只能阻塞等待t1线程执行完. t1线程在1s后将r=10, t1线程执行完, 此时main线程才会接着执行
Java并发编程 一 线程介绍、创建线程的4种方式、线程常用方法、两阶段终止状态、线程的五种/六种状态_第6张图片

3.5 interrupt 方法详解

该方法用于打断 sleep,wait,join的线程, 在阻塞期间cpu不会分配给时间片
  • 先了解一些interrupt()方法的相关知识:博客地址
  • 如果一个线程在在运行中被打断打断标记会被置为true
  • 如果是打断因sleep wait join方法而被阻塞的线程,会将打断标记置为false

sleep,wait,join的线程,这几个方法都会让线程进入阻塞状态,以 sleep 为例

public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("sleep...");
            try {
                Thread.sleep(5000); // wait, join
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        Thread.sleep(1000);
        System.out.println("iterrupt..");
        t1.interrupt();
        System.out.println(t1.isInterrupted()); // 如果是打断sleep,wait,join的线程, 即使打断了, 标记也为false
    }
}
sleep...
iterrupt..
打断标记为:false
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.guizy.ThreadPrintDemo.lambda$main$0(ThreadPrintDemo.java:14)
	at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0

打断正常运行的线程

  • 打断正常运行的线程, 线程并不会暂停,只是调用方法Thread.currentThread().isInterrupted();的返回值为true,可以判断Thread.currentThread().isInterrupted();的值来手动停止线程
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while(true) {
            boolean interrupted = Thread.currentThread().isInterrupted();
            if(interrupted) {
                System.out.println("被打断了, 退出循环");
                break;
            }
        }
    }, "t1");
    t1.start();
    Thread.sleep(1000);
    System.out.println("interrupt");
    t1.interrupt();
    System.out.println("打断标记为: "+t1.isInterrupted());
}
interrupt
被打断了, 退出循环
打断标记为: true

Process finished with exit code 0

3.6、 终止模式之两阶段终止模式

当我们在执行线程一时,想要终止线程二,这是就需要使用interrupt方法来优雅的停止线程二。

  • Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2线程一个处理其他事情的机会(如释放锁)。
    Java并发编程 一 线程介绍、创建线程的4种方式、线程常用方法、两阶段终止状态、线程的五种/六种状态_第7张图片

  • 如下所示:那么线程的isInterrupted()方法可以取得线程的打断标记

    • 如果线程在睡眠sleep期间被打断,打断标记是不会变的,为false,但是sleep期间被打断会抛出异常,我们据此手动设置打断标记为true
    • 如果是在程序正常运行期间被打断的,那么打断标记就被自动设置为true。处理好这两种情况那我们就可以放心地来料理后事啦!

下图①就是正常运行打断, ②是在睡眠中被打断
Java并发编程 一 线程介绍、创建线程的4种方式、线程常用方法、两阶段终止状态、线程的五种/六种状态_第8张图片

代码实现如下:

public class Test7 {
	public static void main(String[] args) throws InterruptedException {
		Monitor monitor = new Monitor();
		monitor.start();
		Thread.sleep(3500);
		monitor.stop();
	}
}

class Monitor {

	Thread monitor;

	/**
	 * 启动监控器线程
	 */
	public void start() {
		//设置线控器线程,用于监控线程状态
		monitor = new Thread() {
			@Override
			public void run() {
				//开始不停的监控
				while (true) {
                    //判断当前线程是否被打断了
					if(Thread.currentThread().isInterrupted()) {
						System.out.println("处理后续任务");
                        //终止线程执行
						break;
					}
					System.out.println("监控器运行中...");
					try {
						//线程休眠
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
						//如果是在休眠的时候被打断,不会将打断标记设置为true,这时要重新设置打断标记
						Thread.currentThread().interrupt();
					}
				}
			}
		};
		monitor.start();
	}

	/**
	 * 	用于停止监控器线程
	 */
	public void stop() {
		//打断线程
		monitor.interrupt();
	}
}

3.7、sleep,yiled,wait,join 对比

补充:

  • sleep,join,yield,interrupted是Thread类中的方法
  • wait/notify是object中的方法
  • sleep 不释放锁、释放cpu
  • join 释放锁、抢占cpu
  • yiled 不释放锁、释放cpu
  • wait 释放锁、释放cpu

Java并发编程 一 线程介绍、创建线程的4种方式、线程常用方法、两阶段终止状态、线程的五种/六种状态_第9张图片

3.8、 守护线程

  • Java进程中有多个线程在执行时,只有当所有非守护线程都执行完毕后,Java进程才会结束。但当非守护线程全部执行完毕后,守护线程无论是否执行完毕,也会一同结束。

注意:

  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等

四、 线程状态

五种状态

  • 操作系统的层面上
    Java并发编程 一 线程介绍、创建线程的4种方式、线程常用方法、两阶段终止状态、线程的五种/六种状态_第10张图片
  1. 初始状态,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();,还未与操作系统线程关联
  2. 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
  3. 运行状态指线程获取了CPU时间片,正在运行
    1. 当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
  4. 阻塞状态
    1. 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
    2. 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    3. 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
  5. 终止状态表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

六种状态

  • 这是从 Java API 层面来描述的
  • 根据Thread.State 枚举,分为六种状态

Java并发编程 一 线程介绍、创建线程的4种方式、线程常用方法、两阶段终止状态、线程的五种/六种状态_第11张图片

新建状态运行状态(就绪状态, 运行中状态)、阻塞状态等待状态定时等待状态终止状态

  • NEW (新建状态) 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE (运行状态) 当调用了 start() 方法之后,注意,Java API 层面的RUNNABLE 状态涵盖了操作系统层面的 【就绪状态】、【运行中状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
  • BLOCKED (阻塞状态)WAITING (等待状态)TIMED_WAITING(定时等待状态) 都是 Java API 层面对【阻塞状态】的细分,如sleep就位TIMED_WAITINGjoinWAITING状态。后面会在状态转换一节详述。
  • TERMINATED (结束状态) 当线程代码运行结束
@Slf4j(topic = "c.TestState")
public class TestState {
    public static void main(String[] args) throws IOException {
        Thread t1 = new Thread("t1") {	// new 状态
            @Override
            public void run() {
                log.debug("running...");
            }
        };

        Thread t2 = new Thread("t2") {
            @Override
            public void run() {
                while(true) { // runnable 状态

                }
            }
        };
        t2.start();

        Thread t3 = new Thread("t3") {
            @Override
            public void run() {
                log.debug("running...");
            }
        };
        t3.start();

        Thread t4 = new Thread("t4") {
            @Override
            public void run() {
                synchronized (TestState.class) {
                    try {
                        Thread.sleep(1000000); // timed_waiting 显示阻塞状态
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t4.start();

        Thread t5 = new Thread("t5") {
            @Override
            public void run() {
                try {
                    t2.join(); // waiting 状态
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t5.start();

        Thread t6 = new Thread("t6") {
            @Override
            public void run() {
                synchronized (TestState.class) { // blocked 状态
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t6.start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("t1 state {}", t1.getState());
        log.debug("t2 state {}", t2.getState());
        log.debug("t3 state {}", t3.getState());
        log.debug("t4 state {}", t4.getState());
        log.debug("t5 state {}", t5.getState());
        log.debug("t6 state {}", t6.getState());
    }
}

你可能感兴趣的:(Java并发编程,线程,创建线程的方式,Thread,Runnable,并发编程)