深入理解Java线程
一、线程基础概念
线程,本质上是程序执行流的最小单元。它是进程中的一个执行路径,多个线程共享所属进程的资源,比如内存空间、文件描述符等。想象一个进程如同工厂,线程就是工厂里不同生产线,各生产线可同时运作,处理不同任务,又共享工厂的基础资源。
二、协程、线程、进程
(一)概念详解
进程:是程序在计算机中的一次执行过程,是系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间、系统资源(如文件描述符、信号处理等)。运行的一个Java程序就是一个进程,进程间相互隔离,数据不共享。
线程:是进程中的一个执行单元,是程序执行的最小单位。一个进程可以包含多个线程,它们共享进程的内存空间和系统资源,但有各自独立的程序计数器、栈和局部变量等。比如,在一个浏览器进程中,可能有多个线程,如负责页面渲染的线程、处理网络请求的线程、响应用户输入的线程等,这些线程协同工作,提高了浏览器的运行效率。
协程:是一种用户态的轻量级线程,也被称为微线程。它由程序员在代码中手动控制执行流程,不需要操作系统的调度。协程在执行过程中可以暂停执行,将执行权交给其他协程,然后在适当的时候再恢复执行。例如,在一些异步编程场景中,使用协程可以更方便地处理异步任务,避免回调地狱,使代码更易于理解和维护。
(二)区别剖析
资源占用:进程资源独立,占用资源多;线程共享进程资源,资源占用少;协程资源占用最少,仅保存必要上下文信息。
调度方式:进程由操作系统调度,切换开销大;线程也由操作系统调度,但因共享资源,切换开销小于进程;协程由用户程序调度,开销最小。
并发性:进程间并发通过操作系统调度实现;线程并发既靠操作系统调度,也可通过多线程编程实现;协程并发完全由用户程序控制,在单线程内实现微观并发。
(三)适用场景
进程:适用于任务独立性强、需隔离资源的场景,如运行多个不同的Java应用程序,相互不影响。
线程:在Java开发中广泛应用,如Web服务器处理多个HTTP请求、多线程计算任务等,能充分利用多核CPU性能。
协程:适合I/O密集型任务,如网络爬虫、数据库读写等,减少线程上下文切换开销,提升性能。
三、线程上下文切换
(一)切换定义
上下文切换是指当操作系统需要从一个正在运行的进程或线程切换到另一个进程或线程时,需要保存当前进程或线程的执行上下文,然后恢复要切换到的进程或线程的上下文,以便其能够继续执行的过程。这里的上下文包括程序计数器、寄存器的值、栈指针以及其他一些与进程或线程执行相关的状态信息。
(二)切换原因
时间片耗尽:分时操作系统给每个线程分配时间片,时间到就切换,保证公平性。
线程阻塞:线程执行I/O操作、等待锁、调用
sleep
等方法时进入阻塞状态,需切换线程执行。线程优先级变化:高优先级线程就绪,系统切换让其优先执行。
(三)切换开销
保存当前上下文:操作系统首先会将当前进程或线程的寄存器值、程序计数器等上下文信息保存到该进程或线程的控制块(如进程控制块 PCB 或线程控制块 TCB)中。这些信息记录了当前进程或线程在暂停时的执行状态,以便后续能够恢复执行。
选择新的进程或线程:根据调度算法,操作系统从就绪队列中选择一个新的进程或线程来执行。这个选择过程可能会考虑进程或线程的优先级、等待时间、资源需求等因素。
恢复新的上下文:将选中的进程或线程的上下文信息从其控制块中加载到 CPU 的寄存器中,设置程序计数器为该进程或线程上次暂停时的地址,栈指针也恢复到相应的位置。这样,CPU 就可以从上次中断的地方继续执行新的进程或线程。
上下文切换会带来一定的开销,包括保存和恢复上下文信息的时间、CPU 缓存失效的影响以及调度算法执行所花费的时间等。频繁的上下文切换可能会导致系统性能下降,因此在设计和优化多任务系统时,需要尽量减少不必要的上下文切换。
linux
ps -fe 查看所有进程
ps -fT -p <PID> 查看某个进程(PID)的所有线程
kill 杀死进程
top 按大写 H 切换是否显示线程
top -H -p <PID> 查看某个进程(PID)的所有线程
Java
jps 命令查看所有 Java 进程
jstack <PID> 查看某个 Java 进程(PID)的所有线程状态
jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
四、线程的生命周期
(一)新建(New)
创建Thread
对象后,线程进入新建状态,尚未启动,如Thread thread = new Thread(() -> System.out.println("Hello from thread"));
,此时线程资源未分配。
(二)就绪(Runnable)
调用start
方法后,线程进入就绪状态,准备运行,等待CPU调度。线程已获除CPU外所有运行资源,如thread.start();
后线程进入该状态。
(三)运行(Running)
CPU调度到就绪线程,线程进入运行状态,执行run
方法代码。运行中可能因时间片耗尽、阻塞等原因离开该状态。
(四)阻塞(Blocked)
线程等待资源(如锁)或执行I/O操作时进入阻塞状态,不参与CPU调度,如线程竞争锁失败,synchronized
块无法进入,进入阻塞等待。
(五)等待(Waiting)
线程执行Object.wait()
、Thread.join()
等方法进入等待状态,需其他线程唤醒,如生产者 - 消费者模型中,消费者线程等待生产者生产数据。
(六)超时等待(Timed Waiting)
线程执行Thread.sleep(long millis)
、Object.wait(long timeout)
等带超时参数方法进入该状态,超时后自动唤醒。
(七)终止(Terminated)
线程run
方法执行完毕或因异常终止,进入终止状态,资源释放,生命周期结束,无法重启。
五、线程的实现方式、原理
(一)继承Thread类
实现步骤:创建类继承
Thread
类,重写run
方法定义线程执行逻辑,创建该类实例并调用start
方法启动线程。原理:
start
方法触发系统创建新线程,执行run
方法,新线程与主线程并发执行。示例代码:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
(二)实现Runnable接口
实现步骤:创建类实现
Runnable
接口,实现run
方法,创建Thread
类实例,将实现Runnable
接口的对象作为参数传入Thread
构造函数,调用start
方法启动线程。原理:
Thread
类构造函数接收Runnable
对象,start
方法启动线程后执行Runnable
对象的run
方法。示例代码:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable is running");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
(三)实现Callable接口
实现步骤:创建类实现
Callable
接口,实现call
方法(可返回值、抛异常),创建FutureTask
对象并传入Callable
对象,将FutureTask
对象作为参数传入Thread
构造函数,调用start
方法启动线程,通过FutureTask.get()
获取call
方法返回值。原理:
FutureTask
包装Callable
对象,管理任务执行和结果获取,线程启动后执行Callable
的call
方法。示例代码:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 1 + 2;
}
}
public class Main {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
try {
Integer result = futureTask.get();
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
Java线程属于内核级线程
JDK1.2——基于操作系统原生线程模型来实现。Sun JDK,它的Windows版本和Linux版本都使用一对一的线程模型实现,一条Java线程就映射到一条轻量级进程之中。
内核级线程(Kernel Level Thread ,KLT):它们是依赖于内核的,即无论是用户进程中的线程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。
用户级线程(User Level Thread,ULT):操作系统内核不知道应用线程的存在。
六、线程的调度机制
(一)线程优先级
概念:Java线程有优先级,范围1(
Thread.MIN_PRIORITY
)到10(Thread.MAX_PRIORITY
),默认5(Thread.NORM_PRIORITY
)。设置与获取:通过
setPriority(int priority)
方法设置,getPriority()
方法获取。调度影响:线程调度器倾向调度高优先级线程,但不绝对保证先执行,受操作系统影响,不同系统对优先级支持不同。
(二)调度策略
分时调度:线程轮流获CPU时间片执行,保证公平性,每个线程都有机会运行。
抢占式调度:Java虚拟机采用此策略,高优先级线程可抢占CPU资源优先执行,提高系统整体性能。
七、线程的常用方法
(一)start()
启动线程,让线程进入就绪状态,等待CPU调度执行run
方法,注意一个线程只能调用一次start
方法,否则抛IllegalThreadStateException
异常。
(二)run()
定义线程执行逻辑,线程被调度执行时运行run
方法代码,直接调用run
方法是普通方法调用,不会启动新线程。
(三)sleep(long millis)
使当前线程暂停指定毫秒数,进入阻塞状态,不释放锁资源,时间到进入就绪状态,如try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
使线程暂停1秒。处于休眠中的线程被中断,线程是可以感受到中断信号的,并且会抛出一个InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。
(四)yield()
让当前线程放弃CPU资源,进入就绪状态,给同优先级或更高优先级线程执行机会,不保证其他线程一定获CPU资源。不会释放对象锁。
(五)join()
等待调用join
方法的线程执行完毕,如thread.join();
,当前线程阻塞,直到thread
线程执行完继续执行,有join(long millis)
重载方法,等待指定毫秒数。
(六)interrupt()
中断线程,设置线程中断标志位,线程可检查标志位(isInterrupted
方法)响应中断。线程处于阻塞状态(sleep
、join
、wait
等)时,调用interrupt
抛InterruptedException
异常,清除标志位。
(七)setDaemon(boolean on)
设置线程为守护线程,守护线程为用户线程服务,用户线程结束,守护线程自动结束,如垃圾回收线程是守护线程,通过thread.setDaemon(true);
设置,需在start
方法前调用。
八、线程的中断机制
(一)中断标志位
每个线程有中断标志位,初始false
,调用interrupt
方法设为true
,线程可通过isInterrupted
方法检查,Thread.interrupted
方法(静态)也可检查,且清除标志位。
(二)响应中断
线程可根据业务逻辑决定是否响应中断,如在循环中定期检查标志位,while (!Thread.currentThread().isInterrupted()) { // 执行任务 }
,标志位为true
时提前结束循环处理中断。
(三)中断阻塞线程
线程处于sleep
、join
、wait
等阻塞状态,调用interrupt
抛InterruptedException
异常,线程捕获异常后按业务逻辑处理,如提前结束线程或恢复操作。
九、线程间通信方式
(一)共享变量
通过共享内存变量通信,多线程读写同一变量,一个线程修改,其他线程可读取感知,但需注意线程安全,结合同步机制(synchronized
、Lock
等)保证数据一致性。
(二)wait()、notify()、notifyAll()
原理:
wait
方法使线程等待,释放持有的对象锁,进入对象等待队列;notify
方法唤醒等待队列中一个线程;notifyAll
方法唤醒所有等待线程。示例场景:生产者 - 消费者模型,生产者生产数据放入共享缓冲区,调用
notify
唤醒消费者;消费者从缓冲区取数据,无数据时调用wait
等待。
(三)管道流
用于线程间数据传输,PipedInputStream
和PipedOutputStream
配合,一个线程向PipedOutputStream
写数据,另一个线程从PipedInputStream
读数据,如:
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
class Sender implements Runnable {
private PipedOutputStream outputStream;
public Sender(PipedOutputStream outputStream) {
this.outputStream = outputStream;
}
@Override
public void run() {
try {
outputStream.write("Hello from sender".getBytes());
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class Receiver implements Runnable {
private PipedInputStream inputStream;
public Receiver(PipedInputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public void run() {
try {
byte[] buffer = new byte[1024];
int length = inputStream.read(buffer);
System.out.println("Received: " + new String(buffer, 0, length));
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) {
try {
PipedOutputStream outputStream = new PipedOutputStream();
PipedInputStream inputStream = new PipedInputStream(outputStream);
Sender sender = new Sender(outputStream);
Receiver receiver = new Receiver(inputStream);
new Thread(sender).start();
new Thread(receiver).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
(四)CountDownLatch、CyclicBarrier、Semaphore等工具
CountDownLatch:用于一个或多个线程等待其他线程完成操作,如多个线程初始化资源,主线程等所有线程初始化完再继续执行。
CyclicBarrier:让一组线程相互等待到公共屏障点再继续,可重复使用,如多个线程处理数据,处理完一起汇总结果。
Semaphore:控制同时访问特定资源的线程数量,如数据库连接池限制并发连接数。