目录
[一.继承 Thread,重写run方法:](#一.继承 Thread,重写run方法:)
[二.实现 Runnable, 重写 run:](#二.实现 Runnable, 重写 run:)
[三.Thread,重写 run, 使用匿名内部类:](#三.Thread,重写 run, 使用匿名内部类:)
[四.实现 Runnable, 重写 run, 使用匿名内部类:](#四.实现 Runnable, 重写 run, 使用匿名内部类:)
[五.使用 lambda 表达式:](#五.使用 lambda 表达式:)
[下面继续看 Thread 其他属性和方法。](#下面继续看 Thread 其他属性和方法。)
众所周知,茴香豆的茴字有四种写法,线程和茴香豆差不多,区别是这两根本没有关系,hhhhh~
那为啥要提茴香豆呢?因为茴字有四种写法,多线程也有四种(其实有五种,只是想用茴字写法引出主题而已)。
线程(Thread)的创建方法
按照之前学过的知识,线程应该是这样创建的
java
public class Demo1 {
public static void main(String[] args) {
Thread t=new Thread();
t.start();
}
}
可真的是这样吗?很显然不是,因为 run 的一瞬间程序就结束了。

什么原因造成的?需要我们进到 Thread 源代码找。

在这个方法中,target 是线程中的一个私有成员属性。如果创建的线程里传入了值,它会自动执行,反之,程序立即结束。

也就是用来告诉线程是否有活。
那如何正确打开线程呢?
一.继承 Thread,重写run方法:
java
class MyThread extends Thread{
@Override
public void run() {
System.out.println("Hello World!!");
}
}
public class Demo1 {
public static void main(String[] args) {
Thread t=new MyThread();
//执行完这步才是成功创建一个线程
t.start();
System.out.println("Hello World!!");
}
}

这里有的小伙伴可能疑惑,为什么Thread 不用导入包?
因为**java.lang****,它是核心语言包**!
这个包 包含了 Java 最基础、最核心的类。
例如:
-
Object(所有类的父类) -
String -
System -
Exception和Error(异常体系) -
基本数据类型的包装类,如
Integer,Double,Boolean..... -
还包括
Thread和Runnable。
根据 Java 语言规范,所有 Java 编译器都会自动、隐式地为每个源文件导入整个 java.lang 包, 这就好像编译器在每个 .java 文件的开头都自动为你加上了这样一行
java
import java.lang.*;
因此你可以直接使用 java.lang 包中的所有类,而无需何 import 语句。
二.实现 Runnable, 重写 run:
java
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("Hello World!!");
}
}
public class Demo2 {
public static void main(String[] args) {
Runnable r =new Thread();
r.start();
System.out.println("Hello World!!");
}
}
是不是有小伙伴和我想的一样,直接用 Runnable 开始线程,很可惜,这个想法是错误的。

Runnable 只是一个任务,一段在内CPU执行的逻辑代码,真正创建线程还是要通过 Thread。
java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello World!!");
}
}
public class Demo2 {
public static void main(String[] args) {
Runnable runnable =new MyRunnable();
Thread t=new Thread(runnable);
t.start();
System.out.println("Hello World!!");
}
}

不过线程里要干啥,可以通过 Runnable 表示,就不用直接重写 Thread 的 run 方法去表示。
这也是好的一点,为什么这么说?
我们希望敲的代码是低耦合的,就是要执行的 任务本身 和 线程 这个概念的关联度尽可能低,后续如果变更代码(通过其他方式),修改会简单许多。不然一改红一片,不仅要拼加时,还容易白费一年的辛苦.......
线程要执行定义的任务,选择放到 Thread 里,或者放到 (Runnable) 中都行。
三.Thread,重写 run, 使用匿名内部类:
java
public class Demo3 {
public static void main(String[] args) {
Thread t=new Thread(){
@Override
public void run() {
System.out.println("Hello World!!");
}
};
t.start();
System.out.println("Hello World!!");
}
}


这个语句做了三件事:
1.创建一个 Thread 的子类,子类叫什么?不知道,匿名的,new Thread 用来告诉编译器要创建一个Thread对象。
2.{ } 里面可以编写子类的定义代码,子类要有哪些属性 哪些方法 重写父类的哪些方法.....
3.创建这个匿名内部类的实例,把实例的引用赋值给 t 。
这种写法可以少定义一些类,如果某个代码是"一次性",就可以使用匿名内部类的写法。
详细解释一下什么是"一次性":
1.代码逻辑的单一使用场景,你定义的这个类(或这段代码逻辑)在整个程序中,只会在这一个特定的地方被使用一次,以后复用的可能性极低。
上述代码来说, 重写了 run() 方法、不断打印 "hello thread" 的线程,它的逻辑非常明确且唯一。几乎不可能在程序的其他 地方需要一个行为一模一样 的线程。为它单独创建个 .java 文件,显然不合适。
反面例子 :如果你有一个类,负责验证用户信息的各种规则,这个类很可能在"用户注册"、"用户登录"、"修改信息"等多个地方被调用。这就不是一次性的,应该定义成一个独立的、可复用的类。
2.实现的简洁性与临时性,这个类的实现非常简短 ,通常只是为了实现一个简单的接口(如 Runnable, Comparator),或者重写一个方法。如果逻辑变得复杂,再使用匿名内部类就会导致代码可读性急剧下降,这时就应该把它提取成一个命名的独立类。
所以,使用匿名内部类时先考虑几个问题:
1.这段逻辑我以后还会在其他地方原样再用吗? 否
2.这段逻辑是不是很简单(比如主要就是重写一个方法)? 是
3.这段逻辑是不是和当前方法的其他变量/状态紧密相关? 是
如果你也是这么想的,那可以使用匿名内部类,这避免了代码库被大量仅使用一次的"碎片类"污染,让代码更集中、更易读。
四.实现 Runnable, 重写 run, 使用匿名内部类:
java
public class Demo4 {
public static void main(String[] args) {
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println("Hello World!!");
}
};
Thread t=new Thread(runnable);
t.start();
System.out.println("Hello World!!");
}
}

和前面第二个一样,目的是为了降低耦合,分离任务 和 线程 的概念。
五.使用 lambda 表达式:
这东西本质是一个"匿名函数"。
什么意思?
它的侧重点在于用来完成什么行为逻辑,而不是在声明一个"新的类型",但是java中严格要求方法必须依托于 类 存在,为了解决这个问题,大佬们想出了一个叫"函数式接口"的东西。下面的代码便用了这个接口
java
public class Demo5 {
public static void main(String[] args) {
Thread t =new Thread(()->{
while(true){
System.out.println("Hello World!!");
}
});
t.start();
}
}
针对三 四改进,引入lambda表达式。
( ) -> { } 这个语法和 run( ) { } 的作用完全一样,因为写法更简便所以更常使用。


下面继续看 Thread 其他属性和方法。

java
public class Demo3 {
public static void main(String[] args) {
Thread t=new Thread("sayHello"){
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Hello World!!");
}
}
};
Runnable runnable=new Runnable() {
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Hello World!!");
}
}
};
Thread t2=new Thread(runnable,"sayHello1");
Thread t3 =new Thread(()->{
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Hello World!!");
}
},"sayHello2");
t.start();
t2.start();
t3.start();
}
}
如何查看线程是否真的是这些命名?需要借助jconsole,不知道在哪的按照下方提示找。

先让程序跑起来,再看。

看到结果有什么发现?居然不是按顺序执行的,这说明什么?原因放到后面解释。
现在有一个新问题,main线程去哪了?
写个代码找找。
java
t.start();
t2.start();
t3.start();
System.out.println("main线程开始");
System.out.println("main线程结束");
把代码放到开始三个线程后面,运行看看。

从结果看出,其他线程执行前 main 线程已经结束了。
上面我们给了1s的睡眠时间,改成0会怎样?
答:CPU会狂转,接着能听到风扇呼呼声。结果是看不到 main开始和结束输出的,瞬间就刷没了。
那为什么主线程结束了程序还在运行?
这就是多线程和单线程的区别,以前认为main方法执行完,程序进程就结束了,但是只针对单线程程序。
要让多线程结束,需要 Thread 类里的一个方法。

必须在开始前设定,否则会报错
java
t.setDaemon(true);
t2.setDaemon(true);
t3.setDaemon(true);
t.start();
t2.start();
t3.start();

那是不是还有前台线程?
哎,还真有。这两区别在于,一个能让进程结束,一个只能结束线程本身(不影响进程的其他线程运行)。
举个栗子:
张三李四王五几个人去参加一个活动,活动从上午11点持续到下午6点。三人玩了几个小时感觉非常累,于是和主持人打声招呼就回去了。
其中这三人的行为影响到活动进行了吗?显然没有,那就称三人为 "后台进程" ,只结束线程本身,无法决定进程是否结束。主持人便是 "前台线程" ,在台上宣布一声,活动结束,进程结束。
那如何分辨线程是前台还是后台?java 会在程序运行时给各个线程分配一个表示身份的 id ,类似于CPU给进程的 PID。通过方法获取对应线程的情况。

java
//获取线程id
System.out.println(t.getId());
System.out.println(t2.getId());
System.out.println(t3.getId());
//获取是否后台线程
System.out.println(t.isDaemon());
System.out.println(t2.isDaemon());
System.out.println(t3.isDaemon());

三个线程都是前台,因此main线程结束也无法阻断程序继续执行。
用alive方法获取线程是否存活。
java
System.out.println(t.isAlive());
System.out.println(t2.isAlive());
System.out.println(t3.isAlive());

每个Thread对象都只能start一次,多了就会报错,好比一个针头只能一个人用一次。
每次想创建一个新线程都得创建一个新的 Thread 对象(不能重复利用)

直接报一个线程非法状态异常。
至于为什么报错先出来了,是因为CPU随机调度的原因,这也解释上面用 jconsole 观察时并没有"按顺序"输出的原因。

(编译N次终于先调用成功的......)
start与run区别
答:这两都没啥关系,也就谈不上"区别"。start 调用系统接口,run 是线程入口方法。
学完运行接着要中断了。
在类里面定义一个布尔值的成员变量,把whlie循环判断条件改为布尔变量即可。
利用"内部类访问外部类的成员"语法完成中断命令。
java
private static boolean isFinished = false;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while (!isFinished){
System.out.println("Hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(1000);
isFinished=true;
}

那么问题来了,获取布尔值的变量能否定义成局部变量?


简单来说,lambda 希望使用外面的变量触发 "变量捕获" 的语法。
上面说过,lambda 是 回调函数,在操作系统真正创建出线程之后才会执行,而 main函数可能在线程创建前就结束了,导致 lambda 在获取变量之前,main线程结束,isFinished 被销毁 ,变量无法被捕获。

所以,java解决方法是把被捕获的变量拷贝一份给lambda,外边的变量是否被销毁,也就影响不到lambda 的执行了。
线程终止
java 的 Thread 对象中提供了现成变量,直接进行判定。

直接用线程调用会怎样?

报错.....
原因看图示

所以,需要使用另一个更安全的方法终止线程。

静态方法,相当于 this 关键字,哪个线程调用,获取对应线程的 Thread 引用。
java
Thread.currentThread().isInterrupted()
判断线程是否是被终止了。
java
t.interrupt();
则是主动进行终止操作。
java
public class Demo7 {
public static void main(String[] args) throws InterruptedException {
Thread t =new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
System.out.println("Hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
};
});
t.start();
Thread.sleep(1000);
t.interrupt();
}
}

---------------------------------------------------------------完---------------------------------------------------------------