第一章 - Java与线程
目录
Java与线程
线程的类型
线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度。目前线程是Java里面进行处理器资源调度的最基本单位
实现线程主要有三种方式:使用内核线程实现(1:1实现),使用用户线程实现(1:N实现),使用用户线程加轻量级进程混合实现(N:M实现)
内核线程实现
内核线程实现的方式也被称为1:1实现。内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就称为多线程内核(Multi-Threads Kernel)
轻量级进程 --- java使用的方式
程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(LightWeight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型, 对应了下图轻量级线程和内核线程的关系
由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使其中某一个轻量级进程在系统调用中被阻塞了,也不会影响整个进程继续工作。轻量级进程也具有它的局限性:首先,由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。其次,每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的
但是轻量级进程也是有缺点的, 它是用户层的函数, 实现于内核线程, 所以他的每次函数调用, 都是系统调用, 需要在用户态和内核态之间来回切换, 这代码是非常大的, 并且因为轻量级线程需要和内核线程1 1配对, 需要消耗一定量的内核资源, 所以是有数量的限制的, 还有它的调度代价也相当的大, 这也是当前java存在的一个问题
用户线程实现
用户线程实现的方式被称为1:N实现。广义上来讲,一个线程只要不是内核线程,都可以认为是用户线程(User Thread,UT)的一种,因此从这个定义上看,轻量级进程也属于用户线程,但轻量级进程的实现始终是建立在内核之上的,许多操作都要进行系统调用,因此效率会受到限制,并不具备通常意义上的用户线程的优点
而狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也能够支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系称为一对多的线程模型,下图是进程和用户线程 1:N 的关系
用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要由用户程序自己去处理。线程的创建、销毁、切换和调度都是用户必须考虑的问题,而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至有些是不可能实现的。因为使用用户线程实现的程序通常都比较复杂,除了有明确的需求外(譬如以前在不支持多线程的操作系统中的多线程程序、需要支持大规模线程数量的应用),一般的应用程序都不倾向使用用户线程。Java、Ruby等语言都曾经使用过用户线程,最终又都放弃了使用它。但是近年来许多新的、以高并发为卖点的编程语言又普遍支持了用户线程,譬如Golang、Erlang等,使得用户线程的使用率有所回升
混合实现
线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式,被称为N:M实现。在这种混合实现下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,这大大降低了整个进程被完全阻塞的风险。在这种混合模式中,用户线程与轻量级进程的数量
比是不定的,是N:M的关系, 下图就是用户线程与轻量级进程之间M:N的关系
许多UNIX系列的操作系统,如Solaris、HP-UX等都提供了M:N的线程模型实现。在这些操作系统上的应用也相对更容易应用M:N的线程模型
Java线程的实现
java 1.2之前使用的是绿色线程(说白了就是用户线程)实现, 但是到了jdk1.3后改成了使用基于系统线程实现的java线程
以hotspot为例, 它的每个线程都是直接映射到系统原生线程的, 中间没有额外的间接结构, 这样做的好处就是jvm不需要去管理线程的调度, 全权交给底下的操作系统去处理, 所以何时冻结或唤醒线程、该给线程分配多少处理器执行时间、该把线程安排给哪个处理器核心去执行等,都是由操作系统完成的,也都是由操作系统全权决定的
说白了就是操作系统是怎样的线程, 一定程度上影响了jvm使用了什么线程
Java线程调度
分为两种, 一种是协同式调度(线程运行多久由线程自己决定, 当线程运行完毕, 会告诉系统让它切换到另一个线程)另一种是抢占式调度(线程调度由系统决定)
协同式调度会出现cpu一直卡在线程这里一直运行的情况, 可能导致系统崩溃
抢占式调度线程只能让出自己的执行权限, 想要抢占cpu时间片确是不可能了, 但是可以另辟蹊径, 使用线程优先级来做, 但是说到线程优先级却无法完全保证线程优先运行, 比如: window 就存在"优先级推进器(Priority Boosting)"的功能, 即使线程优先级最高也没用, 这个功能的大致作用是一个线程被执行的特别频繁时, window可能会放弃正在运行的高优先级线程,转而去运行这个频繁被执行的线程
并且java的线程优先级也存在一个问题, 那就是如果系统的优先级等级数量比较多, 那么java的优先级不会出现很多问题, 但是如果系统的优先级只有1个(极端一点), 那么java的所有优先级就一点用都没有了
状态转换
线程的五种状态
系统层面分为五种状态
初始状态: 线程刚刚创建, 这个时候它只是对象, 没有执行start函数
可运行状态: 线程执行了start函数, 但是还未获得时间片
运行状态: 线程获得了时间片
阻塞状态: 线程读取文件或者IO操作, 该线程不会实际使用到cpu, 会导致上下文切换, 进入阻塞状态
终止状态: 线程结束, 生命周期已经结束
线程六种状态
根据Thread.State枚举分为六种状态(java层面分析线程状态)
new(新建): 新建状态, 线程创建, 但是未调用 start() 方法
runnable(运行): 运行状态, 线程正在执行也有可能线程正在等待系统为它分配执行时间
waiting(无限期等待) : 处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线程显式唤醒。以下方法会让线程陷入无限期的等待状态:
- 没有设置Timeout参数的Object::wait()方法;
- 没有设置Timeout参数的Thread::join()方法;
- LockSupport::park()方法
Time Waiting(限期等待): 处于这种状态的线程也不会被分配处理器执行时间,不过无须等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
■Thread::sleep()方法;
■设置了Timeout参数的Object::wait()方法;
■设置了Timeout参数的Thread::join()方法;
■LockSupport::parkNanos()方法;
■LockSupport::parkUntil()方法。
Blocked(阻塞): 线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态
Terminated(结束):已终止线程的线程状态,线程已经结束执行。
下图是线程状态相互转化的方法
下图是RUNNABLE状态当前可能存在的状态
这六种状态的方式, 主要用于调试线程查看线程状态错误
@Slf4j
public class TestState {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") { // NEW
@Override
public void run() {
log.debug("running ... ");
}
};
Thread t2 = new Thread("t2") { // RUNNABLE
@Override
public void run() {
while (true) {
}
}
};
t2.start();
Thread t3 = new Thread("t3") { // TERMINATED
@Override
public void run() {
log.debug("running ... ");
}
};
t3.start();
Thread t4 = new Thread("t4") { // TIMED_WAITING
@Override
public void run() {
synchronized (TestState.class) {
try {
TimeUnit.MILLISECONDS.sleep(1000000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5") { // WAITING
@Override
public void run() {
try {
t2.join();
} catch (Exception e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6") { // BLOCKED
@Override
public void run() {
synchronized (TestState.class) {
try {
TimeUnit.MILLISECONDS.sleep(1000000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
t6.start();
TimeUnit.MILLISECONDS.sleep(500);
log.debug("{} state {}", t1.getName(), t1.getState()); // NEW
log.debug("{} state {}", t2.getName(), t2.getState()); // RUNNABLE
log.debug("{} state {}", t3.getName(), t3.getState()); // TERMINATED
log.debug("{} state {}", t4.getName(), t4.getState()); // TIMED_WAITING
log.debug("{} state {}", t5.getName(), t5.getState()); // WAITING
log.debug("{} state {}", t6.getName(), t6.getState()); // BLOCKED
}
}
重新理解线程状态转换
假设有线程 Thread t
情况 1 NEW --> RUNNABLE
- 当调用 t.start() 方法时,由 NEW --> RUNNABLE
情况 2 RUNNABLE <--> WAITING
t 线程用 synchronized(obj) 获取了对象锁后
-
调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
-
调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
竞争锁成功,t 线程从 WAITING --> RUNNABLE
竞争锁失败,t 线程从 WAITING --> BLOCKED
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码...."); // 断点
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码...."); // 断点
}
},"t2").start();
sleep(0.5);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notifyAll(); // 唤醒obj上所有等待线程 断点
}
}
}
情况 3 RUNNABLE <--> WAITING
-
当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
注意是当前线程在t 线程对象的监视器上等待
-
t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE
情况 4 RUNNABLE <--> WAITING
- 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE
情况 5 RUNNABLE <--> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
-
调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
-
t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
情况 6 RUNNABLE <--> TIMED_WAITING
- 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
注意是当前线程在t 线程对象的监视器上等待
- 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从
TIMED_WAITING --> RUNNABLE
情况 7 RUNNABLE <--> TIMED_WAITING
- 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
- 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
情况 8 RUNNABLE <--> TIMED_WAITING
- 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING--> RUNNABLE
情况 9 RUNNABLE <--> BLOCKED
- t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
情况 10 RUNNABLE <--> TERMINATED
- 当前线程所有代码运行完毕,进入 TERMINATED
线程上下文切换
cpu不可能一直都在一个线程上运行程序, 而是会切换到另一个进程上的线程上运行, 所以这中间需要保存当前线程的场景, 等到下次回到这个线程中执行的时候, 恢复场景使用
而每次切换都需要备份和重新载入这是需要消耗性能资源的
程序计数器+栈帧+局部变量+操作数栈+返回地址等信息需要恢复和备份
线程启动
一般调用start方法后, 线程不一定执行了, 也不一定没执行, 它是不确定的, 可能线程正处于就绪状态, 而且一个线程的start方法只能被调用一次, 多次调用会抛出异常IllegalThreadStateException
判断线程是否启动需要由程序调度器进行判断
线程新建默认状态是 NEW
线程start() 之后状态是 RUNNABLE
线程sleep() 后是 TIME_WAITING
线程阻塞之后是 BLOCKED
线程结束后是 TERMINATED
线程调用join() 方法后是WAITING
sleep方法和yield方法还有wait方法
sleep方法调用之后未必马上sleep, 如果sleep完毕之后线程状态为 TIME_WAIT 状态
sleep后的线程可以被唤醒, 但是会抛出异常
sleep和wait的区别在于, sleep不会释放锁和管程, wait会
线程中断Interrupt
注意Interrupt中的两个方法区别
isInterrupted 和 Interrupted 这两个方法都是判断是否终止了
但是前者不会消除中断标记, 后者会
这种消除中断标记和不消除中断标记的方式主要用在park的时候使用
如果标记没被消除, 该线程下次还是会被park住, 如果被消除, 则下次调用park无效
@Slf4j
public class ParkDemo {
@Test
public void test() throws Exception {
Thread thread = new Thread(() -> {
log.debug("park...");
// park
LockSupport.park();
log.debug("unpark ...");
// 显示打断标记
// log.debug("打断不清除标记: {}", Thread.currentThread().isInterrupted());
log.debug("打断清除标记: {}", Thread.interrupted());
LockSupport.park(); // Thread.interrupted() 清除标记后, 线程会在这里阻塞
log.debug("unpark ...");
});
thread.start();
// 打断线程
thread.interrupt();
thread.join();
}
}
两阶段终止模式
线程结束以前使用 stop() 但这是存在问题的, 比如线程A被stop掉了, 但是它的锁还在没被解除掉, 这样另一个等待这个锁的线程就永远阻塞了, 但是如果使用的是interrupt方法, 不仅仅线程被终止, 而且该线程的锁资源会被释放, 并且使用interrupt方法结束的线程还能够监控到自己被终止( isInterrupted 和 interrupted)而做出相对应的反应
利用 isInterrupted
interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行
class TPTInterrupt {
private Thread thread;
public void start() {
thread = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("将结果保存");
} catch (InterruptedException e) {
current.interrupt();
}
// 执行监控操作
}
}, "监控线程");
thread.start();
}
public void stop() {
thread.interrupt();
}
}
调用
class Zhazha{
public static void main(String[] args) {
TPTInterrupt t = new TPTInterrupt();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
}
}
结果
11:49:42.915 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:43.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:44.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:45.413 c.TestTwoPhaseTermination [main] - stop
11:49:45.413 c.TwoPhaseTermination [监控线程] - 料理后事
利用停止标记
// 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性
// 我们的例子中,即主线程把它修改为 true 对 t1 线程可见
class TPTVolatile {
private Thread thread;
private volatile boolean stop = false;
public void start() {
thread = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (stop) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("将结果保存");
} catch (InterruptedException e) {
}
// 执行监控操作
}
}, "监控线程");
thread.start();
}
public void stop() {
stop = true;
thread.interrupt();
}
}
调用
class Zhazha {
public static void main(String[] args) {
TPTVolatile t = new TPTVolatile();
t.start(); t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
Thread.sleep(3500);
log.debug("stop");
t.stop();
}
}
结果
11:54:52.003 c.TPTVolatile [监控线程] - 将结果保存
11:54:53.006 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.007 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.502 c.TestTwoPhaseTermination [main] - stop
11:54:54.502 c.TPTVolatile [监控线程] - 料理后事
主线程和守护线程
一般情况下, 进程需要等待所有的线程退出后才会退出, 但是守护线程不会, 进程不会等待守护线程, 直接强制退出
我们jvm的垃圾回收gc就是守护线程