1. 线程基础知识
1.1 线程和进程
- 进程:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的 基本单位。
- 线程:线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个 线程共享进程的资源
1.2 上下文切换(Context switch)
上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。
- 上下文切换只能在内核模式下发生。
内核模式是CPU的特权模式,其中只有内核运行,并提供对 所有内存位置和所有其他系统资源的访问。其他程序(包括应用程序)最初在用户模式下运行,但 它们可以通过系统调用运行部分内核代码。
- 上下文切换是多任务操作系统的一个基本特性。
内核模式(Kernel Mode)vs 用户模式(User Mode)
Kernel Mode(内核模式)
在内核模式下,执行代码可以完全且不受限制地访问底层硬件。它可以执行任何CPU指令和引用任何内存地址。内核模式通常为操作系统的最低级别、最受信任的功能保留。内核模式下的崩溃是灾难性的;他们会让整个电脑瘫痪。
User Mode(用户模式)
在用户模式下,执行代码不能直接访问硬件或引用内存。在用户模式下运行的代码必须委托给系统api来访问硬件或内存。由于这种隔离提供的保护,用户模式下的崩溃总是可恢复的。在您的计算机上运行的大多数代码将在用户模式下执行。
应用程序一般会在以下几种情况下切换到内核模式:
1. 系统调用。
2. 异常事件。当发生某些预先不可知的异常时,就会切换到内核态,以执行相关的异常事件。
3. 设备中断。在使用外围设备时,如外围设备完成了用户请求,就会向CPU发送一个中断信号,此时,CPU就会暂停执行原本的下一条指令,转去处理中断事件。此时,如果原来在用户态,则自然就会切换到内核态。
1.3 操作系统层面线程生命周期
这五态分别是:**初始状态、可运行状态、运行状态、休眠状态和终止状态 **
2. Java线程详解
2.1 Java线程的实现方式
方式1:使用 Thread类或继承Thread类
java
//创建线程
Thread thread = new Thread() {
@Override
public void run() {
//执行的任务
}
};
//运行线程
thread.start();
方式2:实现 Runnable 接口配合Thread
java
Runnable runnable = new Runnable() {
@Override
public void run() {
//执行的任务
}
};
Thread thread1 = new Thread(runnable);
thread1.start();
方式3:使用有返回值的 Callable
java
class CallableTash implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return new Random().nextInt();
}
}
//创建线程池
ExecutorService execut = Executors.newFixedThreadPool(10);
//提交任务,并用 Future提交返回结果
Future<Integer> result = execut.submit(new CallableTash());
方式4:使用 lambda
java
Thread thread2 = new Thread(() -> {/**执行的任务*/}, "thread");
//运行线程
thread2.start();
本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启 动线程最终都会调用Thread#run方法
2.2 Java线程的实现原理
线程创建和启动的流程
- 使用new Thread0创建一个线程,然后调用start0方法进行java层面的线程启动
- 调用本地方法start0(),去调用jvm中的JVM_StartThread方法进行线程创建和启动;
- 调用new JavaThread(&thread entry, sz)进行线程的创建,并根据不同的操作系统平台调用对应的os::create thread方法进行线程创建
- 新创建的线程状态为lnitialized,调用了sync->wait0的方法进行等待,等到被唤醒才继续执行thread->run0;:
- 调用Thread::starti(native thread),方法进行线程信动,此时将线程状态设置为RUNNABLE,接着调用os::start_thread(thread),根据不同的操系统选择不同的线程启动方式,
- 线程启动之后状态设置为RUNNABLE, 并唤醒第4步中等待的线程,接着执行thread->run0的方法
- JavaThread::run0方法会回调第1步new Thread中复写的run0方法
Java线程执行为什么不能直接调用run()方法,而要调用start()方法?
直接调用run()方法仅仅简单的调用对象的方法,而调用start()方法会调用本地方法栈,从而让操作系统新建新线程,最终让新线程执行run方法。
2.3 Java线程属于内核级线程
- 内核级线程(Kernel Level Thread ,KLT):
它们是依赖于内核的,即无论是用户进程中的线 程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。
- 用户级线程(User Level Thread,ULT):
操作系统内核不知道应用线程的存在
2.4 Java线程的调度机制
- 协同式线程调度
线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另 外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。
- 抢占式线程调度
每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中, Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有 一个线程导致整个进程阻塞。
2.5 Java线程的生命周期
Java 语言中线程共有六种状态,分别是:
- NEW(初始化状态)
- RUNNABLE(可运行状态+运行状态)
- BLOCKED(阻塞状态)
- WAITING(无时限等待)
- TIMED_WAITING(有时限等待)
- TERMINATED(终止状态)
2.6 Thread常用方法
sleep方法
- 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
- 睡眠结束后的线程未必会立刻得到执行
- sleep当传入参数为0时,和yield相
yield方法
- yield会释放CPU资源 ,让当前线程从 Running 进入 Runnable状态,让优先级更高 (至少是相同)的线程获得执行机会,不会释放对象锁;
- 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比 它优先级更高的线程;
- 具体的实现依赖于操作系统的任务调度器
join方法
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之 后才能继续运行的场景。
2.7 Java线程的中断机制
中断机制是一 种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。被 中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选 择压根不停止
API的使用
- interrupt(): 将线程的中断标志位设置为true,不会停止线程
- isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
- Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志 位,重置为fasle
java
while (!Thread.currentThread().isInterrupted() && more work to do) {
do more work
}
例如:
java
class StopThread implements Runnable{
@Override
public void run() {
for (int i =0 ;
Thread.currentThread().isInterrupted() && i<1000;
i++){
System.out.println(i);
}
}
}
public static void main(String[] args) {
Thread thread3 = new Thread(new StopThread());
thread3.start();
Thread.sleep(5);
thread3.interrupt();
}
注意:
sleep可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位
wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位
上述情况需要重新 线程中断状态为true
java
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
//重新设置线程中断状态为true
Thread.currentThread().interrupt();
}
2.8 Java线程间通信
volatile
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程 之间进行通信。
等待唤醒(等待通知)机制
- wait和notify
等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法, 线程将进入等待队列进行等待直到被唤醒。
- LockSupport park/unpark
LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待"许可",调用 unpark则为指定线程提供"许可"。使用它可以在任何场合使线程阻塞,可以指定任何线程进行 唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样 的。
java
class ParkThread implements Runnable{
@Override
public void run() {
System.out.println("程序正在执行");
System.out.println("程序将要进入等待.....");
LockSupport.park();
}
}
// main...
Thread parkThread = new Thread(new ParkThread());
parkThread.start();
System.out.println("唤醒parkThread");
LockSupport.unpark(parkThread);
管道输入输出流
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程 之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现: PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节, 而后两种面向字符。
Thread.join
join可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等 待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但 是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串 行的,最后join的实现其实是基于等待通知机制的