高并发编程基础-02-线程基础知识说明
引言
现在几乎所有操作系统都支持多任务执行,其中每个任务被视为一个进程。在每个进程内部,至少有一个线程在运行,线程也被称为轻量级进程。
线程可以看作是程序执行的一条路径,每个线程都有自己的局部变量表、程序计数器(指向当前正在执行的指令)以及各自的生命周期。现代操作系统通常支持同时运行多个线程。例如,在启动Java虚拟机(JVM)时,操作系统会创建一个新的进程(即JVM进程),并在该进程中生成多个派生或创建的线程。
线程生命周期
在JDK17版本的JVM线程的生命周期共7个状态,可以在java.lang.Thread.State
枚举类看到,具体如下:
- 新建状态(New):当一个Thread类或其子类的对象被创建时,该线程处于新建状态。此时它尚未启动,即没有开始执行run()方法。
- 就绪状态(Runnable):当线程对象调用了start()方法之后,该线程进入就绪状态。此时该线程已经有了执行资格,只等待CPU的调度,也就是分配时间片。
- 运行状态(Running):当就绪状态的线程获得了CPU时间片,开始执行run()方法时,该线程进入运行状态。
- 阻塞状态(Blocked):在某些情况下,线程可能会暂时失去对CPU的控制权,暂停执行。这时线程进入阻塞状态。例如,线程调用了sleep()方法、I/O操作、等待synchronized锁等都会使线程进入阻塞状态。
- 等待状态(Waiting):当线程执行wait()、join()、park()等方法之后,线程进入等待状态。此时线程不会占用CPU资源,也不会释放持有的锁,需要其他线程的唤醒才能继续执行。
- 超时等待状态(Timed Waiting):与等待状态类似,但是可以设置等待的时间。当线程调用了带有时间参数的sleep()、wait()、join()方法或者LockSupport.parkNanos()、LockSupport.parkUntil()方法时,线程进入超时等待状态。
- 终止状态(Terminated):线程执行完run()方法后,或者出现异常而结束时,线程进入终止状态。此时线程已经彻底结束,不会再回到其他状态。
这些线程状态在Java中非常重要,理解它们的含义和转换规则有助于我们编写高效、正确的多线程程序。
线程的生命周期是从新建状态开始,通过调用 start() 方法进入可运行状态,然后可能进入阻塞、等待或者被中断,最后进入终止状态。JVM 管理线程状态的转换,可以通过 Thread 类的状态相关方法来查询当前线程的状态。
Running状态的转换
- 直接进入 TERMINATED 状态,比如用JDK已经不推荐使用的stop法或者判断某个逻辑标识。
- 进入 BLOCKED 状态,比如调用了 sleep 或者 wait 方法而加入了 waitSet 中。
- 进行某个阻塞的 IO 操作,比如因网络数据的读写而进入了 BLOCKED 状态获取某个锁资源,从而加人到该锁的阻塞队列中而进入了 BLOCKED 状态。
- 由于CPU的调度器轮询使该线程放弃执行,进入RUNNABLE 状态。
- 线程主动调用 yield 方法,放弃 CPU 执行权,进入 RUNNABLE 状态。
Blocked状态的转换
- 直接进人 TERMINATED 状态,比如调用JDK已经不推荐使用的 stop 方法或者意外死亡(JVM Crash)。
- 线程阻塞的操作结束,比如读取了想要的数据字节进入到 RUNNABLE 状态。
- 线程完成了指定时间的休眠,进入到了 RUNNABLE 状态。
- Wait 中的线程被其他线程 notify/notifyall 唤醒,进入RUNNABLE状态。
- 线程获取到了某个锁资源,进人 RUNNABLE 状态。
- 线程在阻塞过程中被打断,比如其他线程调用了 interrupt 方法,进入RUNNABLE状态。
Terminated状态的形成
- 线程运行正常结束,结束生命周期。
- 线程运行出错意外结束。
- JVM Crash,导致所有的线程都结束。
线程的创建
创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元则有两种方式。
- 重写Thread的run方法。
- 实现 Runnable 接口的run方法,并且将Runnable 实例用作构造Thread 的参数。
public class MyThread extends Thread {
public void run() {
// 定义线程执行的任务
System.out.println("This is a new thread.");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 启动线程
}
}
public class MyRunnable implements Runnable {
public void run() {
// 定义线程执行的任务
System.out.println("This is a new thread.");
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable); // 将实现了 Runnable 接口的对象作为参数传递给 Thread 类的构造方法
thread.start(); // 启动线程
}
}
线程中的策略模式
无论是 Runnable 接口的 run() 方法,还是 Thread 类本身的 run() 方法,都遵循了将线程的控制逻辑与业务逻辑分离的原则,以实现职责分明、功能单一的设计思想。这种设计方式与 GoF(Gang of Four)设计模式中的策略模式有相似之处。
在策略模式中,将可变的算法封装成独立的策略类,并通过接口或抽象类与调用者进行解耦。调用者可以根据需要选择不同的策略来完成特定的任务。类似地,Java 中的线程创建方式也将线程的执行逻辑封装在一个单独的类(实现 Runnable 接口或继承 Thread 类)中,通过调用 start() 方法来启动线程。
使用这种设计模式,可以使线程控制逻辑与业务逻辑分离,提高代码的可维护性和可扩展性。例如,可以根据不同的业务需求,定义不同的 Runnable 实现类或 Thread 子类,并在启动线程时选择合适的线程对象,从而实现不同的业务逻辑。
总结来说,Java 中线程的创建方式与策略设计模式相似,都体现了将控制逻辑与具体业务逻辑分离的设计原则,以实现代码的灵活性和可扩展性。
线程中的Runnable复用
重写 Thread 类的 run() 方法和实现 Runnable 接口的 run() 方法有一个关键的不同点。Thread 类的 run() 方法是无法共享的,也就是说,一个线程的 run() 方法不能被另一个线程当作自己的执行单元。相比之下,使用 Runnable 接口可以实现线程执行单元的共享。通过传递同一个实现了 Runnable 接口的对象给多个 Thread 实例,可以使多个线程共享同一个执行单元,从而提高代码的复用性和可维护性。
public class MyRunnable implements Runnable {
public void run() {
// 定义线程执行的任务
System.out.println("This is a new thread.");
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 启动线程
Thread thread2 = new Thread(myRunnable);
thread2.start(); // 启动线程
}
}
例子
创建线程
package engineer.concurrent.battle.onebasic;
import java.util.concurrent.TimeUnit;
public class TryConcurrent {
public static void main(String[] args) {
new Thread(TryConcurrent::writeCode).start();
listenMusic();
}
private static void listenMusic() {
for(;;){
System.out.println("music is good");
sleep(1);
}
}
private static void writeCode() {
for(;;){
System.out.println("write code and work hard");
sleep(1);
}
}
private static void sleep(int i) {
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
多线程排队模拟
/**
* 叫号机排队模拟,通过多线程并发
*/
public class TicketWindow extends Thread {
private final String name;
private final static int MAX = 100;
private static AtomicInteger index = new AtomicInteger(1);
public TicketWindow(String name) {
this.name = name;
}
public void run() {
while (index.get() <= MAX) {
System.out.println(name + "柜台正在排队,排队号码为:" + index);
index.getAndIncrement();
}
}
public static void main(String[] args) {
new TicketWindow("一号窗口").start();
new TicketWindow("二号窗口").start();
new TicketWindow("三号窗口").start();
new TicketWindow("四号窗口").start();
}
}
参考
- 《Java高并发编程详解:多线程与架构设计》
- Java Thread Doc