Java 17 多线程 Thread 的基础知识点
这节开始说说 Java 中的 多线程 ,在说多线程之前,先说说这些基础的概念。
说 线程 前,先说说进程。
“进程”这一术语在20世纪60年代初期首先于美国麻省理工学院的MULTICS系统和 IBM公司 的CTSS/360系统中引入。进程是操作系统中的一个最基本也是最重要的概念。
对于进程而言,一般具有 5 个特征:动态性、并发性、独立性、异步性、结构特征。
因为操作系统没有真正的并行,所以对于进程来说会有不同的运行状态,包含 3 种基础状态:就绪状态、执行状态、阻塞状态。也就是说频繁的切换不同的进程让你看起来像是多个进行同时进行。
线程的概念
自从 20 世纪 60 年代提出进程的概念后,进程就是在操作系统中作为能独立运行的基本单位。接着来到了 20 世纪 80 年代中期,当时的前辈们又提出了比进程更小的能独立运行的基本单位 “线程”。线程的提出为了提高系统中程序并发执行的程度,从而可以提供系统的吞吐量。
对于线程而言,是为了使程序能够并发执行。与之对应的,线程也同样也有就绪、阻塞和执行三种状态。
并且对于线程有不同的属性。
- 都有一个唯一的标识符。
- 不同的线程也可以执行相同的程序。
- 同一个进程中的各个线程共享对应进程的内存地址空间。
- 线程被创建后就有对应的 生命周期 ,直到终止,线程在生命周期内会经历阻塞状态、就绪状态和执行状态等各种状态变化。
以上都是些基础概念,更多详细的可以参考操作系统中的进程与线程章节,更加详细。
说了这么多现在开始来看看 Java 中线程是怎么操作的。
在 Java 中线程有 6 种状态:
- New(新创建)
- Runnable(可运行)
- Blocked(被阻塞)
- Waiting(等待)
- Timed waiting(计时等待)
- Terminated(被终止)
创建线程在 Java 中有三种方式,1. 继承 Thread 类创建线程;2. 实现 Runnable 接口的 run 方法创建线程;3. 使用 Callable 和 Future 创建线程。
首先先来看看使用 Thread 创建的方式。
Thread
类的定义:
public class Thread implements Runnable
根据线程的状态可以知道,线程想要使用必须先创建。如何进行创建呢。 看下该类的 构造函数 有哪些。
构造器
构造器 |
描述 |
Thread() |
分配一个新的 Thread 对象。 |
Thread(Runnable target) |
根据传入的 Runnable,分配一个新的 Thread 对象。 |
Thread(Runnable target, String name) |
根据传入的 Runnable,和线程名称,分配一个新的 Thread 对象。 |
Thread(String name) |
传入线程名称,分配一个新的 Thread 对象。 |
Thread(ThreadGroup group, Runnable target) |
根据线程分组和Runnable,分配一个新的 Thread 对象。 |
Thread(ThreadGroup group, Runnable target, String name) |
分配一个新的 Thread 对象,使其以 target 作为其运行对象,以指定的名称作为其名称,并属于 group 所引用的线程组。 |
Thread(ThreadGroup group, Runnable target, String name, long stackSize) |
分配一个新的 Thread 对象,使其以 target 作为其运行对象,以指定的名称作为其名称,并属于由 group 引用的线程组,并具有指定的堆栈大小。 |
Thread(ThreadGroup group, Runnable target, String name, long stackSize, boolean inheritThreadLocals) |
分配一个新的 Thread 对象,使其以 target 作为其运行对象,以指定的名称作为其名称,属于由 group 引用的线程组,具有指定的 stackSize,并继承可继承线程局部变量的初始值(如果 inheritThreadLocals)是真的。 |
Thread(ThreadGroup group, String name) |
根据线程分组和线程字符名字,分配一个新的 Thread 对象。 |
为了更好的区分线程的关系,先看几个基础的方法,以及方法对应的含义。
线程的信息方法
修饰符和类型 |
方法 |
描述 |
long |
getId() |
返回此线程的标识符。 |
final String |
getName() |
返回此线程的名称。 |
final int |
getPriority() |
返回此线程的优先级。 |
Thread.State |
getState() |
返回此线程的状态。 |
final ThreadGroup |
getThreadGroup() |
返回该线程所属的线程组。 |
创建一个默认的对象。
Thread thread = new Thread();
虽然能够创建对象,但是对于创建的默认构造, 并没有实际执行的实现,对于 Thread 是使用 start 启动一个线程,但是实际的线程实现却是 run 方法。
所以默认构造即使调用 start 方法, 也没有任何可以执行的方法体。但是也可以查看线程的一些基础的知识点。
线程的启动和实现
修饰符和类型 |
方法 |
描述 |
void |
run() |
如果该线程是使用单独的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法; 否则,此方法不执行任何操作并返回。 |
void |
start() |
使该线程开始执行; Java 虚拟机 调用该线程的 run 方法。 |
代码示例如下,显示线程的分组、线程的标识符、线程的名字以及线程的状态。
所以对于这种方式,可以直接使用子类实现 Thread 的 run 方法即可使用另一个线程启动应用。
举例说明:
class MyThread extends Thread {
@Override
public void run() {
for (int i =; i < 100; i++) {
System.out.print(" T" + i);
}
}
}
public class Thread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for (int i =; i < 100; i++) {
System.out.print(" M" + i);;
}
}
}
运行之后, 查看效果会发现。注意查看其中的效果。
查看 Thread(Runnable target) 构造函数。
Thread thread = new Thread(()->{
for (int i =; i < 100; i++) {
System.out.print(" T" + i);
}
});
thread.start();
for (int i =; i < 100; i++) {
System.out.print(" M" + i);;
}
注意上面的 ()->{} Lambda 表达式,等同于下面的代码:
new Thread(new Runnable() {
@Override
public void run() {
//待执行的代码
}
});
对于分组,可以认为是为了标志一组线程。暂时不演示这个,先做简单的例子。
对于线程来说,有个 Sleep 静态方法 ,代表着当前线程需要等待多久时间继续运行。
线程的睡眠
修饰符和类型 |
方法 |
描述 |
static void |
sleep(long millis) |
使当前执行的线程休眠(暂时停止执行)指定的毫秒数,取决于系统计时器和调度程序的精度和准确性。 |
static void |
sleep(long millis, int nanos) |
使当前执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统计时器和调度程序的精度和准确性。 |
在 Thread 中有一个 join 方法,等待着线程的结束。
线程的等待
修饰符和类型 |
方法 |
描述 |
final void |
join() |
等待这个线程死掉。 |
final void |
join(long millis) |
最多等待几毫秒让该线程终止。 |
final void |
join(long millis, int nanos) |
最多等待毫秒加上纳秒以使该线程终止。 |
通常情况下, 如果一个子线程的运行时间比主线程时间长,这个时候,因为主线程的结束, 会关闭子线程。这个时候,可以使用 join 方法等待线程的结束,也可以直接默认的毫秒或者加上纳秒的时间结束线程。演示案例如下:
其中
customThread.join();
修改成
customThread.join();
重新执行查看效果:
这个时候, 你会发现,其中的差别,如果不指定时间的时候,将一直等待这该线程执行完毕,指定之后,就会继续执行。这个时候, 如果有流媒体之类的,已经发起执行, 会可能导致提前结束主线程。
线程如果已经执行如何进行中断, 以及如何判断中断的状态呢,在 API 中可以看到 stop 方法已经是过期的,并且该方法非安全性的,所以不在使用这个方法。
线程的中断
中断相关的方法有:
修饰符和类型 |
方法 |
描述 |
void |
interrupt () |
中断这个线程。 |
boolean |
isInterrupted() |
测试此线程是否已被中断。 |
final boolean |
isAlive() |
测试此线程是否存活。 |
代码如下:
public class Thread {
public static void main(String[] args) throws Interrupted Exception {
CustomThread customThread = new CustomThread();
customThread.start();
Thread.sleep();
customThread.interrupt();
System.out.print("end main");
}
}
class CustomThread extends Thread {
@Override
public void run() {
System.out.println("CustomThread run");
for (int i =; i < 100; i++) {
try {
if (this.isInterrupted()) {
break;
}
Thread.sleep();
} catch (Exception e) {
System.err.println("terr:" + e.getMessage());
break;
}
System.out.print(" T" + i);
}
}
}
代码运行的结果。
简单的说一下,需要注意的是,这里的中断只是做了中断的标志,具体要不要中断,以及中断之后怎么处理,还需要我们自己根据实际的业务进行判断。
生命周期演示
直接看演示的效果吧。 为了出现演示效果,另外又创建了一个线程。用来睡眠等待这第一个线程的结束。还有被阻塞和等待这两个生命周期状态不好演示,如果有办法更方便的演示,可以评论区留言,共同学习。
多线程的 Thread 基础使用先这样, 当然线程的知识还有很多, 另外会继续开篇再来说明。
感谢您的阅读,持续输出全栈知识,当前进度是 Java 的 0 到实战系列。欢迎关注收藏点赞。