一、线程简介
线程是操作系统能够进行运算调度的最小单位,同时也被称为轻量级进程(lightweight processes)。线程存在于进程中,是进程的实际运作单位。一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程的作用主要包括:
- 提高程序的执行效率:多线程允许程序中不同的执行流同时运行,这意味着程序可以更快地完成更多的工作。
- 充分利用多核处理器:在多核处理器上,多线程程序可以分配到多个处理器核心上并行执行,显著提升程序运行速度。
- 改善用户体验:例如,在图形用户界面(GUI)程序中,通过将耗时的任务放在一个单独的线程中执行,可以避免界面冻结,从而提升用户体验。
- 更好的资源共享:在同一进程中的多个线程可以共享进程所拥有的资源,如内存空间和文件句柄等。
具体的线程操作包括创建、启动、暂停、恢复、停止和终止线程。在如Java等高级语言中,可以通过实现Runnable接口或继承Thread类来创建线程,通过调用start()方法启动线程,通过run()方法来定义线程的执行体。
需要注意的是,多线程的使用应该谨慎,因为错误的线程操作可能会导致程序的性能反而下降,比如过多的线程切换开销,或者由于数据不一致导致的程序错误。因此在设计和实现多线程程序时,需要仔细考虑程序逻辑和线程安全。
二、线程启动
线程有两种启动方式:实现Runnable接口;继承Thread类并重写run()方法。
1、方法一:实现Runnable接口
public class Main1 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程启动方式1:实现Runnable接口");
}
}).start();
}
}
这种方式创建Runnable实例并重写Rnnable接口的run方法,并以构造函数的方式传给Thread实例,调用Thread实例的start方法启动线程。
通常情况下,实现Runnable接口然后启动线程是最好的选择,这可以提高程序的灵活性和扩展性,并且用Runnable接口描述任务也更容易理解。
2、方式二:继承Thread类
public class Main2 {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
System.out.println("线程启动方式2:继承Thread类");
}
}.start();
}
}
这种方式一般不推荐使用。
- Java是单继承的语言:Java 是单继承的语言,这意味着如果您选择继承
Thread
类来创建线程,就无法再继承其他类。这可能会导致设计上的限制,特别是在需要扩展其他类的情况下。 - 耦合性高:通过继承
Thread
类创建线程会将线程的执行逻辑和线程本身的定义耦合在一起,这使得代码难以维护和扩展。将任务和线程分离是更清晰和灵活的设计方式。 - 推荐使用接口:Java 推荐使用实现
Runnable
接口的方式来创建线程。这种方式更符合面向对象设计原则中的“组合优于继承”原则,将任务和线程分离,提高了代码的灵活性和可维护性。 - 线程池的管理:当使用线程池来管理线程时,更适合使用实现
Runnable
接口的方式。线程池可以更好地管理和重用线程,而继承Thread
类则无法很好地与线程池结合使用。
因此,虽然可以通过继承 Thread
类来创建线程,但为了保持代码的清晰性、可维护性和可扩展性,推荐使用实现 Runnable
接口的方式来创建线程
3、Thread类和Runnable接口
Thread类默认实现了Runnable接口,并且其构造方法的重载形式允许传入Runnable接口对象作为任务。
线程的两种启动方式,其本质都是实现Thread类中的run()方法。而实现Runnable接口,然后传递给Thread类的方式,比Thread子类重写run()方法更加灵活。
三、线程的六种状态
Thread类有个内部类:State,它描述了在java中线程的六种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
调用getState方法可以获取线程的当前状态。
1、新建状态:NEW
NEW代表着线程新建状态,一个已创建但是未启动(start)的线程处于NEW状态。
public class ThreadStartState {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
//空方法
});
System.out.println(thread.getState());
}
}
输出:
NEW
2、运行状态:RUNNABLE
RUNNABLE状态表示一个线程正在Java虚拟机中运行,但是这个线程是否获得了处理器分配资源并不确定,因此有可能它并没有在执行任务,这时候也可以称它为 READY 状态,该状态并没有纳入java的状态枚举中,但是它可以准确的描述出当前线程“虽然是Runnable状态,但是并没有在运行”这一状态。调用Thread的start()方法后,线程从NEW状态切换到了RUNNABLE状态。
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ThreadRunnableState {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
log.info("线程{}正在执行任务", Thread.currentThread().getName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
log.error("", e);
}
log.info("线程{}结束执行任务", Thread.currentThread().getName());
});
log.info("线程未执行任务状态:{}", thread.getState());
thread.start();
log.info("线程执行任务中状态:{}", thread.getState());
}
}
运行结果:
2024-08-26 23:00:41.502 [INFO ] [main] - 线程未执行任务状态:NEW
2024-08-26 23:00:41.503 [INFO ] [main] - 线程执行任务中状态:RUNNABLE
2024-08-26 23:00:41.503 [INFO ] [Thread-1] - 线程Thread-1正在执行任务
2024-08-26 23:00:46.510 [INFO ] [Thread-1] - 线程Thread-1结束执行任务
3、阻塞状态:BLOCKED
BLOCKED为阻塞状态,表示当前线程正在阻塞等待获得监视器锁。当一个线程要访问被其他线程synchronized锁定的资源时,当前线程需要阻塞等待。
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ThreadBlockedState {
private static final Object obj = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (obj) {
log.info("t1开始执行任务");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
log.error("", e);
}
log.info("t1线程结束执行任务");
}
});
Thread t2 = new Thread(() -> {
synchronized (obj) {
log.info("t2开始执行任务");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
log.error("", e);
}
log.info("t2线程结束执行任务");
}
});
log.info("【1】t1线程状态:{}", t1.getState());
log.info("【1】t2线程状态:{}", t2.getState());
log.info("启动t1线程");
t1.start();
log.info("【2】t1线程状态:{}", t1.getState());
log.info("【2】t2线程状态:{}", t2.getState());
log.info("启动t2线程");
t2.start();
log.info("【3】t1线程状态:{}", t1.getState());
log.info("【3】t2线程状态:{}", t2.getState());
log.info("主线程等待1秒钟");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("", e);
}
log.info("【4】t1线程状态:{}", t1.getState());
log.info("【4】t2线程状态:{}", t2.getState());
log.info("主线程等待6秒钟");
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
log.error("", e);
}
log.info("【5】t1线程状态:{}", t1.getState());
log.info("【5】t2线程状态:{}", t2.getState());
}
}
执行结果如下
2024-08-26 23:37:36.415 [INFO ] [main ] - 【1】t1线程状态:NEW
2024-08-26 23:37:36.417 [INFO ] [main ] - 【1】t2线程状态:NEW
2024-08-26 23:37:36.417 [INFO ] [main ] - 启动t1线程
2024-08-26 23:37:36.417 [INFO ] [main ] - 【2】t1线程状态:RUNNABLE
2024-08-26 23:37:36.417 [INFO ] [main ] - 【2】t2线程状态:NEW
2024-08-26 23:37:36.417 [INFO ] [main ] - 启动t2线程
2024-08-26 23:37:36.417 [INFO ] [main ] - 【3】t1线程状态:RUNNABLE
2024-08-26 23:37:36.417 [INFO ] [main ] - 【3】t2线程状态:RUNNABLE
2024-08-26 23:37:36.417 [INFO ] [main ] - 主线程等待1秒钟
2024-08-26 23:37:36.417 [INFO ] [Thread-1 ] - t1开始执行任务
2024-08-26 23:37:37.426 [INFO ] [main ] - 【4】t1线程状态:TIMED_WAITING
2024-08-26 23:37:37.426 [INFO ] [main ] - 【4】t2线程状态:BLOCKED
2024-08-26 23:37:37.426 [INFO ] [main ] - 主线程等待6秒钟
2024-08-26 23:37:41.426 [INFO ] [Thread-1 ] - t1线程结束执行任务
2024-08-26 23:37:41.426 [INFO ] [Thread-2 ] - t2开始执行任务
2024-08-26 23:37:43.431 [INFO ] [main ] - 【5】t1线程状态:TERMINATED
2024-08-26 23:37:43.431 [INFO ] [main ] - 【5】t2线程状态:TIMED_WAITING
2024-08-26 23:37:44.435 [INFO ] [Thread-2 ] - t2线程结束执行任务
t1线程的线程状态:NEW->RUNNABLE->TIME_WAITING->TERMINATED
t2线程的线程状态:NEW->RUNNABLE->BLOCKED->TIME_WAITING->TERMINATED
t2线程在变成RUNNABLE状态之后尝试去执行任务,所以要先拿到监视器锁,此时监视器锁还在t1线程手里还未释放,所以t2变成了BLOCKED状态等待t1释放锁。
4、等待状态:WAITING
WAITING状态表示线程处于等待状态。根据源码中的注释说明,调用以下方法之一,会使当前线程变成WAITING状态
- Object类的wait方法(不带超时设置)
- Thread类的join方法(不带超时设置)
- LockSupport类的park方法
处于等待状态的线程,正在等待另外一个线程去完成某个特殊操作。例如,在某个线程中调用了Object对象的wait()方法,它会进入等待状态,等待Object对象调用notify()或notifyAll()方法。一个线程对象调用了join()方法,则会等待指定的线程终止任务。
4.1 Object类的wait()/nofify()
下面用Object对象的wait方法为例,测试线程的WAITING状态。需要注意的是,调用Object对象的wait方法,必须先用synchronized关键字锁定Object对象。
线程进入WAITING状态之后,后续代码不再执行,直到Object对象调用了notify方法,线程才继续运行。
import lombok.extern.slf4j.Slf4j;
/**
* @author kdyzm
* @date 2024/8/27
*/
@Slf4j
public class ThreadWaitingState {
private static final Object obj = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (obj) {
log.info("t1开始执行任务");
try {
obj.wait();
} catch (InterruptedException e) {
log.error("", e);
}
log.info("t1继续执行任务");
}
});
Thread t2 = new Thread(() -> {
synchronized (obj) {
log.info("t2发送notify通知");
obj.notify();
}
});
log.info("启动t1线程");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("", e);
}
log.info("t1线程状态:{}", t1.getState());
log.info("t2线程状态:{}", t2.getState());
log.info("启动t2线程");
t2.start();
}
}
运行结果:
2024-08-27 14:35:04.708 [INFO ] [main ] - 启动t1线程
2024-08-27 14:35:04.709 [INFO ] [Thread-1 ] - t1开始执行任务
2024-08-27 14:35:05.717 [INFO ] [main ] - t1线程状态:WAITING
2024-08-27 14:35:05.718 [INFO ] [main ] - t2线程状态:NEW
2024-08-27 14:35:05.718 [INFO ] [main ] - 启动t2线程
2024-08-27 14:35:05.719 [INFO ] [Thread-2 ] - t2发送notify通知
2024-08-27 14:35:05.719 [INFO ] [Thread-1 ] - t1继续执行任务
需要注意两点:
- 调用wait和notify方法必须先获取对象锁,也就是说需要用synchronized关键字锁定Object对象
- t1线程调用wait方法之后就会自动释放锁,这时候t2就可以进入同步代码块了。
4.2 LockSupport类的park()/unpark()
可以通过park()/unpark()实现同样的功能,代码示例如下
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
/**
* @author kdyzm
* @date 2024/8/27
*/
@Slf4j
public class ThreadWaitingState {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.info("t1开始执行任务");
LockSupport.park();
log.info("t1继续执行任务");
});
Thread t2 = new Thread(() -> {
log.info("t2发送notify通知");
LockSupport.unpark(t1);
});
log.info("启动t1线程");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("", e);
}
log.info("t1线程状态:{}", t1.getState());
log.info("t2线程状态:{}", t2.getState());
log.info("启动t2线程");
t2.start();
}
}
运行结果如下:
2024-08-27 14:45:27.862 [INFO ] [main ] - 启动t1线程
2024-08-27 14:45:27.863 [INFO ] [Thread-1 ] - t1开始执行任务
2024-08-27 14:45:28.868 [INFO ] [main ] - t1线程状态:WAITING
2024-08-27 14:45:28.872 [INFO ] [main ] - t2线程状态:NEW
2024-08-27 14:45:28.872 [INFO ] [main ] - 启动t2线程
2024-08-27 14:45:28.872 [INFO ] [Thread-2 ] - t2发送notify通知
2024-08-27 14:45:28.873 [INFO ] [Thread-1 ] - t1继续执行任务
可以看到和上面的wait()/notify()方法的运行结果是一样的。但是和wait()/notify()不一样的是
-
park()/unpark()方法调用不需要监视器锁,直接调用即可。
-
park()/unpark()是基于线程的,而wait()/notify()是基于对象锁(监视器锁)的
-
传统的等待唤醒机制场景一般选择wait()/notify()组合,有些高度定制的场景则可以选择park()/unpark组合。
4.3 Thread类的join()
join方法的使用场景:一个线程等待另外一个线程结束,比如thread.join(),表示当前线程要等待thread线程结束后才能继续自己的任务。
代码示例如下
import lombok.extern.slf4j.Slf4j;
/**
* @author kdyzm
* @date 2024/8/27
*/
@Slf4j
public class ThreadWaitingStateJoinExample {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.info("t1开始执行任务...");
try {
Thread.sleep(2000); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("t1结束执行任务.");
});
Thread t2 = new Thread(() -> {
log.info("t2等待t1执行完任务...");
try {
t1.join();
} catch (InterruptedException e) {
log.error("", e);
}
log.info("t2结束执行任务.");
});
t1.start();
t2.start();
log.info("t1线程状态:{}",t1.getState());
log.info("t2线程状态:{}",t2.getState());
}
}
运行结果:
2024-08-27 15:17:28.004 [INFO ] [Thread-1 ] - t1开始执行任务...
2024-08-27 15:17:28.004 [INFO ] [Thread-2 ] - t2等待t1执行完任务...
2024-08-27 15:17:28.004 [INFO ] [main ] - t1线程状态:RUNNABLE
2024-08-27 15:17:28.005 [INFO ] [main ] - t2线程状态:WAITING
2024-08-27 15:17:30.008 [INFO ] [Thread-1 ] - t1结束执行任务.
2024-08-27 15:17:30.009 [INFO ] [Thread-2 ] - t2结束执行任务.
5、定时等待状态:TIMED_WAITING
这种状态之前的案例中已经出现过了,在线程中调用Thread.sleep方法,线程的状态就会变成TIMED_WAITING。
根据源码中的注释说明,共有以下几种情况会让线程的状态变成TIMED_WAITING:
- Object类的wait()方法(有超时设置)
- Thread类的join()方法(有超时设置)
- Thread类的sleep()方法
- LockSupport类的parkNanos ()方法
- LockSupport类的parkUntil()方法
Object类的wait()方法和Thread类的join、sleep方法之前的例子都有提过,不再演示,这里说下LockSupport类的parkNanos方法和parkUntil方法。
parkNanos和parkUntil方法都是用于暂停线程一段时间用的
- parkNanos方法用于暂停线程一段时间,参数是纳秒,表示过了多长时间之后再继续执行任务
- parkUntil方法是"暂停到什么时间",参数是个时间戳,表示到了那个时间再继续执行任务
这里有必要科普下“纳秒”的概念:
1秒 = 1000毫秒
1毫秒 = 1000微秒
1微妙 = 100,0000纳秒
1纳秒 = 1000皮秒
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
/**
* @author kdyzm
* @date 2024/8/27
*/
@Slf4j
public class ThreadWaitingStateJoinExample {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.info("t1开始执行任务...");
LockSupport.parkNanos(1000000 * 2000);
log.info("t1结束执行任务.");
});
Thread t2 = new Thread(() -> {
log.info("t2开始执行任务...");
LockSupport.parkUntil(System.currentTimeMillis() + 2000);
log.info("t2结束执行任务.");
});
t1.start();
t2.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("", e);
}
log.info("t1线程状态:{}", t1.getState());
log.info("t2线程状态:{}", t2.getState());
}
}
运行结果:
2024-08-27 15:44:53.468 [INFO ] [Thread-1 ] - t1开始执行任务...
2024-08-27 15:44:53.468 [INFO ] [Thread-2 ] - t2开始执行任务...
2024-08-27 15:44:54.471 [INFO ] [main ] - t1线程状态:TIMED_WAITING
2024-08-27 15:44:54.474 [INFO ] [main ] - t2线程状态:TIMED_WAITING
2024-08-27 15:44:55.482 [INFO ] [Thread-1 ] - t1结束执行任务.
2024-08-27 15:44:55.482 [INFO ] [Thread-2 ] - t2结束执行任务.
注意写法:LockSupport.parkNanos(1000000 * 2000);
以及LockSupport.parkUntil(System.currentTimeMillis() + 2000);
都是暂停两秒的意思。
6、完成状态:TERMINATED
TERMINATED表示线程为完结状态。当线程完成其run()方法中的任务,或者因为某些未知的异常而强制中断时,线程状态变为TERMINATED。这个前面的例子中也有所体现。
import lombok.extern.slf4j.Slf4j;
/**
* @author kdyzm
* @date 2024/8/27
*/
@Slf4j
public class ThreadTerminatedState {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
log.info("线程执行任务");
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
log.error("", e);
}
log.info("线程的当前状态:{}", thread.getState());
}
}
运行结果:
2024-08-27 15:51:27.638 [INFO ] [Thread-1 ] - 线程执行任务
2024-08-27 15:51:27.639 [INFO ] [main ] - 线程的当前状态:TERMINATED
四、线程状态转化
根据上面三的详细线程状态分析,整理出线程状态转化图
这里需要注意的是RUNNABLE状态在这里被拆分成了RUNNING状态和READY状态,但这在JAVA中并没有体现,实际上将RUNNABLE
状态拆分为RUNNING
和READY
状态可以更细致地描述线程的行为,更好地反映线程在系统中的执行情况。
在RUNNABLE
状态下的线程可以被操作系统调度为RUNNING
状态(正在执行)或者READY
状态(准备好执行但还未获得CPU执行时间)。
RUNNING
状态表示线程正在执行指令,占用CPU资源。READY
状态表示线程已经准备好执行,但尚未获得CPU执行时间。线程在READY
状态下等待操作系统的调度器将其调度为RUNNING
状态,以便真正执行代码。
RUNNING状态和READY状态转换:
当线程处于READY
状态时,如果被调度器选中,它会转换为RUNNING
状态开始执行。
当线程处于RUNNING
状态时,它可能会由于时间片用尽、等待I/O操作等原因,而转换为READY
状态,等待下一次调度;也可能会因为某些原因(如被其他高优先级线程抢占)而转换为READY
状态。
注意:本文归作者所有,未经作者允许,不得转载