【并发】深入理解Java线程的底层原理
【并发】深入理解Java线程的底层原理
线程基础知识
一、线程与进程
进程
操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
线程
线程,有时被称为轻量级进程,是操作系统调度(CPU调度)执行的最小单位。
线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
【面试题】进程与线程的区别
(1)进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
(2)进程拥有共享的资源,如内存空间等,供其内部的线程共享
(3)进程间通信较为复杂
- 同一台计算机的进程通信称为 IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
(4)线程间通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
(5)线程更轻量,线程的上下文切换成本更低
进程间通信的方式
相信大学期间有准备过408的小伙伴应该很熟悉!大致是有6种方式。
1. 管道(pipe)及有名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
2. 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的
3. 消息队列(message queue):生产者与消费者之间的通信
4. 共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
5. 信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。(线程级别也可以!)
6. 套接字(socket):这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
在开发中有一个很常见的场景,就是分布式锁(redis,mysql,zk)!进程与进程之间通信也可以通过操作共享内存的方式去实现。
二、线程的同步互斥
线程同步
是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
举一个例子
我们开发一个网页,用户在浏览器操作,浏览器就会向Tomcat服务器发请求,Tomcat可以将请求转发到一个具体的应用(应用中也有线程),可能会去查MySQL数据库。
浏览器线程、tomcat线程、应用中的线程、数据库线程,它们之间相互协作去完成请求与响应,这其实就是一个同步的过程。
线程互斥
线程互斥对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用(例如:synchronized ),其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
上下文切换
上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。
上下文切换步骤
- 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
- 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
- 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。
上下文切换只能在内核模式下发生。
内核模式是CPU的特权模式,其中只有内核运行,并提供对所有内存位置和所有其他系统资源的访问。
其他程序(包括应用程序)最初在用户模式下运行,但它们可以通过系统调用运行部分内核代码。
【面试题】什么情况下会发生上下文切换?
- 线程、进程切换
- 系统调用,当发生某些预先不可知的异常时,就会切换到内核态,以执行相关的异常事件。
- 中断,在使用外围设备时,如外围设备完成了用户请求,就会向CPU发送一个中断信号,此时,CPU就会暂停执行原本的下一条指令,转去处理中断事件。此时,如果原来在用户态,则自然就会切换到内核态。
具体看这里:
内核模式(Kernel Mode)vs 用户模式(User Mode)
三、操作系统层面线程生命周期
操作系统层面的线程生命周期基本上可以用下图这个“五态模型”来描述。这五态分别是:初始状态、可运行状态、运行状态、休眠状态和终止状态。
这个五态模型也是我们之前学习操作系统的时候课本里面讲的(当然还有“七态模型”)。
这里我们直接用网图:
五态模型
七态模型
但是需要注意的是,不同的语言规范,这些状态模型的数量、名称都会发生改变!!!我们这里就用Java为例
Java中的状态模型
看Thread源码可以发现,在Java中线程一共有6种状态
public enum State {
// 创建态
NEW,
// 运行态 + 就绪态
RUNNABLE,
// 阻塞态(synchronized)
BLOCKED,
// 等待(join、wait、park)
WAITING,
// 有时限的等待(sleep)
TIMED_WAITING,
// 终止态
TERMINATED;
}
Java线程详解
一、Java线程的实现方式
方式1:使用 Thread类或继承Thread类
public class demo01 {
public static void main(String[] args) {
MyThread th1 = new MyThread();
MyThread th2 = new MyThread();
th1.start();
// start()方法只能调用一次,否则报错!
th1.start();
//区别start 与 run 方法的区别
//run 没有开启线程
//start 启动线程,由JVM虚拟机调用线程的
}
}
public class MyThread extends Thread{
@Override
public void run() {
//100.fori
for (int i = 0; i < 100; i++) {
System.out.println("线程执行了" + i);
}
}
}
方式2:实现 Runnable 接口配合Thread
把【线程】和【任务】(要执行的代码)分开
- Thread 代表线程
- Runnable 可运行的任务(线程要执行的代码)
public class demo02 {
public static void main(String[] args) {
//创建一个参数对象
MyRunnable mr = new MyRunnable();
//创建一个线程对象,把参数传递给这个线程
//在线程启动之后,执行的就是参数里面的run()方法
Thread t1 = new Thread(mr);
t1.start();
Thread t2 = new Thread(mr);
t2.start();
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 使用getName() 方法获取线程对象,不能直接使用getName()
// 要使用 currentThread() 方法获取当前Thread类对象
System.out.println(Thread.currentThread().getName() + "多线程的实现" + i);
}
}
}
方式3:使用有返回值的 Callable
它与上面的那个Runnable最大的区别就是是否有返回值?是否可以自动抛异常?
public class Demo03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<String> ft = new FutureTask<>(mc);
Thread t1 = new Thread(ft);
t1.start();
System.out.println(ft.get());
}
}
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("第三种线程启动方式" + i);
}
//返回值就表示线程运行完毕的结果
//上面的 泛型 就表示返回值的数据类型
//第三种方法最大的区别 (优势)
return "启动";
}
}
使用 ft.get() 获取返回值
方式4:使用 lambda
public class Demo {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
}).start();
}
}
}
方式5:线程池
public class Demo {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(10, 20,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(10));
for (int i = 0; i < 100; i++) {
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread());
}
});
}
}
}
本质上Java中实现线程只有一种方式
本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终都会调用Thread#run方法
只不过,创建任务(Taks)的方式有很多种而已!!!
二、Java线程实现原理(start与run)
Java的线程是怎么实现的,或者换句话说,是怎么让这个线程运行起来的???
在Java是通过start方法调用run方法让这个线程运行起来的:
JVM中线程执行流程
- 使用 new Thread() 创建一个线程,然后调用 start0()方法 进行 Java 层面的线程启动
- 调用本地方法start0(),去调用 JVM 中的JVM_StartThread方法进行线程创建和启动(JVM层面)
- 调用 new JavaThread(&thread entry, sz) 进行线程的创建,并根据不同的操作系统平台调用对应的 os::create thread 方法进行线程创建(真正创建一个线程!)
- 新创建的线程状态为 lnitialized(初始化状态),调用了sync->wait() 的方法进行等待(先阻塞!!!),等到被唤醒才继续执行 thread->run()
- 调用 Thread::start(native thread) 方法进行线程启动,此时将线程状态设置为RUNNABLE,接着调用 os::start_thread(thread),根据不同的操作系统选择不同的线程启动方式
- 线程启动之后状态设置为RUNNABLE,并唤醒第4步中等待的线程,接着执行 thread->run() 的方法
- JavaThread::run()方法会回调第1步new Thread中复写的run()方法。
简单的来说就是:
在OS层面的线程处于初始化的状态后,会进入等待状态,直到JVM层面的线程通过绑定为runnable状态的方式将其唤醒,OS层面的线程开始向回调run()方法
这个过程涉及到用户态到内核态的切换,所以说Java线程的创建、销毁时一个很重的操作!
这也是为什么要有线程池?为什么要谈线程复用的原因?!!!
Java线程属于内核级线程!!!
内核级线程
它们是依赖于内核的,即无论是用户进程中的线程,还是系统进程中的线程,它们的创建、撤消、切换都只能由内核实现。
用户级线程
操作系统内核不知道应用线程的存在。
协程
是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特性。
这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
- 线程的切换由操作系统调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
- 线程的默认stack大小是1M,而协程更轻量,接近1k。因此可以在相同的内存中开启更多的协程。
- 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
关于协程与线程的对比
Java之协程(quasar) - 小不点丶 - 博客园 (cnblogs.com)https://www.cnblogs.com/ll409546297/p/10945119.html协程适用于被阻塞的,且需要大量并发的场景(网络IO)。不适合大量计算的场景。
三、Java线程的调度机制
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式分两种,分别是协同式线程调度和抢占式线程调度(Java中线程调度的机制)
协同式线程调度
线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。
好处:是实现简单,且切换操作对线程自己是可知的,没有线程同步问题。
坏处:是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。
抢占式线程调度
每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。
线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。
Java线程调度就是抢占式调度
如果我们想让系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。
Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY 至 Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行(注意,不是一定!)
但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统!!!
例如下面的例子:
public class SellTicketDemo implements Runnable {
/**
* 卖车票
*/
private int ticket;
public SellTicketDemo() {
this.ticket = 1000;
}
@Override
public void run() {
while (ticket > 0) {
synchronized (this) {
if (ticket > 0) {
try {
// 线程进入暂时的休眠
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取到当前正在执行的程序的名称,打印余票
System.out.println(Thread.currentThread().getName()
+ ":正在执行操作,余票:" + ticket--);
}
}
// 释放时间片
Thread.yield();
}
}
public static void main(String[] args) {
SellTicketDemo demo = new SellTicketDemo();
Thread thread1 = new Thread(demo,"thread1");
Thread thread2 = new Thread(demo,"thread2");
Thread thread3 = new Thread(demo,"thread3");
Thread thread4 = new Thread(demo,"thread4");
//priority优先级默认是5,最低1,最高10
thread1.setPriority(Thread.MIN_PRIORITY);
thread2.setPriority(Thread.MAX_PRIORITY);
thread3.setPriority(Thread.MIN_PRIORITY);
thread4.setPriority(Thread.MAX_PRIORITY);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
运行结果
四、Java线程的生命周期(了解即可)
这个我们刚刚在上文中谈到过!
Java 语言中线程共有六种状态
- NEW(初始化状态)
- RUNNABLE(可运行状态+运行状态)
- BLOCKED(阻塞状态)
- WAITING(无时限等待)
- TIMED_WAITING(有时限等待)
- TERMINATED(终止状态)
在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。
也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权!!!
从 JavaThread 的角度,JVM定义了一些针对Java Thread对象的状态(jvm.h)
从 OSThread 的角度,JVM还定义了一些线程状态给外部使用,比如用jstack输出的线程堆栈信息中线程的状态(osThread.hpp)
五、Thread常用方法
sleep方法
- 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁!
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
- 睡眠结束后的线程未必会立刻得到执行
- sleep当传入参数为0时,和yield相同
yield方法
- yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁;
- 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程;
- 具体的实现依赖于操作系统的任务调度器
join方法
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。
public class ThreadJoinDemo {
public static void main(String[] sure) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t begin");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t finished");
}
});
long start = System.currentTimeMillis();
t.start();
//主线程等待线程t执行完成
t.join();
System.out.println("执行时间:" + (System.currentTimeMillis() - start));
// 主线程
System.out.println("Main finished");
}
}
假设我们要让主线程在子线程执行结束后才开始工作,就可以使用 子线程.join() 方法!
stop方法
stop() 方法已经被jdk废弃,原因就是stop()方法太过于暴力,强行把执行到一半的线程终止。
那么有什么办法可以令线程停止???
Java的中断机制!
六、Java线程的中断机制(优雅的停止线程)
Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。
中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。
被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。
API的使用
- interrupt():将线程的中断标志位设置为true,不会停止线程
- isInterrupted():判断当前线程的中断标志位是否为true,不会清除中断标志位
- Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为false
利用中断机制优雅的停止线程
public class StopThread implements Runnable {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted() && count < 1000) {
System.out.println("count = " + count++);
}
System.out.println("线程停止: stop thread");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new StopThread());
thread.start();
Thread.sleep(5);
thread.interrupt();
}
}
在这个循环中,我们可以通过判断中断标志位的方式来控制线程!
注意:使用中断机制时一定要注意是否存在中断标志位被清除的情况(这里有个坑!sleep、wait)
sleep 期间能否感受到中断
在上面案例的代码中加上一个sleep()!
public class StopThread implements Runnable {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted() && count < 1000) {
System.out.println("count = " + count++);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
// 重新设置线程中断状态为true!!!
// Thread.currentThread().interrupt();
}
}
System.out.println("线程停止: stop thread");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new StopThread());
thread.start();
Thread.sleep(5);
thread.interrupt();
}
}
运行结果
处于休眠中的线程被中断,线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。
这样就会导致while中的条件Thread.currentThread().isInterrupted()为false,程序会在不满足count < 1000这个条件时退出。
如果不在catch中重新手动添加中断信号,不做任何处理,就会屏蔽中断请求,有可能导致线程无法正确停止!
sleep可以被中断,抛出中断异常:sleep interrupted,清除中断标志位
wait可以被中断,抛出中断异常:InterruptedException,清除中断标志位