- 进程 process 和 线程 thread
两个名词都是对CPU时间段的描述。
进程就是包括CPU上下文切换和程序执行时间的总和 = CPU加载上下文 + CPU执行 + CPU保存上下文
进程是资源分配的最小单位,由操作系统分配,任意时刻,CPU总是运行一个进程,其他的进程处于非运行状态。
线程由程序自己分配,线程共享了进程的上下文环境,是比进程更小的CPU时间段。一个进程可以包含多个线程,进程在执行
时,CPU会在进程的线程之间来回切换,所以线程是CPU调度的最小单位。
对于Java程序来说,多个线程共享进程的堆和方法区资源,但是每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。
使用JMX查看Java线程:
public class MultiThread {
public static void main(String[] args) {
// 获取 Java 线程管理 MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,仅打印线程 ID 和线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
}
}
}
- Java内存模型
- 并发、并行
并发:在某个时间段内,拥有处理多个任务的能力。比如线程A正在运行,运行到一半,CPU切换到线程B运行,然后又切换回线程A继续运行。
并行:在某一时刻,同时处理多个任务的能力。比如两个人肩并肩同时走,互不影响。
- 线程的生命周期和状态
Java线程在运行的生命周期中的指定时刻只能处于下面6种不同状态中的其中一个:
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但还没有调用start()运行 |
RUNNABLE | 运行状态,java线程将操作系统中的就绪和运行两种状态笼统地称作“可运行” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示当前线程需要等待其他线程做出一些特定的动作(通知或中断) |
TIME_WAITING | 超时等待状态,不同于WAITING,线程可以在指定的时间自行返回 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
Java线程状态变迁如下图所示:
由上图可以看出,线程调用start()方法后处于READY状态,READY状态的线程获得CPU时间片(timeslice)后就处于RUNNING状态。
操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
- 什么是上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
- 线程死锁
6.1 什么是死锁?
线程在等待某个资源被释放,但是这个资源被其他线程无限期占用,导致线程不能获取资源而进入无限期的等待,程序也因此不能正常终止。
下面是模拟死锁的代码:
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
产生死锁必须具备以下四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
6.2如何避免死锁?
我们只要破坏产生死锁的四个条件中的其中一个就可以了。
- 破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
- 破坏请求与保持条件
一次性申请所有的资源。
- 破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
我们对线程 2 的代码修改成下面这样就不会产生死锁了。
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();