Thread线程源码解析,Java线程的状态,线程之间的通信
线程的基本概念
什么是线程
现代操作系统在运行一个程序的时候,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。线代操作系统调度的最小单位是线程。也叫做轻量级进程。在一个进程里可以创建多个线程,这些线程都拥有自己的程序计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉这些线程在同时执行。进程是资源分配的基本单位,线程时系统调用的基本单位。
实际上Java本身就是多线程程序,因为执行main方法的时候就是一个main线程,其实平时一个简单的main方法执行中,有负责分发处理给JVM的线程,也有清除对象引用的线程,还有调用对象的finalize方法的线程等。
线程的优先级
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干个时间片,当线程时间片用完后,就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多分配或者少分配处理器资源的线程属性。
来看一下Thread源码中关于线程优先级的定义。
private int priority;
//最低优先级是 1
public final static int MIN_PRIORITY = 1;
/**
* 默认的线程优先级是 5
*/
public final static int NORM_PRIORITY = 5;
/**
* 线程最高优先级是 10
*/
public final static int MAX_PRIORITY = 10;
在Java线程中,通过一个整形变量priority来控制优先级,优先级的范围从1~10,在下次构建的时候可以通过 setPriority方法来修改线程优先级,默认优先级是5,最高优先级是10,优先级高的线程分配的时间片的数量要多余优先级低的。
设置优先级时,如果是频繁阻塞的线程需要设置较高的优先级,而偏重于计算的线程需要设置较高的优先级,以确保处理器不会被抢占。
线程的状态
这里从Thread源码的角度说一下线程拥有哪些状态。
public enum State {
/**
* 初始状态,线程被构建 ,线程还没有开启start方法
*/
NEW,
/**
* 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统的称为“运行中”
*/
RUNNABLE,
/**
*阻塞状态,表示线程阻塞于锁
*/
BLOCKED,
/**
* 等待状态,表示线程进入等待状态进入该状态,表示当前线程需要等待其他线程做出通知或者中断
*以下方法会是一个线程进入等待状态
*<ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </u>
*/
WAITING,
/**
* 超时等待状态,它可以在指定时间内自行返回
* 以下是会造成 超时等待的方法
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* 终止状态,表示当前线程已经执行完毕
*/
TERMINATED;
}
在给定的一个时刻,线程只能处于其中的一种状态。
从上图中可以看出,线程创建后,调用start()方法开始运行。当线程执行wait方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能返回到运行状态。而超时状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时候会自动返回运行态。当线程调用同步方法是,在没有获取到锁的情况下,线程就会进入到阻塞状态。当线程执行完run方法后就代表这个线程执行完毕进入终止态。
守护线程
守护线程(Daemon线程)是一种支持型线程,它主要被用于程序中后台调度以及支持性工作。当虚拟机中不存在非Daemon线程时,Java虚拟机就会自动退出。
可以通过Thread.setDaemon(true)
将线程设置成守护线程。
举个例子
public class DaemonThread {
public static void main(String[] args) {
Thread daemonRunner = new Thread(new DaemonRunner(), "DaemonRunner");
daemonRunner.setDaemon(true);
daemonRunner.start();
}
static class DaemonRunner implements Runnable{
@Override
public void run() {
try{
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("DaemonThread finally run!");
}
}
}
}
运行main方法,发现控制台什么都没输出,当main线程启动了我们设置的守护线程,main方法执行完毕后终止,Java虚拟机中已经没有非守护线程,JVM退出。所以守护线程需要立即终止,因此守护线程中的finally代码块并没有执行。
使用守护进程的时候,尽量不要分配读写文件之类的任务给守护进程,因为你不知道在用户进程执行完程序之前,守护进程是否可以将数据读出或写入。
在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或者清理资源的逻辑。
线程的初始化方式
构建线程
在运行线程之前需要先构建一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,例如线程优先级,线程组,是否是守护线程等。
Thread源码中的init方法。
//g 代表线程组,target是我们要运行的对象,name是线程名字,默认是 "Thread-" + nextThreadNum(),nextThreadNum 返回自增数字,stackSize设置堆栈的大小
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
//当前线程时该线程的父线程
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
//如果没有线程组就使用父线程的线程组
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();//守护线程,线程优先级都是从父类直接复制过来
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
//如果父线程的inheritThreadLocals不为空时,会把inheritThreadLocals属性的值全部传递给子线程。
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 */
tid = nextThreadID();
}
最终构建的线程对象放在堆中,等待被开启。
创建无返回值的线程的两种方式
- 继承Thread类,成为Thread的子类。
//继承Thread,实现run方法
class MyThread extends Thread{
@Override
public void run() {
log.info(Thread.currentThread().getName());
}
}
//直接start()开启线程就会执行run方法中的内容
public static void main(String[] args) {
new MyThread().start();
}
- 实现Runnable接口,作为Thread的入参
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log.info("{} begin run",Thread.currentThread().getName());
}
});
thread.start()//开启线程去执行
thread.run()//不会开启线程,是在当前主线程上运行 run方法
start()方法源码分析
public synchronized void start() {
/**
*如果该线程没有进过初始化就抛出异常
*
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
//标识符,开始之前是false
boolean started = false;
try {
//start0是native方法,执行完成后创建一个新的线程并运行,target中的内容已经运行了
start0();
started = true;//这里执行的还是主线程
} finally {
try {
//如果started还是false代表执行失败
if (!started) {
//将线程从线程组中删除
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
线程之间的通信
每一个线程都拥有自己的栈空间,按照已经给定的代码一行一行的执行,直到执行完毕。但是如果各个线程之间只是孤立运行,那么产生的价值会很低,如果多个线程之间相互配合的话,将会产生更大的价值。
等待/通知机制有关的方法
等待/通知机制是指:例如线程A调用对象O的wait方法进而进入到了等待状态,而另一个线程B调用对象O的notify/notifyAll()方法,线程A收到通知后从对象O的wait方法中返回,继续执行后面的有关操作。A、B两个线程通过对象O来完成等待方和通信方之间的交互工作。这就是等待/通知机制。
- notify():通知一个对象上等待的线程,返回的前提是该线程获取到对象的锁
- notifyAll():唤醒所有处于该对象上等待状态的线程
- wait():调用该方法的线程进入WAITING状态,只有等待其他线程的通知或被中断才会返回,需要注意的是,wait方法会释放对象的锁。
- wait(long): 超时等待一段时间,这里的参数的单位是毫秒,也就是等待多少毫秒,如果没有超过指定的时间,没有收到其他线程的通知就会自动返回。
- wait(long,int):对于超时时间更细粒度的控制,可以精确到纳秒。
需要注意的是,如果有多个线程处于等待状态,notify方法从等待队列中唤醒一个线程(移动到同步队列中)并且是随机的,我们无法指定从中唤醒的是哪一个。notifyAll方法唤醒的是全部处于等待状态的线程,也就是将等待队列中的所有线程全部移到同步队列中,被移动的线程从WAITING–>BLOCKED。
Thread.join()
join的意思就是当前线程等待另一个线程执行完成之后们才能继续操作。线程除了提供join方法还提供了join(long mills)和join(long mills,int nanos)两个具备超时特性的方法。如果线程thread在给定时间内没有执行完,就会从该方法中返回。
举个例子
**
* 以下代码来自Java并发编程的艺术p103
*/
public class JoinDemo {
public static void main(String[] args) {
Thread previous = Thread.currentThread();
for (int i = 0; i < 10; i++) {
//每个线程都持有上一个线程的引用,需要等待上一个线程终止,才能从等待中返回
Thread thread = new Thread(new Domino(previous), String.valueOf(i));
thread.start();
previous = thread;
}
}
static class Domino implements Runnable {
private Thread thread;
public Domino(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
}
//打印当前线程的名字
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
}
//输出结果
0 terminate.
1 terminate.
2 terminate.
3 terminate.
4 terminate.
5 terminate.
6 terminate.
7 terminate.
8 terminate.
9 terminate.
sleep
public static native void sleep(long millis) throws InterruptedException;
sleep是native
方法,可以接受毫秒的一个入参,也可以接受毫秒和纳秒的两个入参。表示当前线程会沉睡n毫秒/纳秒,在下次沉睡期间,不会释放资源,所以沉睡时,其他线程无法得到锁。
yield
public static native void yield();
yield 是个native方法,表示当前线程做出让步,放弃cpu执行权,让CPU重新选择线程,避免线程过度使用CPU,需要注意的是:当前线程释放CPU后,重新竞争时,CPU可能还会选到当前线程。
在使用while循环时,可以使用yield方法使当前线程在规定时间内结束,防止CPU一直被死循环霸占。
interrupt
interrupt:线程的中断操作,意思是可以打断正在处于运行态或者等待状态的线程。
Object#wait ()、Thread#join ()、Thread#sleep (long) 这些方法运行后,线程的状态是 WAITING 或 TIMED_WAITING,这时候打断这些线程,就会抛出 InterruptedException 异常,使线程的状态直接到 TERMINATED;
在IO操作中,如果IO阻塞,主动打断线程,连接就会关闭,并抛出ClosedByInterrupException异常;
private Logger log = LoggerFactory.getLogger(InterruptTest.class);
@Test
public void testInterrupt() throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log.info("{} begin run",Thread.currentThread().getName());
try {
log.info("子线程开始沉睡 30 s");
Thread.sleep(30000L);
} catch (InterruptedException e) {
log.info("子线程被打断");
e.printStackTrace();
}
log.info("{} end run",Thread.currentThread().getName());
}
});
// 开一个子线程去执行
thread.start();
Thread.sleep(1000L);
log.info("主线程等待 1s 后,发现子线程还没有运行成功,打断子线程");
thread.interrupt();
}
执行结果
参考:《Java并发编程的艺术》方腾飞
Thread线程源码解析,Java线程的状态,线程之间的通信的更多相关文章
- Spring IOC容器启动流程源码解析(四)——初始化单实例bean阶段
目录 1. 引言 2. 初始化bean的入口 3 尝试从当前容器及其父容器的缓存中获取bean 3.1 获取真正的beanName 3.2 尝试从当前容器的缓存中获取bean 3.3 从父容器中查找b ...
- Hadoop中Yarnrunner里面submit Job以及AM生成 至Job处理过程源码解析
参考 http://blog.csdn.net/caodaoxi/article/details/12970993 Hadoop中Yarnrunner里面submit Job以及AM生成 至Job处理 ...
- .NET 事件总线,简化项目、类库、线程、服务等之间的通信,代码更少,质量更好。
Jaina .NET 事件总线,简化项目.类库.线程.服务等之间的通信,代码更少,质量更好. 安装 Package Manager Install-Package Jaina .NET CLI do ...
- 【Java实战】源码解析Java SPI(Service Provider Interface )机制原理
一.背景知识 在阅读开源框架源码时,发现许多框架都支持SPI(Service Provider Interface ),前面有篇文章JDBC对Driver的加载时应用了SPI,参考[Hibernate ...
- Spark作业执行流程源码解析
目录 相关概念 概述 源码解析 作业提交 划分&提交调度阶段 提交任务 执行任务 结果处理 Reference 本文梳理一下Spark作业执行的流程. Spark作业和任务调度系统是其核心,通 ...
- HotSpot学习(二):虚拟机的启动过程源码解析
1. 前言 上文介绍了HotSpot编译和调试的方法,而这篇文章将迈出正式调试的第一步--调试HotSpot的启动过程. 学习启动过程可以帮助我们了解程序的入口,并对虚拟机的运行有个整体的把握,方便日 ...
- Thread类源码解析
源码版本:jdk8 其中的部分论证和示例代码:Java_Concurrency 类声明: Thread本身实现了Runnable接口 Runnable:任务,<java编程思想>中表示该命 ...
- Dubbo服务调用过程源码解析④
目录 0.服务的调用 1.发送请求 2.请求编码 3.请求的解码 4.调用具体服务 5.返回调用结果 6.接收调用结果 Dubbo SPI源码解析① Dubbo服务暴露源码解析② Dubbo服务引用源 ...
- 源码解析Java Attach处理流程
前言 当Java程序运行时出现CPU负载高.内存占用大等异常情况时,通常需要使用JDK自带的工具jstack.jmap查看JVM的运行时数据,并进行分析. 什么是Java Attach 那么JVM自带 ...
随机推荐
- 落谷 P1412 经营与开发
题目链接 Solution 用传统的思想考虑正推,发现后面的答案依赖于当前的 \(p\),你不但要记录前 \(i\) 个还要记录 \(p\),显然空间爆炸. 类似 AcWing 300. 任务安排1, ...
- 深入理解Java虚拟机(八)——类加载机制
是什么是类加载机制 Java虚拟机将class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程就是类加载机制. 类的生命周期 一个类从加载到内存 ...
- word IF嵌套实现登记学生成绩(合格,良好,优秀)
word IF函数 IF语法 IF(判断条件,条件成立的动作,条件不成立的动作),以逗号(英文)分隔 例:=IF( 0 < 1 , "good" , "bad&qu ...
- window下使用cmd查看端口占用的进程,并杀死该进程
做项目的时候经常会遇到"address already in use"的情况,此时可以选择使用dos命令将该进程杀死. 首先,查找端口对应的进程,使用命令(以进程号8080为例): ...
- 数据结构与算法——循环链表的算法实现(Joseph 问题)
Joseph 问题: 如果有10 个人,按编号顺序1,2,...,10 顺时针方向围成一圈.从1 号开始顺时针方向1,2,...,9 报数,凡报数9 者出列(显然,第一个出圈为编号9 者). 最后一个 ...
- js--前端开发工作中常见的时间处理问题
前言 在前端开发工作中,服务端返回的时间数据或者你传递给服务端的时间参数经常会遇到时间格式转换及处理问题.这里分享一些我收集到的一些处理方法,方便日后工作中快速找到.先附上必须了解的知识内置对象传送门 ...
- Mysql性能优化专栏
1. 最大数据量 Mysql没有对单表的数据量大小做限制,单表的大小取决于操作系统对文件大小的限制. <阿里巴巴Java开发手册>中建议当单表的数据量大小超过500万行或者大于2GB时需 ...
- ssh-copy-id三步实现SSH免密登录
背景 在日常工作中,不希望每次登录都输入密码,这里主要介绍一种简单的配置Linux主机间免密登录的方式 先了解两个核心命令: ssh-keygen :产生公钥和私钥对 ssh-copy-id:将北极的 ...
- Collections.synchronizedList 并发
1.背景 集合类中的map,大家一定熟悉,知道它非线程安全.使用的方法有两种,一种是在map上加同步器(锁),另一种是创建容器时使用Collections中的静态方法对map进行包装. java ap ...
- Docker(四):Docker安装Redis
查找Redis镜像 镜像仓库 https://hub.docker.com/ 下拉镜像 docker pull redis 查看镜像 docker images 创建Redis容器 运行Redis镜像 ...