目录
Thread.Sleep(0) vs Sleep(1) vs Yield
参考
Java创建线程的四种方式_Andy-CSDN博客_java创建线程的四种方式
JAVA中断机制_小虾米的博客-CSDN博客_java中断机制
线程和进程
何为进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。
何为线程
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
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());
}
}
}
上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可):
[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
[3] Finalizer //调用对象 finalize 方法的线程
[2] Reference Handler //清除 reference 线程
[1] main //main 线程,程序入口
从上面的输出内容可以看出:一个 Java 程序的运行是 main 线程和多个其他线程同时运行
简述线程,程序、进程的基本概念。以及他们之间关系是什么?
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
线程上下文的切换比进程上下文切换要快很多
进程切换时,涉及到当前进程的CPU环境的保存和新被调度运行进程的CPU环境的设置。
线程切换仅需要保存和设置少量的寄存器内容,不涉及存储管理方面的操作。
从 JVM 角度说进程和线程之间的关系
下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
线程理论
线程的生命周期和状态
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):
原图中 wait 到 runnable 状态的转换中,join
实际上是Thread
类的方法,但这里写成了Object
。
由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start()
方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
在操作系统中层面线程有 READY 和 RUNNING 状态,而在 JVM 层面只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
为什么 JVM 没有区分这两种状态呢?
现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。
当线程执行 wait()
方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)
方法或 wait(long millis)
方法可以将 Java 线程置于 TIMED_WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()
方法之后将会进入到 TERMINATED(终止) 状态。
注意:阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为Java.concurrent包中Lock接口对于 阻塞的实现均使用了LockSupport类中的相关方法。
Java线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别 是协同式(Cooperative Threads-Scheduling)线程调度和抢占式(Preemptive ThreadscbeduHng)线程调度。
如果使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上去。
协同式多线程 的最大好处是实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切 换操作对线程自己是可知的,所以没有什么线程同步的问题。Lua语言中的“协同例 程”就是这类实现。
它的坏处也很明显:线程执行时间不可控制,甚至如果一个线程编 写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。很久以前的 Windows 3.x系统就是使用协同式来实现多进程多任务的,那是相当的不稳定,一个进 程坚持不让出CPlJ执行时间就会导致整个系统的崩溃。
如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程 的切换不由线程本身来决定(在Java中,Thread.yield()可以让出执行时间,但是要获 取执行时间的话,线程本身是没有什么办法的)。在这种实现线程调度的方式下,线程 的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的间题。
Java使用的线程调度方式就是抢占式调度。与前面所说的Windows 3.x的例子相对,在Windows 9x/NT内核中就是使用抢占式来实现多进程的,当一个进程出了问题,我们还可以使用任 务管理器把这个进程杀掉,而不至于导致系统崩溃。
虽然说Java线程调度是系统自动完成的,但是我们还是可以“建议”系统给某些 线程多分配一点执行时间,另外的一些线程则可以少分配一点一一这项操作可以通过设置线程优先级来完成。
Java语言一共设置了10个级别的线程优先级Thread.MlN_ PRIORITY至Thread.MAX_PRIORITY), 在两个线程同时处于Ready状态时,优先级 越高的线程越容易被系统选择执行。
不过,线程优先级并不是太靠谱,原因是Java的线程是被映射到系统的原生线程 上来实现的,所以线程调度最终还是由操作系统说了算,虽然现在很多操作系统都提供线程优先级的概念,但是并不见得能与Java线程的优先级一一对应,如Solaris中有 2147483648 (2的31次方)种优先级,但Windows中就只有7种,比Java线程优先级 多的系统还好说,中间留下一点空位就是了,但比Java线程优先级少的系统,就不得不 出现几个优先级相同的情况了,显示了Java线程优先级与Windows线程优先级对应关系,Windows平台的JDK中使用了除THREAD _PRIORITY _IDLE之外的 其余6种线程优先级。
“线程优先级并不是太靠谱“,不仅仅是说在一些平台上不同的优先 级实际会变得相同这一点,还有其他情况让我们不能太依赖优先级:优先级可能会 被系统自行改变。例如在Windows系统中存在一个名为“优先级推进器”的功能 (Priority Boosting, 当然它可以被关闭掉),它的大致作用就是当系统发现一个线程被执行得特别“勤奋努力”的话,可能会越过线程优先级去为它分配执行时间。因此 我们不能在程序中通过优先级来完全准确地判断一组状态都为Ready的线程将会先 执行哪一个。
线程优先级
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线 程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待浩下次分配。线程 分配到的时间片多少也就决定了线程使用处理器资涌的多少,而线程优先级就是决定线程需 要多或者少分配一些处理器资源的线程属性。
在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10, 在线 程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5, 优先级高的线程分 配时间片的数量要多于优先级低的线程。设置线程优先级时,针对频繁阻塞(休眠或者IO操 作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较 低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异, 有些操作系统甚至会忽略对线程优先级的设定。
可以看到线程优先级没有生效,优先级1和优先级10的Job计数的结果非常相近, 没有明显差距。这表示程序正确性不能依赖线程的优先级高低。
注意:线程优先级不能作为程序正确性的依赖,因为橾作系统可以完全不用理会Java 线程对于优先级的设定。笔者的环境为: Mac OS X 10.10, Java版本为1.7.0一71, 经过笔者验证 该环境下所有Java线程优先级均为5(通过jstack查看),对线程优先级的设置会被忽略。另外, 尝试在Ubuntu 14. 04环境下运行该示例,输出结果也表示该环境忽略了线程优先级的设置。
Daemon线程
Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这 意味着当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调 用Thread. setDaemon(true)将线程设置为Daemon线程。
注意:Daemon的属性需要在启动线程之前设置,不能在启动线程之后设置。
Daemon线程被用作完成支待性工作,但是在Java虚拟机退出时Daemon线程中的finally块 并不一定会执行。
注意:在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
什么是上下文切换?
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
1 主动让出 CPU,比如调用了 sleep()
, wait()
等。
2 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死。
3 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
4 被终止或结束运行
这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。
上下文切换的步骤
在上下文切换中,会保存和恢复上下文,丢失局部性,把大量的时间消耗在线程切换上而不是线程运行上。
为什么线程切换会开销如此之大呢?线程间的切换会涉及到以下几个步骤
将 CPU 从一个线程切换到另一线程涉及挂起当前线程,保存其状态,例如寄存器,然后恢复到要切换的线程的状态,加载新的程序计数器,此时线程切换实际上就已经完成了;此时,CPU 不在执行线程切换代码