这里是Themberfue
· 上一节我们使用了最基础的方式来创建一个线程,并实现了多线程代码的运行。
· 这一节我们更进一步的介绍其他创建线程的方式。
Thread
我们先来看一段通过Thread类创建线程的代码
java
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
// 创建匿名内部类,继承了Thread类
Thread t = new Thread() {
@Override
public void run() {
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("My main");
Thread.sleep(1000);
}
}
}
· 上述代码创建与之前说到的略有不同,这次而是直接通过new Thread类来实现。
· 但其实本质上和上节的无二区别,这是通过匿名内部类创建,该匿名内部类继承了Thread,并重写了 run 方法,随后启动该线程。
· 随后启动线程,线程执行的逻辑就是匿名内部类重写run方法里的代码逻辑。
· 这样写的好处之一就是不用创建过多的类,方便后续更改其执行逻辑。
Runnable
· 前面我们提到,直接在Thread类里重写run方法,执行里面的逻辑,那么代码的耦合性会增大,这是不利于后续进一步编程的。
· 所以实现Runnable接口重写run方法,随后通过Thread的构造方法,执行run方法里的逻辑。这是一种解耦合的方式,它将线程和任务的概念分开了,不至于耦合在一起。
java
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
// 匿名内部类,实现了Runnable接口
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread t = new Thread(runnable);
t.start();
while (true) {
System.out.println("My main");
Thread.sleep(1000);
}
}
}
· 与前一个代码示例类似,也是通过匿名内部类的方式创建一个任务,该匿名内部类实现了Runnable接口,重写了run方法。
· 随后提供Thread类的构造方法将这个任务交给我们创建的这个线程去执行,可以实现解耦合的效果。
Lambda
· Lambda表达式想必大家不陌生,这是一种匿名函数接口,其最主要的用途就是作为 "回调函数"
· 但是在Java中,其必须依托于类才可以使用lambda表达式,这和其他语言还是有区别的
· lambda表达式本质是一个函数式接口( () -> {} ),创建了一个匿名的函数式接口的子类,创建出相应的实例,并且重写了里面的方法
· Thread同样支持lambda表达式的方式重写run方法,由于Runnable是接口,不是一个类,所以不能提供该方法创建
java
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
// 使用lambda表达式
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("My main");
Thread.sleep(1000);
}
}
}
Thread的构造方法
· Thread类提供了丰富的构造方法,等待我们去挖掘,我们先了解四个
|--------------------------------------|---------------------------|
| 方法 | 说明 |
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用Runnable对象创建线程对象 |
| Thread(String name) | 创建线程对象并且给其命名 |
| Thread(Runnable target, String name) | 使用Runnable对象创建线程对象,并且给其命名 |
我们可以在创建线程时给其命名,便于后续出现线程安全问题时,可以快速定位到哪个线程出现了问题。
java
public class Demo6 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "T1"); // 给线程命名
t1.start();
}
}
运行上述代码,通过 jconsole 查看该线程的名字。
Thread提供的方法
· 我们先来看 Thread 的几个常见属性,表格如下
|---------|-----------------|
| 属性 | 获取方法 |
| ID | getId() |
| 名称 | getName() |
| 状态 | getState() |
| 优先级 | getPriority() |
| 是否为后台线程 | isDaemon() |
| 是否存活 | isAlive() |
| 是否被中断 | isInterrupted() |
· ID 是线程的唯一标识,就和MySQL的主键一个性质,在这个进程不可能出现重复的 ID
· 名称可以根据 Thread 的构造方法进行手动设置,默认为 Thread-0,从0开始,依此递增
· 线程的状态主要分为就绪状态和阻塞状态,但其又细分了数个状态,这我们放在以后细说
· 优先级高的线程更容易被系统先调度
· 后台线程是什么?后台线程也称守护线程,它们的存在不影响进程的结束,如果进程结束了,那么它们也随即结束了,在一个进程运行好后,JVM会默认创建几个后台线程
· 手动创建的线程,比如 main 线程,它们默认都是前台线程,如果存在多个前台线程,必须所有前台线程结束后,进程才会结束
· 是否存活?也就是线程此时是否在执行中
· 是否被中断?线程是否被中断后停止运行
后台线程
前面的提到的 后台线程,默认自己创建的线程都是前台线程,不过我们通过 Thread类提供的 setDaemon 来将其设置为后台线程。
java
public class Demo7 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 设置为后台线程(守护线程) 必须在start之前
t.setDaemon(true);
t.start();
for (int i = 0; i < 3; i++) {
System.out.println(1);
Thread.sleep(1000);
}
}
}
· Java 代码中创建的 Thread 对象,和系统中的线程是一一对应的。
· 但是,Thread对象的生命周期和系统中的线程的生命周期可能不是对应的,会存在,Thread对象还存活,但是线程已经被系统销毁的情况。
· 通过如下代码加以演示
java
public class Demo8 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while (true) {
// 查看线程是否运行中
System.out.println(t.isAlive());
Thread.sleep(1000);
}
}
}
· 上述代码中,3秒后,t 线程执行完逻辑就会被操作系统给销毁,但是 main线程在同样打印3 次 t 线程的存活状态后,可能还会出现第四次打印 t 线程的存货状态,切输出为 true
· 这是为什么?不难发现,线程在系统的调度的是随机的,三秒后,虽然 t 线程确实好像执行完了逻辑,但此时 main 线程被调度了,也就打印了 t 线程的存活状态,为 true
· 好比 t 线程还留有一口气,没有完全被销毁
线程终止
· 如果线程正常执行完逻辑,而后线程被销毁,这并不叫 "线程终止" ,这只是线程正常退出了
· 我们可以通过 isInterrupted() 方法来查看线程是否终止了,通过 interrupt() 方法来终止线程
· 不过,在使用上述方法前,我们先来看一个现象,请看下面的代码
java
public class Demo9 {
private static boolean isFinished = false;
public static void main(String[] args) throws InterruptedException {
// boolean isFinished = false;
Thread t = new Thread(() -> {
while (!isFinished) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(3000);
isFinished = true;
System.out.println("t线程结束了");
}
}
· 上述代码,为什么使用成员变量来控制一个线程是否终止,而不是简单的使用局部变量?
· 如果设置为局部变量,那么其必须是常量或者不可变变量,这是为什么?
· lambda 想要使用外面的变量,会触发 "变量捕获" 的语法
· 由于 lambda 本质上是回调函数,其执行时机,可能是很久之后,这是相对计算机的时间的,在完全创建完线程后可能才执行里的代码逻辑。但此时 main 线程的逻辑可能已经结束了,那么就意味着这个变量被销毁了,此时再去捕获的话就会失败。
· 所以 JVM 会在先对这个变量进行一个临时拷贝,拷贝到 lambda 里面,既然是拷贝的变量,那么这两个变量就是形参和实参的关系,一个变量的修改不会影响到另外一个变量,也就不希望对这个变量进行修改
· 所以 JVM 只允许使用常量或者不可变变量
· 若将其设置为成员变量,由于 lambda 本质上是一个函数式接口,相当于一个内部类· 成员变量也就是外部类的成员,内部类本身就可以访问外部类的成员变量,不需要对其进行变量捕获操作
· 成员变量的销毁时交给 GC (garbage collection) 机制来回收的,不必担心变量提取被销毁的问题
使用 Thread 类提供 isInterrupted() 和 interrupt() 就可以就实现上述操作
java
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
// Thread.currentThread()表示 t 线程,类似于 this
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// throw new RuntimeException(e);
break;
}
}
});
t.start();
System.out.println(Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("t线程终止");
t.interrupt();
}
}
好的~~
本节的内容就讲到这里,下期将带来更多有关多线程的精彩内容~~
😍😍😍