Java 并发编程(二 )Thread
线程状态
线程一般的状态转换图如下:

在线程生命周期中存在的状态解释如下:
New(初始化)状态此时线程刚刚被实例化,可以通过调用
start()方法来启动这个实例化的的线程,使其状态转变成为Ready状态Runnable状态Ready状态和Running状态统称为Runnable状态Ready(就绪)状态此时线程已经可以被操作系统调度了,但是此时还没有执行,当被操作系统调度,获得 CPU 的执行权限后,此时线程的状态就转变成为
Ready状态。Running状态此时线程获得了 CPU 的时间片,正在执行中
Blocked(阻塞)状态此时线程由于某种原因(获取锁、IO 等)无法利用 CPU
Wating(等待)状态此时线程由于某些原因,需要等待其它的线程执行完成之后才能继续执行,与阻塞状态不同的地方在与,等待状态是自己主动地等待某个操作完成,而阻塞则是由于一些不可控的外在因素被动地等待
TIMED_WATING(超时等待)状态和等待状态类似,但是与之不同的地方在于超时等待状态会在指定的时间之后自行结束等待
Terminated(终止)状态该线程的任务已经完成了,是时候被操作系统回收了
等待队列[2]

- 线程 1 正在持有对象的锁,此时线程 5 和线程 6 由于未获得锁而处于阻塞状态;线程 2、线程 3、线程 4 没有去夺取锁,因此处于等待状态,位于等待队列中
- 线程 1 通过调用
wait方法释放当前持有的锁,进入等待状态,当前线程 1 在释放锁之后进入等待状态中 - 在同步队列中,线程 5 和线程 6 发起一次对对象锁的竞争,假设线程 5 获取到对象的锁(
synhronized是非公平锁) - 线程 5 通过调用
notifyAll()方法唤醒在等待队列中的所有线程,使得它们进入同步队列 - 线程 5 执行结束,释放当前对象锁
- 之后由同步队列中的线程竞争锁,最终获取到锁的线程是未知的
Deamon 线程
Deamon 线程也被称为“守护线程”, 这一类线程的主要任务是给其它的线程提供一些支持性的工作。 JVM 在不存在非 Deamon 线程的情况下将会退出,因此 Deamon 线程中的任务不一定会被执行
创建一个 Deamon 线程的方式如下:
Thread thread = new Thread(() ->{});
thread.setDaemon(true); // 将当前的线程设置为 Deamon 线程
线程的取消与关闭[1]
Java 没有显式的方式直接去停止一个正在运行的线程,尽管 Thread 类存在 stop 和 suspend 等方法显式地终止线程,但是这些方法都存在严重的缺陷,因此应当避免使用这些方法。
Java 提供了一种中断的方式来取消一个线程,这是一种协作机制,即使一个线程向另一个线程发送中断信号,从而停止另一个线程的工作
如果一个线程能够在某个操作正常完成之前就能够将其置入 “完成” 状态,那么这个线程就被称为是 “可取消的”。
取消一个正在运行的线程有以下几种情况:
用户取消请求
使用
JMX或其它显式的取消操作有时间限制的操作
比如,如果一个服务端程序在 3s 的时间内都没能做出响应,那么就丢弃这个请求
错误
如果由于底层的某些硬件限制,导致出现错误的情况。如:内存已满、磁盘已满等
关闭
当一个程序或者服务关闭之后,必须对正在处理和等待的线程执行对应的操作,使得程序能够正常退出。在这个过程中,某些正在等待的线程可能会被取消
线程中断
与线程中断相关的方法如下:
public class Thread {
// 这个方法会中断当前线程
public void interrupt(){}
// 该静态方法将会清除当前线程的中断状态
public static boolean interrupted(){}
// 检测当前的线程是否被中断
public boolean isInterrupted(){}
}
线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适或者可能的情况下停止当前工作,并转而执行其它的工作。
在 Java 的 API 规范中,并没有将中断与任何取消语义关联起来,但实际上,如果在取消之外的其它操作中使用中断,那么都是不合适的,并且很难支撑起更大的应用
与一般的设置一个 boolean 位来自定义中断不同,自定义的中断在某些情况下可能不能按照预期的工作,这是因为:有个阻塞库的 API 在调用时将会导致自定义的取消操作无法执行,从而使得整个操作一直都是阻塞的。在这种情况下,使用线程中断将能够解决这一类问题,因为一般的阻塞库 API 都会检查当前线程是否已经被中断,从而终止某些操作
线程中断的本质只是设置线程的中断标志,大部分的的阻塞库 API 对于中断的处理如下:清除当前线程的中断标记,然后抛出 InterruptedException。这是因为:每个新创建的线程都不是在自己的线程环境下运行的,它只能在父线程服务(如线程池)中进行,对于中断的响应应当是通知调用者执行自定义的后续处理,而不是由自己处理,这就是为什么大部分的阻塞库函数都只是抛出 InterruptedException 异常的原因
调用 interrupt 并不意味着立即停止目标线程正在执行的工作,而只是传递了请求中断的消息
在使用静态的 interrupted() 方法时要格外小心,因为它会清除当前线程的中断状态。如果在调用 interrupted() 时返回了 true,那么除非希望屏蔽这个中断,否则应当再次调用 Thread 对象的实例方法 interrupt() 来恢复线程的中断状态。具体的示例如下:
import java.util.concurrent.*;
// 此代码来自 《Java 并发编程实战》
public class TaskRunnable implements Runnable {
BlockingQueue<Task> queue;
public void run() {
try {
processTask(queue.take());
} catch (InterruptedException e) {
/*
由于 BlockingQueue 的 take 方法在响应中断时会清除线程的中断状态,
因此在捕获到这个异常时需要再次将当前的线程的中断状态恢复
*/
Thread.currentThread().interrupt();
}
}
void processTask(Task task) {
}
interface Task {
}
}
响应中断
一般响应中断主要有以下两种方式:
传递异常,使得调用该方法的方法也变成可中断的阻塞方法
BlockingQueue<Task> queue;
// 抛出 InterruptedException,使得 getNextTask 成为可中断的阻塞方法
public Task getNextTask() throws InterruptedException {
return queue.take();
}
恢复中断状态,使得调用栈中的上层代码能够对其进行处理
如果不想或者无法传递
InterruptedException,那么恢复线程的中断状态将是一个可选的方案
只有在实现了中断策略的代码才能屏蔽中断请求,在常规的任务和代码库中都不应该屏蔽中断请求
线程的生命周期
线程创建
Java 中创建一个线程,最终对应的源代码如下
private Thread(
ThreadGroup g,
Runnable target,
String name,
long stackSize,
AccessControlContext acc,
boolean inheritThreadLocals
) {
// 省略一部分参数检测代码。。。。
this.name = name;
// 将当前线程设置为要创建的线程的父线程
Thread parent = currentThread();
if (g == null) {
if (g == null) {
g = parent.getThreadGroup();
}
}
g.checkAccess();
// 省略一部分不太重要的代码。。。。
g.addUnstarted();
this.group = g;
// 将 Deamon、priority 属性设置为父线程对应的属性
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
// 省略一部分不太重要的代码
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
// 将父线程相关的 ThrealLocal 对象复制到当前创建的线程
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
// 设置当前要创建的线程的 ID
this.tid = nextThreadID();
}
线程启动
新创建的线程只是一个单独的对象,如果需要使得操作系统能够去调度这个线程,那么就需要调用线程对象的 start() 方法,将当前线程的状态转换为 Runnbale 状态
具体的 start() 源代码为 native 方法,在此不做深入的分析
线程运行
一般正常的运行,在上文已经详细介绍过,在此不做赘述
线程终止
可以通过线程中断的方式或者自定义 boolean 标志位的方式来终止一个线程
public class ShutDown {
public static void main(String[] args) {
Runner one = new Runner();
Thread countThread = new Thread(one, "Thread One");
countThread.start();
countThread.interrupt(); // 使用线程中断的方式来终止当前执行的线程
Runner two = new Runner();
countThread = new Thread(two, "Thread Two");
countThread.start();
two.cancel(); // 通过自定义 boolean 标志位的方式来终止一个线程
}
static class Runner implements Runnable {
private long i;
private volatile boolean on = true; // 这个字段必须是 volatile 修饰的
public void run() {
while (on && !Thread.currentThread().isInterrupted())
i++;
}
public void cancel() {
on = false;
}
}
}
线程间通信
JMM
由 JMM 定义的偏序规则,volatile 和加锁都可以实现线程之间的通信
volatile 修饰的变量保证了变量的可见性和有序性
synchronized 和其它的加锁机制保证了线程之间的可见性和排它性
等待/通知机制
等待方(消费者)
- 获取对象的锁
- 如果条件不满足,则调用锁对象的
wait()方法 - 条件满足则执行对应的逻辑
伪代码形式如下:
synchronized (lock) {
while (condition) {
lock.wait();
}
// 对应的处理逻辑
}
通知方(生产者)
- 获取对象的锁
- 改变原有条件
- 唤醒所有在等待队列中的线程
伪代码的形式如下:
synchronized (lock) {
// 改变等待方的条件
lock.notifyAll(); // 避免使用 notify,因为它会随机唤醒一个在等待队列中的线程
}
join 方法
在 JDK 1.7 中的描述如下:[3]
Waiting for the finalization of a thread
In some situations, we will have to wait for the finalization of a thread. For example, we may have a program that will begin initializing the resources it needs before proceeding with the rest of the execution. We can run the initialization tasks as threads and wait for its finalization before continuing with the rest of the program. For this purpose, we can use the join() method of the Thread class. When we call this method using a thread object, it suspends the execution of the calling thread until the object called finishes its execution.
大概意思:主线程等待子线程的终止。如果在主线程的代码块中,遇到了 t.join() ,那么当前执行的线程就需要等待 t 执行完成之后才能继续执行。
join 方法的本质是通过锁对象的 wait() 方法来实现的(即 “等待/通知” 机制),对应的源代码如下:
public final synchronized void join(long millis)
throws InterruptedException {
/*
isAlive() 用于判断当前的线程是否存活,
这里的主要目的是避免由于锁对象的虚假唤醒带来的影响
*/
while (isAlive()) {
wait(0);
}
// 省略一部分不太重要的代码
}
由于是调用锁对象的 wait() 方法,因此 join() 方法会释放当前持有的锁
参考:
[1] 《Java 并发编程实战》
[2] https://blog.csdn.net/pange1991/article/details/53860651
[3] https://www.cnblogs.com/duanxz/p/5038471.html
Java 并发编程(二 )Thread的更多相关文章
- Java并发编程:Thread类的使用
Java并发编程:Thread类的使用 在前面2篇文章分别讲到了线程和进程的由来.以及如何在Java中怎么创建线程和进程.今天我们来学习一下Thread类,在学习Thread类之前,先介绍与线程相关知 ...
- Java并发编程二三事
Java并发编程二三事 转自我的Github 近日重新翻了一下<Java Concurrency in Practice>故以此文记之. 我觉得Java的并发可以从下面三个点去理解: * ...
- 【转】Java并发编程:Thread类的使用
一.线程的状态 在正式学习Thread类中的具体方法之前,我们先来了解一下线程有哪些状态,这个将会有助于对Thread类中的方法的理解. 线程从创建到最终的消亡,要经历若干个状态.一般来说,线程包括以 ...
- 3、Java并发编程:Thread类的使用
Java并发编程:Thread类的使用 在前面2篇文章分别讲到了线程和进程的由来.以及如何在Java中怎么创建线程和进程.今天我们来学习一下Thread类,在学习Thread类之前,先介绍与线程相关知 ...
- 【Java并发编程二】同步容器和并发容器
一.同步容器 在Java中,同步容器包括两个部分,一个是vector和HashTable,查看vector.HashTable的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并 ...
- Java 并发编程(二):如何保证共享变量的原子性?
线程安全性是我们在进行 Java 并发编程的时候必须要先考虑清楚的一个问题.这个类在单线程环境下是没有问题的,那么我们就能确保它在多线程并发的情况下表现出正确的行为吗? 我这个人,在没有副业之前,一心 ...
- Java并发编程 (二) 并发基础
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.CPU多级缓存-缓存一致性 1.CPU多级缓存 上图展示的是CPU高级缓存的配置,数据的读取和存 ...
- 【Java并发编程二】Java并发包
1.Java容器 1.1.同步容器 Vector ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问.数组的缺点是每个元素之间不能有间隔,当数组大小不满足时 ...
- Java并发编程:Thread类的使用介绍
在学习Thread类之前,先介绍与线程相关知识:线程的几种状态.上下文切换,然后接着介绍Thread类中的方法的具体使用. 以下是本文的目录大纲: 一.线程的状态 二.上下文切换 三.Thread类中 ...
- Java并发编程:Thread类的使用(转载)
一:线程的状态: 在正式学习Thread类中的具体方法之前,我们先来了解一下线程有哪些状态,这个将会有助于后面对Thread类中的方法的理解. 线程从创建到最终的消亡,要经历若干个状态.一般来说,线程 ...
随机推荐
- pbjs 无法编码 bytes 类型数据问题的解决方案
问题背景 之前写过一篇<使用脚本收发 protobuf 协议数据>,通过 pbjs 命令可以将 protobuf 二进制数据转换为 json: > pbjs msg.proto -- ...
- Xshell链接不上解决问题
#5.远程连接工具排错? #一.测试网络是否通畅 1.测试网络连通性:ping 服务端ip地址 2.关闭防火墙 systemctl stop firewalld #关闭防火墙 systemctl di ...
- ios ipa apple company 开发者账号申请分享攻略
ios公司开发者账号申请分享攻略 好不容易终于申请下来了ios 公司开发者账号,真是一路艰辛和漫长啊,特别是对于远在大洋彼岸的大中华国家.以下我就分享一下这一路下来的经验,希望对于那些新手同仁们有所帮 ...
- idea 连接远程 docker 并部署项目到 docker
目录 idea 连接远程 docker 1. 安装 docker 插件 2. 登录远程服务器,修改docker配置 3. 添加云服务器防火墙规则 4. idea 配置连接 docker 部署项目到 d ...
- 一次完整的Http请求过程(转)
一次完整的Http请求过程 在网上看了很多关于http完整流程的介绍文档,都讲的很不错,但是还是各有缺失,所以自己就根据学习及理解整理了一张图,给大家分享下http一次完整的交互流程,只是大概画了下流 ...
- Golang面试题从浅入深高频必刷「2023版」
大家好,我是阳哥.专注Go语言的学习经验分享和就业辅导. Go语言特点 Go语言相比C++/Java等语言是优雅且简洁的,是我最喜爱的编程语言之一,它既保留了C++的高性能,又可以像Java,Pyth ...
- 文心一言 VS 讯飞星火 VS chatgpt (129)-- 算法导论11.1 4题
四.用go语言,我们希望在一个非常大的数组上,通过利用直接寻址的方式来实现一个字典.开始时该数组中可能包含一些无用信息,但要对整个数组进行初始化是不太实际的,因为该数组的规模太大.请给出在大数组上实现 ...
- 带您了解 O2OA 流程中的人工活动处理方式
这次咱们来介绍 O2OA (翱途) 开发平台流程引擎中的人工活动的处理方式和逻辑,O2OA (翱途) 主要采用拖拽可视化开发的方式完成流程的设计和配置,不需要过多的代码编写,业务人员可以直接进行修改操 ...
- 看完包你搞懂Redis缓存穿透、击穿和雪崩!!!说到做到
缓存穿透 缓存穿透是指当用户对Redis发出无效或者不存在的数据信息操作时,这条数据在Redis中不存在,Redis就会在MySQL数据库中查询,可时无效的信息在mysql数据库中也不存在,就会造成R ...
- 吉客云与用友U8的系统数据集成对接方案
吉客云与用友U8之间的系统数据集成方案.吉客云作为一款电商ERP产品,旨在为企业的数字化升级提供全方位的支持.用友U8是一个经过多年发展的信息化管理系统,见证了企业信息化从简单到精细.从局部到全面的转 ...