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类中的方法的理解. 线程从创建到最终的消亡,要经历若干个状态.一般来说,线程 ...
随机推荐
- linux中的sar命令
linux中的sar命令 sar命令的安装 [root@localhost test]# yum install sysstat 安装成功! sar命令说明 语法格式 sar [ 选项 ] [ < ...
- 深入理解RocketMQ 广播消费
这篇文章我们聊聊广播消费,因为广播消费在某些场景下真的有奇效.笔者会从基础概念.实现机制.实战案例.注意事项四个方面一一展开,希望能帮助到大家. 1 基础概念 RocketMQ 支持两种消息模式:集群 ...
- 记一次 .NET 某拍摄监控软件 卡死分析
一:背景 1. 讲故事 今天本来想写一篇 非托管泄露 的生产事故分析,但想着昨天就上了一篇非托管文章,连着写也没什么意思,换个口味吧,刚好前些天有位朋友也找到我,说他们的拍摄监控软件卡死了,让我帮忙分 ...
- SSM(Spring+SpringMVC+MyBatis)框架集成
引言 进行SSM(Spring+SpringMVC+MyBatis)集成的主要原因是为了提高开发效率和代码可维护性.SSM是一套非常流行的Java Web开发框架,它集成了Spring框架.Sprin ...
- 【matplotlib 实战】--堆叠面积图
堆叠面积图和面积图都是用于展示数据随时间变化趋势的统计图表,但它们的特点有所不同.面积图的特点在于它能够直观地展示数量之间的关系,而且不需要标注数据点,可以轻松地观察数据的变化趋势.而堆叠面积图则更适 ...
- .Net7自定义GC垃圾回收器
1.前言 CLR和GC高度耦合,.Net7里面分离CLR和GC,则比较容易实现这件事情.本篇来看下,自定义一个GC垃圾回收器. 2.概述 这里首先演示下自定义GC垃圾回收后的效果. 1.下载Custo ...
- Use Closures Not Enumerations
http://c2.com/ Use Closures Not Enumerations I was really disappointed when this turned out not to ...
- 【matplotlib 实战】--堆叠柱状图
堆叠柱状图,是一种用来分解整体.比较各部分的图.与柱状图类似,堆叠柱状图常被用于比较不同类别的数值.而且,它的每一类数值内部,又被划分为多个子类别,这些子类别一般用不同的颜色来指代. 柱状图帮助我们观 ...
- 嵌入式BI的精解与探索
摘要:本文由葡萄城技术团队原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言 1996年,商业智能(BI)的概念首次浮现,随后的20多年间,商 ...
- 聊聊Maven的依赖传递、依赖管理、依赖作用域
1. 依赖传递 在Maven中,依赖是会传递的,假如在业务项目中引入了spring-boot-starter-web依赖: <dependency> <groupId>org. ...