回调函数是一种特殊的函数:它被当作参数传递给另一个函数,然后在这个 “接收参数的函数” 内部被调用执行。其核心特点是 “由其他函数决定何时执行”,而非主动调用。
我们之前在学习优先级队列大根堆小根堆的时候,曾自己定义过Comparable和Comparator接口,并把它们当作比较器传给构造函数。而他们所包含的方法compare和compareto就是所谓的回调函数。
而在上一节中我们说到,在Thread子类中重写run方法可以创建一个任务。其实,我们也可以在Thread创建时传递一个Runnable接口,这样也能达到相同的效果。
在上一章中,我们使用MyThread类,在里面重写run方法,创建了任务。 在这一章,我们重点展示一些新的创建线程、创建任务的方法。
package Thread;
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello Thread");
}
}
public class demo3 {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
}
}将实现了Runnable接口的MyRunnable类的实例化对象通过构造方法传入,让线程与任务关联。
Thread t = new Thread(){
@Override
public void run() {
System.out.println("hello Thread");
}
};
t.start();Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello Thread2");
}
});
t2.start();
以上两种方法都是采用了匿名内部类的方法,但是二者略有不同。 第一段代码是临时创建了一个Thread的子类,这个子类没有名字,只能使用一次,并且通过这个子类实例化了一个t对象,这个子类里面重写了run方法,所以t线程和任务连接了以来,启动线程后,就可以执行任务了。 第二段代码是临时创建了一个实现了Runnable的类,将这个类的唯一实例化对象作为参数传递给了Thread的构造函数,并且重写了run方法。
匿名内部类的语法:new Runnable() { … } 就是匿名内部类的核心语法。这里的逻辑是: 声明要实现Runnable接口; 在{}中直接重写Runnable的run()方法(因为Runnable是函数式接口,只需要重写这一个方法); 整个表达式的结果是一个 “实现了Runnable接口的匿名类的实例”,直接作为参数传给Thread的构造方法。
不过,这两种方法在实际应用中更推荐第二个方法。这里面涉及到耦合的问题。
低耦合:模块间 “少牵扯” 定义:不同模块之间的依赖关系简单、交互接口清晰,一个模块的修改对其他模块影响极小。 核心特点:模块间通过标准化接口通信,不深入彼此内部实现;模块独立存在,可单独替换或修改。 高内聚:模块内 “心往一处想” 定义:一个模块内部的功能、职责高度相关,所有代码都围绕同一个核心目标服务。 核心特点:模块不包含无关功能,职责单一明确;模块内代码关联性强,逻辑连贯且完整。
第一段代码耦合高: 将线程与任务绑在了一起,如果后期想用其他的线程池啦等方法运行任务而非多线程时需要重写所有的代码。 第二段代码耦合低: 如果想使用其他的方法运行任务,只需要将runnable的示例重新传一下就可以了。
在数据结构学习的最后,我们详细介绍过lambda表达式,通过这种方法,可以将代码进一步简化。
博文原文:
Lambda表达式的语法 基本语法: (parameters) -> expression 或 (parameters) ->{ statements; } Lambda表达式由三部分组成: paramaters:类似方法中的形参列表,这里的参数是函数式接口里的参数。这里的参数类型可以明确的声明也可不声明而由JVM隐含的推断。另外当只有一个推断类型时可以省略掉圆括号。 ->:可理解为“被用于”的意思 方法体:可以是表达式也可以代码块,是函数式接口里方法的实现。代码块可返回一个值或者什么都不反回,这里的代码块块等同于方法的方法体。如果是表达式,也可以返回一个值或者什么都不反回。 函数式接口定义:一个接口有且只有一个抽象方法 。
package Thread;
public class demo5 {
public static void main(String[] args) {
Thread thread = new Thread(()-> System.out.println("hello Thread"));
//thread.start();
Thread thread1 = new Thread(()->{
while(true){
try {
Thread.sleep(1000);
System.out.println("hello thread1");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Runnable runnable = ()->{
while(true){
try {
Thread.sleep(1000);
System.out.println("hello thread2");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
其实本质上和匿名内部类是一样的,只是lambda表达式的写法会使得代码更加简洁。

下面我们修改源代码,给线程命名:
Thread thread1 = new Thread(()->{
while(true){
try {
Thread.sleep(1000);
System.out.println("hello thread1");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"first_thread");
Runnable runnable = ()->{
while(true){
try {
Thread.sleep(1000);
System.out.println("hello thread2");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
Thread thread2 = new Thread(runnable,"second_thread");
thread1.start();
thread2.start();
使用图形化工具可以看到,线程被成功命名了。 如果不给线程命名,他们会有默认的命名,从Thread-0开始:

给线程起的名字并不会影响线程的运行,它的作用是为了程序员方便调试。
此时肯定有人要问了,先不说这个图形里面没有Thread-0,咱好歹得有个main线程吧,main哪去了?
package Thread11_11;
public class demo5 {
public static void main(String[] args) {
Thread thread = new Thread(()-> System.out.println("hello Thread"));
//thread.start();
Thread thread1 = new Thread(()->{
while(true){
try {
Thread.sleep(1000);
System.out.println("hello thread1");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"first_thread");
Runnable runnable = ()->{
while(true){
try {
Thread.sleep(1000);
System.out.println("hello thread2");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
Thread thread2 = new Thread(runnable,"second_thread");
thread1.start();
thread2.start();
}
}首先Thread-0确实被创建了,但是没有任何运行操作而已,就在第一行。其次main线程并不是不存在,而是在运行了两个线程的启动之后结束运行了。自然不会显现出来。

前两个方法很简单,ID是Java给每个线程分配的id,用来标识线程身份。name不必多说。
Thread.State返回的六种状态:

package Thread11_11;
public class demo5 {
public static void main(String[] args) {
Thread thread = new Thread(()-> System.out.println("hello Thread"));
//thread.start();
Thread thread1 = new Thread(()->{
while(true){
try {
Thread.sleep(1000);
System.out.println("hello thread1");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"first_thread");
Runnable runnable = ()->{
while(true){
try {
Thread.sleep(1000);
System.out.println("hello thread2");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
Thread thread2 = new Thread(runnable,"second_thread");
thread1.start();
thread2.start();
System.out.println(thread1.getId());
System.out.println(thread2.getId());
System.out.println(thread2.getName());
System.out.println(thread1.getName());
System.out.println("==================");
System.out.println(thread.getState());
System.out.println(Thread.currentThread().getState());
System.out.println(thread1.getState());
System.out.println(thread2.getState());
}
}
thread.getState() → NEW: thread只被创建(new Thread(…)),但未调用start(),处于 “新建未启动” 状态,符合Thread.State.NEW的定义。 Thread.currentThread().getState() → RUNNABLE: currentThread()获取的是当前执行代码的线程(即main线程)。此时main线程正在执行打印状态的语句,处于 “可运行” 状态(要么正在 CPU 上执行,要么等待 CPU 调度),因此状态为RUNNABLE。 thread1.getState()和thread2.getState() → TIMED_WAITING: thread1和thread2启动后,进入run()方法的无限循环,每次循环都会调用Thread.sleep(1000)。sleep(long)方法会让线程进入 “限时等待” 状态(TIMED_WAITING),直到 1 秒后自动唤醒。当main线程查询它们的状态时,这两个线程大概率正处于sleep的等待过程中,因此状态为TIMED_WAITING。
可以通过setPriority改变线程优先级。在默认情况下,线程优先级是一样的。
前台线程:这些线程的存在影响着进程的继续存在,如果这些线程结束了,进程也结束了。 后台线程:又叫守护线程。这些线程的存在不影响进程,进程结束,他们也随之结束了。

比如这张图,除了我们创建的两个线程之外,其他的全是后台线程。他们随着进程的创建而创建,也随着销毁而销毁。
自己创建的线程默认都是前台线程,可以通过setDaemon来改变。
package Thread11_12;
public class demo6 {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while(true){
try {
Thread.sleep(1000);
System.out.println("hello thread");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.setDaemon(true);
thread.start();
}
}
进程直接结束了,作为一个后台线程,无力阻止进程的结束。 注意此处定义后台线程时要在启动之前。
众所周知,java的Thread对象和系统的线程是一一对应的,但是它们的生命周期可能是不同的,可能存在线程没了,对象还在。 首先,线程入口里面对应的方法运行结束了,线程也就被销毁了。
package Thread11_12;
public class demo7 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 3; i++) {
try {
System.out.println("hello thread");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
for (int i = 0; i < 4; i++) {
Thread.sleep(1000);
System.out.println(t.isAlive());
}
}
}
那么照理来说,第三个hello thread结束之后,线程就结束了,但是此时结果仍然是true,thread对象仍然存活。
isInterrupted随后就讲,在此之前,先讲一下别的内容。
大家肯定很好奇,为什么start线程了之后,就会运行run方法里面的任务?–我也很好奇!


点开底层源码之后,我们注意到,具体的方法好像被try捕获里面的start0方法封装起来了,但是当我们找到start0代码时,发现他是一个本地方法。

所以我们没法看到具体的实现。
package Thread11_12;
public class demo8 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("hello thread");
});
t.start();
t.start();
}
}
每次想创建一个新的线程,都需要创建一个新的Thread对象,无法重复使用。
package Thread11_12;
public class demo9 {
private static boolean isFinished = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(!isFinished){
try {
Thread.sleep(1000);
System.out.println("hello thread");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(3000);
isFinished = true;
}
}通过这个代码就可以实现外界力量中断了线程的运行。
但是! 这里为啥要把isFinished变量定义到外面呢? 如果在里面:

这会涉及到lambda表达式的变量捕获问题:
在 Java 中,变量捕获(Variable Capture)指的是:当匿名内部类、Lambda 表达式或局部内部类访问其外部作用域(通常是方法内的局部变量或外部类的成员变量) 中的变量时,这些变量会被内部结构 “捕获” 并持有引用或值的现象。 简单说,就是内部代码块(如 Lambda、匿名类)“记住” 了外部变量的值或引用,以便在自身执行时使用。变量捕获的规则与变量的类型(局部变量、成员变量等)密切相关,核心是保证内部结构访问外部变量时的一致性和安全性。
局部变量的捕获 局部变量指的是方法内定义的变量(包括方法参数)。当内部结构(Lambda / 匿名类)访问局部变量时,该变量必须是 final 或 effectively final(即虽然未显式声明final,但实际从未被修改过)。 原因: 局部变量存储在方法的栈帧中,当方法执行结束后,栈帧销毁,局部变量随之消失; 而内部结构(如 Lambda 作为线程任务)可能在方法执行结束后才运行(例如多线程场景),此时内部结构需要 “记住” 局部变量的值。为了避免内部结构使用的变量值与外部原始值不一致(栈帧已销毁,无法追踪修改),Java 要求局部变量必须不可变(final或 effectively final),确保内部捕获的是 “固定值”。
成员变量的捕获(实例变量 / 静态变量) 成员变量包括实例变量(属于对象)和静态变量(属于类),它们的捕获规则与局部变量完全不同: 内部结构可以直接访问并修改成员变量,无需final修饰; 原因是成员变量存储在堆中**(实例变量在对象堆内存,静态变量在方法区),生命周期与对象或类一致,不会随方法栈帧销毁,内部结构可以通过外部类的引用持续访问最新值。**
使用interrupted中断线程
package Thread11_12;
public class demo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 这个线程掀桌了.
throw new RuntimeException(e);
}
}
System.out.println("t 结束");
});
t.start();
Thread.sleep(3000);
System.out.println("main 线程尝试终止 t 线程");
t.interrupt();
}
}
虽然通过异常,线程确实中断了。但是与我们的预期似乎不太符合。因为我们写的t结束了语句没有被执行,并且我们以为会正常结束而非异常。这一切都是因为线程中断机制:
Java 中的interrupt()方法不是直接终止线程,而是通过以下方式 “通知” 线程: 给线程设置一个中断标志位(isInterrupted()返回 true); 若线程正在执行阻塞操作(如sleep()、wait()、join()等),会立即唤醒线程,并抛出InterruptedException,同时清除中断标志位(此时isInterrupted()会返回 false)。
而本线程大部分时间都处在sleep阻塞时间段,所以很容易产生异常现象。 因此,我们可以利用这一点在异常捕获语句里用别的方式来处理:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 这个线程掀桌了.
// throw new RuntimeException(e);
// 1. 加上 break 就是立即终止
// 2. 啥都不写 就是不终止
// 3. catch 中先执行一些其他逻辑再 break, 就是稍后终止.
//break;
}break时可以正常结束:

其实这样也挺好的,相当于把线程的命运交给了我们程序员,我们可以设置让线程关闭一些东西再中断保证安全以及一些其他的操作。
有时候,我们在处理多线程任务时,一个线程的执行,需要等待另一个线程结束。如果使用sleep,那么在时间上很难把握,所以我们引入join方法,来应对这种情况。 方法:

如果在main线程中使用t.join,只要t线程不结束,main就会一直一直等下去。
package Thread11_12;
public class demo11 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(true){
try {
Thread.sleep(1000);
System.out.println("hello thread");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
t.join();
System.out.println("hello main");
}
}比如此代码,hello main这个输出会被一直卡着无法运行。
但是这样不是特别好,万一一直等下去没了头,最终耽误事就不好了。所以java也提供了带参数的写法来规定等待的最大时间。
package Thread11_12;
public class demo11 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(true){
try {
Thread.sleep(1000);
System.out.println("hello thread");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
t.join(3000);
System.out.println("hello main");
}
}
还有一种传参方法是传入两个参数,第一个参数依然是代表毫秒,第二个参数代表纳秒。这个方法只有在需要精密计算的业务中会使用,平时一般不用这么精确。
就像咱们一直说的,休眠就是使用sleep方法,这个方法也有纳秒级别的精度

注意: 当我们写了一个sleep(1000)的语句时,真的是让线程休眠一秒吗?? 其实,很可能是比一秒钟多一点。 代码调用sleep,其实是让线程让出cpu资源,后续时间到了之后,需要操作系统内核,将这个线程重新调到cpu上,才能继续执行。而时间到了,只是意味着线程可以重新被调度了,并不是立即就执行了。
特殊写法:sleep(0) 写了这个代码意味着让线程立即让出cpu资源,等待操作系统重新调度。把cpu让出来给更多线程机会。
使用场景:
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 这个线程掀桌了.
// throw new RuntimeException(e);
// 1. 加上 break 就是立即终止
// 2. 啥都不写 就是不终止
// 3. catch 中先执行一些其他逻辑再 break, 就是稍后终止.
break;
}
}
System.out.println("t 结束");
});
t.start();我们本来的需求是,判断当前线程是否被打断。但是此时t线程这个示例并没有被创建,我们没法写t.isInterrupted来判断。而Thread.currentThread()方法的作用类似于this关键字,可以得到当前线程的示例,尤其适合在无法直接拿到线程对象引用的场景下使用。
类似的,在mian线程里使用这个方法得到的就是main线程的示例。
本文围绕 Java 线程核心知识展开,先以回调函数为切入点,明确其 “作为参数传递并由其他函数触发执行” 的本质,进而延伸出多种线程创建方式 —— 包括 Thread 构造方法结合 Runnable 接口、匿名内部类(对比两种实现的耦合差异,推荐低耦合方案)、lambda 表达式(简化函数式接口的使用);随后详细讲解了 Thread 类的关键属性(ID、名称、状态、优先级、守护线程、存活状态等)与核心方法,涵盖线程启动(start,强调不可重复调用)、中断(interrupt 结合标志位与异常处理,灵活控制线程终止)、等待(join 解决线程执行顺序依赖)、休眠(sleep 的时间特性与特殊用法);最后补充了 currentThread 方法的实用场景,解决无法直接获取线程对象引用时的操作需求,形成了从线程创建、属性方法到实用工具的完整知识体系。