线程概述
进程是系统分配资源的基本单位,线程是调度 CPU 的基本单位,一个进程至少包含一个执行线程,线程寄生在进程当中。每个线程都要一个程序计数器(记录要执行的下一条指令),一组寄存器(保存当前线程的工作变量),堆栈(记录执行历史,其中每一帧保存了一个已经调用但未返回的过程)。
线程是调度 cpu 的最小单元,也叫轻量级进程 LWP(Light Weight Process)。
线程创建方式
继承 Thread 类
实现 Runnable 接口
实现 Callable 接口
线程池
其实这四种的底层都是一样的,都是用 Runnable 去创建的。
线程分类
线程的实现可以分为两类:
用户级线程(ULT,User-Level Thread)
内核级线程(KLT,Kernel-Level Thread)
在看线程分类之前先了解下我们的用户空间分类:用户空间,内核空间。如下图
ULT 级线程使用用户空间,KLT 级线程使用内核空间。
用户空间和内核空间之间是完全隔离的,想要交互可以通过交互接口。
ring0、ring3 是对 cpu 操作的特权级别,ring0 权限级别最高,ring3 最低。以前有 ring0、ring1、ring2、ring3 共 4 个级别,现在只有两个 ring0、ring3。
ULT 用有对 cpu 的 ring3 操作权限级别,KLT 拥有对 cpu 的 ring0 操作权限级别
一般情况下,线程执行的时候往往会在用户空间和内核空间进行切换,如执行下面代码时:
String str = "hello world"; // 在用户空间
file.write(str); // 切换到内核空间
int i = 0; // 切换回用户空间
再来看下面这张图:
ULT 在内核空间没有线程的的概念,不能感知到线程,内核空间维护了一张进程表,而这些进程在用户空间,每个进程里面有一张线程表,同时也有很多线程。
KLT 在内核空间维护了一张进程表、一张线程表,可以感知到线程。进程,线程都在用户空间里面,当然线程被包含在进程里面。
java 线程与 KLT 的关系
java 1.2 及其之前 java 线程是 ULT,1.2 之后是 KLT。下面是 java 线程与内核线程的关系:
运行一个 java 程序时,是一个 jvm 进程,jvm 进程在用户空间创建多个线程(栈帧空间),每个线程再通过库调度器 在内核空间创建对应的线程,这些内核线程去调用 CPU 内核去处理。
java 线程生命周期
线程的状态有 5 个,新建、就绪、运行、终止、阻塞(先回到就绪状态继续往下进行)。java 的生命周期如下图:
为什么要用并发
原因
充分利用多核 CPU 的计算能力
方便进行业务拆分,提升应用性能
并发与并行
并行是并发,但是并发不一定是并行。
以前使用的电脑大多数是单核处理器,在这样的电脑上运行多线程代码时。宏观上来看是并行,可从微观上来看,其实是给每个线程分配了不同的时间片,每个时间片串行执行,某时刻实际上只有一个线程在执行。
硬件发展到今天,我们的电脑 cpu 一般是多核多处理器(线程数不大于 cpu 逻辑处理器数量),这样在执行并发代码的时候才是真正的并发(也是并行)。
问题
高并发场景下,导致频繁的上下文切换(用户空间和内核空间切换)。
临界区线程安全问题,容易出现死锁,产生死锁就会造成系统功能不可用。
线程安全,导致一些变量的值出现意外的修改。
案例:查看死锁
接下来我们模拟一个死锁,代码如下:
public class DeadLockDemo {
public static final String resource_a = "aaaa";
public static final String resource_b = "bbbb";
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
synchronized (resource_a){
System.out.println("get resource a");
try{
Thread.sleep(2000);
synchronized (resource_b){
System.out.println("get resource b");
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
Thread threadB = new Thread(() -> {
synchronized (resource_b){
System.out.println("get resource b");
try{
Thread.sleep(2000);
synchronized (resource_a){
System.out.println("get resource a");
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
threadA.start();
threadB.start();
}
}
启动该程序,控制台打印了输出内容,这时程序并没有结束,发生了死锁情况。如下图:
这时在dos窗口执行命令:jps,如下图:
我们刚才执行的程序进程号为3412(通过类名),这时执行命令:jstack 3412 (3412为进程号),定位死锁继而进行处理。如下图:
当然也可以在idea工具中直接查看堆栈信息。