线程基础知识
线程和进程
进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中 还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的 。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程,也有的程序只能启动一个实例进程。
- 操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。
- 线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。
线程
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
- 线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。
进程与线程的区别
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂,同一台计算机的进程通信称为 IPC(Inter-process communication)不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
进程间通信的方式
- 管道(pipe)及有名管道(named pipe)
- 信号(signal)信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
- 消息队列(message queue)
- 共享内存(shared memory)
- 信号量(semaphore)主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
- 套接字(socket)
线程的同步互斥
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
- 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。(在一段时间内只允许一个线程访问的资源就称为临界资源)。
- 互斥量:为协调共同对一个共享资源的单独访问而设计的。
- 信号量:为控制一个具有有限数量用户资源而设计。
- 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。
上下文切换(Context switch)
上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。
内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动:
- 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
- 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
- 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。
上下文切换只能在内核模式下发生。
- 系统调用。
- 异常事件。当发生某些预先不可知的异常时,就会切换到内核态,以执行相关的异常事件。
- 设备中断。
CAS不涉及到用户态到内核态切换,没有系统调用,CAS是比较与交换
操作系统层面线程生命周期
初始状态、可运行状态、运行状态、休眠状态和终止状态。
Java 语言里则把可运行状态和运行状态合并了
查看进程线程的方法
windows
任务管理器可以查看进程和线程数,也可以用来杀死进程
tasklist 查看进程
taskkill 杀死进程
linux
ps -ef 查看所有进程
ps -fT -p <PID> 查看某个进程(PID)的所有线程
kill 杀死进程top
按大写 H 切换是否显示线程
top -H -p <PID> 查看某个进程(PID)的所有线程
Java
jps 命令查看所有 Java 进程
jstack <PID> 查看某个 Java 进程(PID)的所有线程状态
jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
Java线程详解
Java线程的实现方式
- 使用 Thread类或继承Thread类
- 实现 Runnable 接口配合Thread
- 使用有返回值的 Callable
class CallableTask implements Callable<Integer> { @Override public Integer call() throws Exception { return 1; }}
- 使用 lambda
本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终都会调用Thread#run方法
Java线程实现原理
Thread#start()源码
线程的创建和启动总结:
- 使用new Thread()创建一个线程,然后调用start()方法进行java层面的线程启动
- 初始化时会执行静态代码块,执行本地方法添加对应表
- 调用本地方法start0(),去调用jvm中的JVM_StartThread方法进行线程创建和启动
- 调用new JavaThread(&thread_entry,sz)进行线程的创建,并根据不同的操作系统平台调用对应的os::creat_thread(thread),方法进行线程创建
- 新创建的线程状态为Initialized,调用了sync->wait()方法进行等待,等到被唤醒才继续执行thread->run();
- 调用Thread::start(native_thread);方法进行线程启动,此时线程状态设置为RUNNABLE,接着调用os::start_thread(thread),根据不同的操作系统选择不同的线程启动方式
- 线程启动之后状态设置为RUNNABLE,并唤醒等待的线程,接着执行thread->run()方法
- javaThread::run()方法会回调第一步new Thread中复写的方法
Java线程属于内核级线程
依赖于内核的,即无论是用户进程中的线程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。
用户级线程
操作系统内核不知道应用线程的存在。
协程
Java线程的调度机制
Java线程调度就是抢占式调度
Java线程的生命周期
Java 语言中线程共有六种状态,分别是:
- NEW(初始化状态)
- RUNNABLE(可运行状态+运行状态)
- BLOCKED(阻塞状态)
- WAITING(无时限等待)
- TIMED_WAITING(有时限等待)
- TERMINATED(终止状态)
Thread常用方法
sleep方法
- 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
- 睡眠结束后的线程未必会立刻得到执行
- sleep当传入参数为0时,和yield相同
yield方法
- yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁;
- 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程;
- 具体的实现依赖于操作系统的任务调度器
join方法
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。
Java线程的中断机制
interrupt(): 将线程的中断标志位设置为true,不会停止线程
isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle
sleep可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位
wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位
Java线程间通信
volatile
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信
等待唤醒(等待通知)机制
wait和notify
等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒。
缺陷:只能在synchronized同步代码块里面使用,notify()只会唤醒一个等待线程,唤醒全部需要使用notifyAll()
LockSupport(AQS使用唤醒同步队列的线程)
LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用unpark则为指定线程提供“许可”。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。优点:可以唤醒指定线程,任意场景都可使用,可以提前调用unpark 后续的park都会获取结果 不会阻塞
Thread.join
join可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现其实是基于等待通知机制的。