·前言
本篇文章会介绍 Java 中的 Thread 类常见的构造方法、常见属性及其常见的用法,会列举一些代码例子来更直观的感受每个方法是作用,文章还会介绍 Java 中线程的状态及他们之间的转换关系,那么下面就开始今天的分享吧~~
一、常见构造方法
Thread 类的常见构造方法如下表所示:
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并对线程命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并对线程命名 |
1. Thread()
下面对 Thread() 这个构造方法进行演示,代码与运行结果如下:
public class ThreadDemo2 {
public static void main(String[] args) {
// 实例化一个线程对象
Thread t = new Thread();
// 真正的去申请系统线程,参与 CPU 调度
t.start();
}
}
通过运行结果可以看出没有任何的响应,原因在通过对 Thread 类的源码进行观察找到了,如下图所示(主要源码部分摘取):
Thread类实例化的对象,在调用 start 方法后会自动调用 run 方法,然而这里 run 方法什么都不执行,所以利用上面代码的运行结果没有任何响应。
2.Thread(Runnable target)
Thread(Runnable target) 这个构造方法在前面创建线程时使用过,具体代码及运行结果如下所示:
// 创建一个 Runnable 的实现类,并实现 run 方法
// Runnable 主要描述的是线程的任务
class MyThread2 implements Runnable {
@Override
public void run() {
System.out.println("hello thread!!!");
}
}
public class ThreadDemo4 {
public static void main(String[] args) {
// 实例化 Runnable 对象
Runnable runnable = new MyThread2();
// 实例化线程对象,并绑定任务
Thread t = new Thread(runnable);
// 真正的去申请系统线程,参与 CPU 调度
t.start();
}
}
3.Thread(String name)
每个线程创建后都有名字,通过 jconsole 可以看见每个线程的名字,为了区分每个线程,在创建线程的时候可以使用 Thread(String name) 构造方法传一个字符串作为线程的名字,由于直接使用 Thread(String name) 这个构造方法,其中没有重写 run 方法所以导致线程启动后没有进行任何操作就被销毁了,这里就无法看见线程的名字了。
4.Thread(Runnable target, String name)
这里的 Thread(Runnable target, String name) 构造方法是与 Thread(Runnable target) 构造方法很相似的,就是这个构造方法在创建线程时手动传了一个字符串作为线程的名字,这个我们可以通过创建一个 Runnable 的实现类,实现一个 run 方法,在 run 方法中设置一个死循环,不让这个线程结束,来观察这个构造方法的效果,代码及运行结果如下所示:
// 创建一个 Runnable 的实现类,并实现 run 方法
// Runnable 主要描述的是线程的任务
class MyThread2 implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("hello " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo4 {
public static void main(String[] args) {
// 实例化 Runnable 对象
Runnable runnable = new MyThread2();
// 实例化线程对象,并绑定任务
Thread t = new Thread(runnable,"这是我的线程1");
// 真正的去申请系统线程,参与 CPU 调度
t.start();
}
}
下面通过 jconsole 来查看一下该线程的名字,如下图所示:
这里对于给线程起名和线程名称放在下面常见属性中进行介绍。
二、常见属性
Thread 类的常见属性如下表所示:
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
1.ID
ID 是线程的唯一标识,不同的线程不会重复。
2.名称
线程的名称主要是各种调试工具会用到,在我们上面常见构造方法中也提到了有关线程的起名与线程名称的一点介绍,这里再进行更细致的介绍,大致有以下几点:
- 如果我们不手动给线程起名,默认的线程名是按照 Thread-0、Thread-1、Thread-2……这种规律来进行命名;
- 给不同的线程起不同的名字,这对于线程的执行是没有什么影响的,起名主要是为了在代码出问题时方便调试;
- 线程之间的名字可以重复,一般在同一个工作中,需要用到多个线程时,就可以把这几个线程都起一样的名字;
- 在对线程起名字时也不要乱起,最好有一定的描述性。
3.状态
在学习进程时,我们了解到进程有状态(就绪状态、阻塞状态和运行状态),在线程中也有状态, Java 中对线程的状态又进一步的区分(比系统原生的状态要更丰富一些),关于线程状态的内容在文章后面还会详细的介绍。
4.优先级
优先级高的理论上来说更容易被调度到,但是在 Java 中,设置优先级效果不是很明显,这是由于系统的随机调度。
5.是否后台线程
和后台线程相对的还有前台线程,前台线程的运行会阻止进程结束,后台线程的运行不会阻止进程结束,所以关于是否后台线程要记住一点:JVM会在一个进程的所有前台线程结束后才会结束运行。下图是上面演示 Thread(Runnable target, String name) 构造方法的使用中用 jconsole 获取的线程信息,根据下图可以发现里面没有 main 线程,这是因为 main 线程已经执行结束了,但是该进程没有结束,是因为还有我们创建的前台线程没有结束,所以进程不会结束。
我们可以使用 setDaemon() 方法来设置我们创建的线程为后台线程(不设置默认是 false 代表创建的线程默认是前台线程),这样不管我们创建的线程有没有执行完,只要 main 线程执行完毕就会结束,对此我们对上面代码进行一些修改,修改代码及运行结果如下所示:
// 创建一个 Runnable 的实现类,并实现 run 方法
// Runnable 主要描述的是线程的任务
class MyThread2 implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("hello " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo4 {
public static void main(String[] args) {
// 实例化 Runnable 对象
Runnable runnable = new MyThread2();
// 实例化线程对象,并绑定任务
Thread t = new Thread(runnable,"这是我的线程1");
// 设置 t 为后台线程
t.setDaemon(true);
// 真正的去申请系统线程,参与 CPU 调度
t.start();
// 休眠一秒,让创建的线程先执行一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
这里我们创建的线程中写了一个死循环,然而在 main 线程执行结束后也一起结束了,这也进一步验证了一个进程的所有前台线程结束后就会会结束运行,在用 jconsole 获取线程中我们也看见了很多其他线程,这些线程也都是后台线程,后台线程不会阻止进程结束,前台线程会阻止进程结束。
6.是否存活
这里是否存活可以简单理解为 run 方法是否运行结束了,具体演示代码及运行结果如下:
public class ThreadDemo7 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("hello thread!!!");
// 休眠一秒,用来获取线程的存活的反馈
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
System.out.println("线程 t 未启动:->" + t.isAlive());
t.start();
System.out.println("线程 t 未执行完:->" + t.isAlive());
// 休眠两秒秒,确保线程 t 的 run 方法执行完毕
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程 t 执行完毕:->" + t.isAlive());
}
}
isAlive() 方法显示的线程存活信息表示了,内核中的线程(TCB)是否还存在,在我们的 Java 代码中定义的线程对象(Thread)实例,虽然表示一个线程,但是这个对象的生命周期和我们内核中的 TCB 的生命周期是完全不一样的,在我们使用 new + 构造方法时创建线程对象 t 时,此时 t 对象有了,但是内核 TCB 还没有,所以调用 isAlive() 方法得到的就是 false ,执行完 t.start() 时,才真正的在内核中创建出这个 TCB ,此时调用 isAlive() 方法得到的就是 true ,当线程 t 的 run 方法执行完,此时内核中的线程也就结束了(内核中的TCB就释放了),但是 t 变量可能还存在,不过因为 TCB 已经释放所以调用 isAlive() 方法得到的就是 false。
7.是否被中断
关于线程是否被中断在文章后面会进一步说明。
三、启动线程
1. start() 方法
Thread 类使用 start() 方法来启动一个线程,对于同一个 Thread 对象来说,start() 方法只能调用一次,对同一个 Thread 对象调用多次 start() 方法,代码及运行结果如下所示:
public class ThreadDemo8 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("hello thread");
});
t.start();
t.start();
}
}
所以想要启动更多的线程就要创建新的的线程对象,调用 start() 方法会创建出新的线程,本质上是 start() 方法会调用系统的 API 来完成创建线程的操作。
2. start() 方法与 run() 方法的区别
这个问题的引入应该与下面这两段代码的执行有一定的关系。
调用 start() 方法,代码与运行结果如下:
public class ThreadDemo8 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("hello thread");
});
t.start();
}
}
调用 run() 方法,代码与运行结果如下:
public class ThreadDemo8 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("hello thread");
});
t.run();
}
}
通过上面两段代码及各自的运行结果可以看出,不管是调用 start() 方法还是调用 run() 方法,得到的运行结果都是一样的,此时就会引入 start() 方法和 run() 方法有什么区别呢?这样的问题,我再通过下面的两段代码进一步区分一下这两个方法。
调用 start() 方法,代码与运行结果如下:
public class ThreadDemo8 {
public static void main(String[] args) {
Thread t = new Thread(()->{
while (true) {
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while (true) {
System.out.println("hello main!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
调用 run() 方法,代码与运行结果如下:
public class ThreadDemo8 {
public static void main(String[] args) {
Thread t = new Thread(()->{
while (true) {
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.run();
while (true) {
System.out.println("hello main!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
再通过上面这两段代码及他们的运行结果可以看出,调用 start() 方法是两个线程的死循环在一起并发的执行,而调用 run() 方法时,只会执行 run() 方法中的死循环,由此可以看出他们两个是有区别的,他们的区别如下:
调用 start() 方法是创建了一个新的线程,并调用其中的 run() 方法,此时执行 run() 中代码与执行 main 中代码的是两个线程,而直接调用 run() 方法,只是是在 main 线程中调用,这里不涉及到创建新的线程,这也是他们本质的区别。
四、中断线程
当一个线程正在运行时,我们可不可以提前将这个正在运行的线程结束呢?这就是线程中断,这里我来介绍两种中断线程的方法。
1.引入标志位
这里是通过共享标记来进行两个线程的沟通,来达到使线程正常退出,也就是执行完 run() 方法后线程终止,演示代码与运行结果如下:
public class ThreadDemo9 {
private static boolean isQuit = false;
public static void main(String[] args) {
Thread t = new Thread(()->{
while (!isQuit) {
System.out.println("hello thread!!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("线程 t 工作完毕");
});
t.start();
// 让 t 线程执行两秒后结束
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
isQuit = true;
System.out.println("让 t 线程结束");
}
}
通过 main 线程修改 isQuit 标志位的值为 true 来中断线程 t 的运行。这里 main 线程想要 t 线程结束,大前提一定是 t 线程的代码对这样的逻辑有所支持,而不是说 t 线程这里的代码随便怎么写都可以提前结束,如果 t 线程中的代码没有配合, main 线程是无法让 t 提前结束的。
2.调用 interrupt() 方法
上面中断线程的写法并不是很优雅,所以 Thread 类还提供了一种更优雅的选择,那就是调用 interrupt() 方法,代码及运行结果如下:
public class ThreadDemo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
// isInterrupted() 方法相当于标志位 isQuit
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread!!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程 t 工作完毕");
});
t.start();
// 让 t 线程执行三秒后结束
Thread.sleep(3000);
// 相当于设置 isQuit 为 true
t.interrupt();
System.out.println("让 t 线程结束");
}
}
上述代码中 isInterrupted() 方法相当于用来判定标志位,interrupt() 方法相当于设置标志位,通过运行结果可以看出,这里并没有按照预想的那样中断线程,主要原因是 sleep 引起的,在上面代码执行 sleep 的过程中,调用了 Interrupt 方法,导致 sleep 休眠时间还没有到,就被提前唤醒了,sleep 被唤醒会做以下两件事:
- 抛出 InterruptedException 异常,被 catch 获取到;
- 清除 Thread 对象的 isInterrupt 标志位。
虽然我们在代码中已经通过 interrupt 方法把标志位设为 true 了,但是 sleep 提前唤醒操作又把标志位重新设回 false 了,此时循环就继续执行了。
想要解决上面的问题很简单,在捕获 sleep 异常的 catch 语句中加上 break 就可以了,此时代码及运行结果如下所示:
public class ThreadDemo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
// isInterrupted() 方法相当于标志位 isQuit
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread!!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
System.out.println("线程 t 工作完毕");
});
t.start();
// 让 t 线程执行三秒后结束
Thread.sleep(3000);
// 相当于设置 isQuit 为 true
t.interrupt();
System.out.println("让 t 线程结束");
}
}
上图运行结果中的异常信息是由 e.printStackTrace(); 这条语句打印的,如果感觉看着别扭可以将这条语句删除,就不会出现异常的信息了。
五、等待线程
在我们多线程代码中,由于系统的随机调度,抢占式执行,每个线程的执行顺序是不能确定的,虽然线程的调度是无序的,但是我们有的时候又希望能够控制线程之间的结束的先后顺序,此时调用 join 方法就是一种控制方式,join 这个方法会让等待线程阻塞,一直阻塞等到被等待线程执行完 run 方法,比如, t2 线程等待 t1 线程,此时,就一定是 t1 先结束 t2 后结束, join 方法就可能使 t2 线程阻塞。利用下面代码来对 join 方法做进一步的介绍与演示,代码及运行结果如下:
package thread_test;
public class ThreadDemo10 {
public static long num1 = 0;
public static long num2 = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5_000_00; i++) {
num1 += i;
}
});
Thread t2 = new Thread(()->{
for (int i = 5_000_00; i <= 1_000_000; i++) {
num2 += i;
}
});
t1.start();
t2.start();
// 等待 t1 执行结束
t1.join();
// 等待 t2 执行结束
t2.join();
long sum = num1 + num2;
System.out.println("num1 + num2 = " + sum);
}
}
上述代码是利用两个线程来求从 0 加到 1000000 的值,这里就需要我们 main 线程等待 t1 线程与 t2 线程都执行完再讲两个线程算出来的结果进行汇总得出最终的结果。
上面的介绍让大家理解等待线程是为了什么,这里我再对 join 方法进一步介绍:
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,但是等待时间的精度更高 |
在前面代码中,我们调用的都是没有参数的 join 方法,这也可以叫做“死等”,这种做法不是很科学,如果代码因为死等导致程序卡住,将无法处理后续逻辑,就会出现很严重的问题,所以在上面表格中还列举了有关 join 方法的两种形式,这两种是带有等待时间的等,等时间达到了超时时间就不再继续等待了,同时 interrupt 方法也可以把阻塞等待的 join 方法提前唤醒。
这里需要明确,哪个线程调用 join 方法,哪个线程阻塞,此时阻塞就明确了两个线程结束的先后顺序。
六、获取当前线程引用
获取当前线程引用的方法如下所示:
方法 | 说明 |
public static Thread currentThread(); | 返回当前线程对象的引用 |
这个方法在前面代码中也有所应用,再以下列代码及运行结果做进一步演示,如下所示:
public class ThreadDemo12 {
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println(t.getName());
}
}
Thread.currentThread() 获取到的是当前线程的引用(Thread 的引用),上面代码是在 main 线程中调用的这个方法,所以获取到的是 main 线程的引用,所以打印 main 线程的名字运行结果显示的就是 main了。
如果当前线程继承 Thread ,那么直接使用 this 就可以拿到线程的实例;如果当前线程是实现 Runnable 接口或者是使用 lambda 的方式来创建的,就不能使用 this 了,此时 this 已经不再指向 Thread 对象了,这时就只能调用 Thread.currentThread() 方法来获取当前线程的引用了。
七、休眠当前线程
休眠当前线程用到的是 sleep 方法,在上面代码中已经多次用到这个方法,这里再针对 sleep 提前唤醒清空标志位这点进行进一步的介绍:
sleep 清空标志位这一点给我们留了很大的操作空间, 比如我们在代码中需要一个线程休眠一秒,结果一秒还没有到,就要终止这个线程,这两个操作相当于是自相矛盾的,此时,我们希望写更多的代码来对这样的情况进行具体的处理,这些代码就是写在 catch 语句中的,大致分为以下这三种情况:
- 让线程立即结束(相当于在 catch 语句中直接加上 break);
- 让线程不结束,继续运行 (不加 break);
- 让线程执行一些其他逻辑再结束(在 catch 语句中,写一些其他代码之后再 break)。
八、线程状态
线程的状态是一个枚举类型 Thread.State 通过下面代码就可以将线程的状态枚举出来,代码及运行结果如下所示:
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
如上图运行结果所示列举出了线程的状态,下面我再对每个状态进行一个介绍:
- NEW:Thread 对象已经创建好了,但是还没有调用 start 方法在系统中创建线程;
- RUNNABLE:表示这个线程正在 CPU 上执行,或者准备就绪随时可以去 CPU 上执行;
- BLOCKED:由于锁竞争,引起了阻塞;
- WAITING:不带时间的阻塞(死等),必须满足一定的条件才会解除阻塞,调用 join 方法或者 wait 方法都会进入 WAITING;
- TIMED_WAITING:指定时间的阻塞,就在到达一定时间时间之后自动解除阻塞,调用 sleep 方法或者使用带有超时时间的 join 方法都会进入TIMED_WAITING;
- TERMINATED:Thread 对象仍然存在,但是系统内部的线程已经执行完毕了。
介绍完每个状态的意思,下面我画一个粗略的图来描述一下各个状态之间的转换关系,大致如下图所示: 了解这些状态的最大作用就是在我们写多线程代码出现 bug 的时候,可以作为一个重要的参考依据,比如我们写的代码运行时卡住了,就意味着一些关键的线程阻塞了,这时我们可以用 jconsole 来观察线程的状态,可以分析出一些原因,还有在前面提到一个 Thread 对象只能 start 一次,这个也是和线程状态密切相关的,只有处于 NEW 状态的线程才能调用 start 方法。
·结尾
文章到此就要结束了,本篇文章内容偏多,但是都是一些基础的内容,不过这些内容也十分重要,这是我们在用 Java 进行多线程编程的基石,如果本篇文章对您有所帮助,希望能给我个三连支持一下吧~~您的支持就是我最大的动力,当然,如果您本篇文章的某个知识点还有疑问,可以在评论区,或者私信中与我讨论,我们下一篇文章再见吧┏(^0^)┛!!!