一、认识线程-Thread类
一个线程就是一个“执行流”。每个线程都可以按照顺序执行自己的代码,而多个线程可以 "同时" 执行多份代码。线程本身是操作系统中的概念。操作系统内核实现了线程这样的机制,并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库)。而Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装。
Thread 类是 JVM 用来管理线程的一个类。在Java中,每个线程执行流都是通过Thread类的对象来描述的。JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
使用Thread类,我们可以创建和管理多个线程,并控制它们的执行顺序、优先级、暂停、恢复等。
1、Thread类的常见构造方法
Thread() :无参的构造方法。
Thread(Runnable target) :Runnable是一个接口类型。该构造方法需要一个Runnable类型的target参数。具体如何使用,在下文“创建线程”的部分会详细说明。
Thread(String name) / Thread(Runnable target, String name):在创建线程的同时自定义一个线程对象名。这个操作相当于是给线程起了一个别名,方便后续查找辨认该线程,不影响程序的正常运行。不指定名称,系统也会给线程一个默认的名称。我们可以用 jconsole 工具来查看Java程序中的线程:
先用Java代码创建两个线程(如何创建线程仍然会在下文说明),并运行程序:
在 jconsole 中可以看到:
这里的Thread-0,Thread-1就是系统默认给我们线程的命名。
此时,如果我们给线程 t1 指定一个name为"hello_t1",再次用 jconsole 查看线程情况:
可见此时,t1 的名称就显示为我们自定义的"hello_t1"了。而 t2 未自定义命名,因此还是默认的名称。但是,起名只是方便标识,无论名称如何,都不影响程序的正常运行。
2、Thread 的几个常见属性
- ID 是线程的唯一标识,不同线程不会重复。
System.out.println(t1.getId()); System.out.println(t2.getId());
- 名称就是我们上面提到的线程的命名,通常各种调试工具会用到。
- 状态表示线程当前所处的一个情况,下文中我们会进一步说明。
- 是否存活,可以简单地理解为 :run 方法是否运行结束了
- 优先级高的线程理论上来说更容易被调度到,但并不绝对。因为优先级只是“建议”性质的,不是强制性质的。优先级高意味着“建议”操作系统优先调度某线程,但实际到底要不要优先调度,还是取决于操作系统。
- 线程分后台线程和前台线程。关于后台线程:JVM会在一个进程的所有非后台线程(也就是前台线程)结束后,才会结束运行。创建的线程默认是前台线程,main线程也是一个前台线程。
Daemon 表示一个后台进程(也叫守护进程)。.isDaemon()方法可以获得线程是前台进程还是后台进程。true表示是后台进程,false表示是前台进程。
.setDaemon()方法可以手动设置一个线程为前台线程或后台线程。具体演示如下:
t1是后台线程,进程随着main线程(前台线程)的终止而终止
如果我们将 t1.setDaemon(true) 语句删除,会怎样呢?此时t1就不再是后台进程,而是一个前台进程了,只有等到t1线程也结束,整个进程才会结束。但由于t1中有死循环,因此进程不会结束。
t1是前台线程,由于t1中有死循环,进程没有终止
二、Thread类的基本用法
1、创建线程
方法一 继承 Thread 类
通过继承 Thread 类的方式来创建线程,主要分为 4 步:
- 自定义类 MyThread 类 继承 Thread类。
- 在 MyThread类 中重写 run() 入口方法。
- 在 main 中创建(new)MyThread类 的实例 t 。
- 通过 t 调用 start() 方法,启动线程。
具体代码演示如下:
// 1、通过继承Thread类来创建线程 class MyThread extends Thread { // 2、重写 run() 方法 @Override public void run() { System.out.println("i am t!"); } } public class Test { public static void main(String[] args) { // 3、创建 MyThread 实例 MyThread t = new MyThread(); // 4、调用 实例t 的 start() 方法 t.start(); // main 线程中的方法 System.out.println("i am main!"); } }
程序运行结果:
方法二 实现 Runnable 接口
与上面的继承Thread类的方法类似,通过 Runnable 接口创建线程,主要有以下 4 个步骤:
- 实现 Runnable 接口。
- 重写run()方法。
- 创建 Thread 类实例 , 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数。Thread有多个重载的构造方法,其中有一个是通过实现过 Runnable 接口的实例来构造,如图:
因此,我们直接在构造器的实参处,new一个实现了Runnable接口的类的实例即可。
- 调用 start() 方法启动线程。
// 1、通过实现Runnable接口来创建线程 class MyRunnable implements Runnable { // 2、重写 run() 方法 @Override public void run() { System.out.println("i am t!"); } } public class Test { public static void main(String[] args) { // 3、创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数 Thread t = new Thread(new MyRunnable()); // 4、调用 实例t 的 start() 方法 t.start(); // main 线程中的方法 System.out.println("i am main!"); } }
包括该方法在内的所有创建线程的程序运行结果均同上。
方法三 使用匿名内部类
通过匿名内部类实现也有两种方式,一种是使用Thread的匿名内部类,另一种是使用Runnable的匿名内部类:
匿名内部类创建 Thread 子类对象
public class Test { public static void main(String[] args) { //匿名内部类 Thread Thread t = new Thread(){ @Override public void run() { System.out.println("i am t!"); } }; t.start(); // main 线程中的方法 System.out.println("i am main!"); } }
匿名内部类创建 Runnable 子类对象
特别注意:Runnable匿名内部类以及大括号的书写位置。由于Runnable实例是要作为Thread构造方法参数传入的,因此Runnable的匿名内部类应当写在new Thread()的括号内。
public class Test { public static void main(String[] args) { // 匿名内部类 Runnable // 注意:Runnable实例作为Thread构造器的参数传入 Thread t = new Thread(new Runnable() { @Override public void run() { System.out.println("i am t!"); } }); t.start(); // main 线程中的方法 System.out.println("i am main!"); } }
方法四(最常用) 采用lambda表达式
查看Runnable接口的Java源码可以发现,Runnable接口是一个函数式接口。因此,我们可以通过lambda表达式来简便地创建一个线程。
public class Test { public static void main(String[] args) { // lambda表达式 Thread t = new Thread(() -> { System.out.println("i am t!"); }); t.start(); // main 线程中的方法 System.out.println("i am main!"); } }
2、启动线程 start()
(1)start()方法
start()方法是启动线程的方法,调用该方法会使线程进入就绪状态,等待CPU分配时间片后开始执行。run()方法是线程的执行体,它包含了线程要执行的代码;当调用start()方法启动线程后,线程会在独立的执行路径上自动执行run()方法中的代码(也就是说,当调用start()启动线程后,系统会自动调用run()方法执行线程的执行体)。
但要注意的是,run()方法并不标识新的线程的创建;调用 start 方法,才真的在操作系统的底层创建出了一个线程。
start()方法的使用:在创建完线程后,通过线程的实例调用start()即可。上面已经演示了很多,这里就不再多说。
(2)start()方法与run()方法的区别--代码演示
事实上,也可以通过线程对象的实例调用run()方法:
但这两个方式有本质区别。
t1.start() 方法,才是真正创建了一个新的线程。当我们运行如下代码,可以看到,"i am t!"与"i am main!"两句交替打印,这时t1线程与main线程并发执行的结果,两个线程即不断打印"i am t!"和不断打印"i am main!"是同时执行的:
但当我们对比着执行下面的代码,会发现程序只打印了"i am t!",并不打印"i am main!"。道理很简单:此时并没有创建出新的线程,打印"i am t!"与打印"i am main!"是同一线程中的先后关系。但由于打印"i am t!"是一个死循环,所以程序就卡在了这里,无法向下执行到打印"i am main!"了。
上述代码与下面这样写的实际运行效果相同:
(3)start()方法与run()方法的区别--总结
由上面的代码演示可知:在调用start()方法启动线程后,系统会自动调用线程的run()方法;如果直接调用run()方法,那么线程不会启动,而是在当前线程中直接执行run()方法中的代码,这种情况下不会有新的线程产生。
因此,run()方法只是普通的方法调用,而start()方法则会创建一个新的线程并启动它,让它在新的线程中执行run()方法中的代码。正确使用start()方法可以实现并发执行多个任务,从而提高程序的性能。
概括来说有如下几点区别:
a. 作用功能不同:
- run方法的作用是描述线程具体要执行的任务。
- start方法的作用是真正的去申请系统线程。
b. 运行结果不同:
- run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
- start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。
ava多线程基础-4:详解Thread类及其基本用法(二)+
https://developer.aliyun.com/article/1520504?spm=a2c6h.13148508.setting.14.61564f0er7vvAy