对Java线程概念的理解

您所在的位置:网站首页 java为什么要支持线程 对Java线程概念的理解

对Java线程概念的理解

#对Java线程概念的理解| 来源: 网络整理| 查看: 265

1、什么是线程

  现代操作系统在运行一个程序时, 会为其创建一个进程。 例如, 启动一个Java程序, 操作系统就会创建一个Java进程。 现代操作系统调度的最小单元是线程, 也叫轻量级进程(Light Weight Process) , 在一个进程里可以创建多个线程, 这些线程都拥有各自的计数器、 堆栈和局部变量等属性, 并且能够访问共享的内存变量。 处理器在这些线程上高速切换, 让使用者感觉到这些线程在同时执行。

  一个Java程序从main()方法开始执行, 然后按照既定的代码逻辑执行, 看似没有其他线程参与, 但实际上Java程序天生就是多线程程序, 因为执行main()方法的是一个名 称为main的线程。 下面使用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()) ; } } }

 

ThreadMXBean :

Java 虚拟机线程系统的管理接口。

Java 虚拟机具有此接口的实现类的单一实例。实现此接口的实例是一个 MXBean,可以通过调用 ManagementFactory.getThreadMXBean() 方法或从平台 MBeanServer 方法获得它。

在 MBeanServer 内唯一标识线程系统的 MXBean 的 ObjectName 是:

java.lang:type=Threading

线程 ID 线程 ID 是一个通过调用线程的 Thread.getId() 方法返回的 long 型正值。线程 ID 在其生存期间是唯一的。线程终止时,该线程 ID 可以被重新使用。 此接口中的某些方法将线程 ID 或线程 ID 数组作为输入参数,并返回每个线程的信息。

线程 CPU 时间 Java 虚拟机实现可能支持测量当前线程的 CPU 时间、测量任何线程的 CPU 时间,或者有可能不测量任何线程的 CPU 时间。 isThreadCpuTimeSupported() 方法可用于确定 Java 虚拟机是否支持测量任何线程的 CPU 时间。isCurrentThreadCpuTimeSupported() 方法可用于确定 Java 虚拟机是否支持测量当前线程的 CPU 时间。支持任何线程 CPU 时间测量的 Java 虚拟机实现也支持当前线程的 CPU 时间测量。

此接口提供的 CPU 时间具有毫微秒精度,但并不具有毫微秒的准确性。

Java 虚拟机可能默认禁用 CPU 时间测量。isThreadCpuTimeEnabled() 和 setThreadCpuTimeEnabled(boolean) 方法可用于测试是否启用 CPU 时间测量,并且可以分别启用/禁用此支持。启用线程 CPU 测量在某些 Java 虚拟机实现中可能开销很大。

线程争用监视 某些 Java 虚拟机可能支持线程争用监视。当启用线程争用监视时,将收集由于同步而受阻塞的线程累积时间和等待通知的线程累积时间,并在 ThreadInfo 对象中返回它们。 isThreadContentionMonitoringSupported() 方法可用于确定 Java 虚拟机是否支持线程争用监视。默认情况下,线程争用监视是禁用的。setThreadContentionMonitoringEnabled(boolean) 方法可用于启用线程争用监视。

同步信息和死锁检测 一些 Java 虚拟机可以支持使用对象监视器和使用可拥有同步器的监视。getThreadInfo(long[], boolean, boolean) 和 dumpAllThreads(boolean, boolean) 方法可以用来获取线程堆栈跟踪和同步信息,这些信息包括在获得线程时被哪一个锁阻塞或者正在等待哪一个锁,以及线程当前拥有哪些锁。 ThreadMXBean 接口提供 findMonitorDeadlockedThreads() 和 findDeadlockedThreads() 方法,这些方法用于在运行的应用程序中查找死锁。

 

java.lang.management.ManagementFactory

 

 

ManagementFactory 类是一种工厂类,用于获取 Java 平台的管理 Bean。此类由静态方法组成,每种静态方法都会返回一个或多个表示 Java 虚拟机组件的管理接口的平台 MXBean。

应用程序可以采用以下方式访问平台 MXBean:

直接访问 MXBean 接口 通过静态工厂方法获取 MXBean 实例,从本地访问正在运行的虚拟机的 MXBean。构造一个 MXBean 代理实例,以通过调用 newPlatfromMXBeanProxy 将方法调用转发到给定的 MBeanServer。构造代理通常是为了远程访问另一个正在运行的虚拟机的 MXBean。通过 MBeanServer 间接地访问 MXBean 接口 通过平台 MBeanServer 本地访问 MXBean 或通过特定的 MBeanServerConnection 远程访问 MXBean。MXBean 的属性和操作仅使用 JMX 开放类型,包括在 OpenType 中定义的基本数据类型 CompositeData 和 TabularData。映射关系在下面指定。

平台 MXBean

平台 MXBean 是一种管理 Bean,它符合 JMX Instrumentation Specification,仅使用以下所描述的一组基本数据类型。有关细节,请参阅 MXBean 规范。JMX 管理应用程序和平台 MBeanServer 可以进行互操作,而无需 MXBean 特定数据类型的类。JMX 连接器服务器和连接器客户机之间传送的数据类型为开放类型,而这就允许不同版本进行互操作。

JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。

平台 MXBean 接口仅使用以下数据类型:

基本类型,如 int、long、boolean 等等基本类型的包装器类(如 Integer、Long、Boolean 等等)和 StringEnum 类仅定义获取方法和具有 CompositeData 参数的静态 from 方法的类,以便将输入的 CompositeData 转换为该类的实例List 其中 E 为基本类型、包装类、枚举类或支持从 CompositeData 转换到此类的类Map 其中 K 和 V 为基本类型、包装类、枚举类或支持从 CompositeData 转换到此类的类

当通过 MBeanServer 访问平台 MXBean 的属性或操作时,将按以下方式对数据类型进行映射:

基本类型或包装器类映射到相同的类型。Enum 映射到 String(其值为枚举常数的名称)。仅定义获取方法和具有 CompositeData 参数的 from 方法的类映射到 CompositeData。Map 映射到 TabularData,后者的行类型为 CompositeType,此类型具有两个项,其名称分别为 "key" 和 "value",项类型分别为 K 和 V 对应的映射类型,"key" 为索引。List 映射到数组,该数组中的元素类型为 E 的映射类型。元素类型为 E 的数组映射到具有相同维数的数组,该数组的元素类型为 E 的映射类型。

平台 MXBean 的 MBeanInfo 将属性和操作的数据类型描述为上述指定要映射到的基本类型或开放类型。

例如,MemoryMXBean 接口具有以下获取和设置方法:

public MemoryUsage getHeapMemoryUsage(); public boolean isVerbose(); public void setVerbose(boolean value);

MemoryMXBean 的 MBeanInfo 中的这些属性的名称和类型如下:

属性名称类型HeapMemoryUsageCompositeData representing MemoryUsageVerboseboolean

MXBean 名称

Java 虚拟机的每个平台 MXBean 都具有唯一的 ObjectName,以在平台 MBeanServer 中注册。Java 虚拟机具有以下管理接口的单一实例:

管理接口对象名称ClassLoadingMXBeanjava.lang:type=ClassLoadingMemoryMXBeanjava.lang:type=MemoryThreadMXBeanjava.lang:type=ThreadingRuntimeMXBeanjava.lang:type=RuntimeOperatingSystemMXBeanjava.lang:type=OperatingSystem

Java 虚拟机具有以下管理接口的零个或一个实例:

管理接口对象名称CompilationMXBeanjava.lang:type=Compilation

Java 虚拟机可能具有以下管理接口的一个或多个实例。

管理接口对象名称GarbageCollectorMXBeanjava.lang:type=GarbageCollector,name=collector's nameMemoryManagerMXBeanjava.lang:type=MemoryManager,name=manager's nameMemoryPoolMXBeanjava.lang:type=MemoryPool,name=pool's name

 

java.lang.management.ThreadInfo

 

线程信息。ThreadInfo 包含有关线程的信息,包括:

常规线程信息

线程 ID。线程名称。

执行信息

线程状态。由于以下原因阻塞线程的对象: 等待进入同步块/方法,或者等待 Object.wait 方法的通知,或者由于 LockSupport.park 调用而暂停。拥有阻塞线程对象的线程的 ID。线程的堆栈跟踪。线程锁定的对象监视器列表。线程锁定的可拥有同步器列表。

同步统计数据

由于同步或等待通知而阻塞线程的次数。从启动线程争用监视开始,由于同步或等待通知而阻塞线程的累计时间。某些 Java 虚拟机实现可能不支持此功能。ThreadMXBean.isThreadContentionMonitoringSupported() 方法可用于确定 Java 虚拟机是否支持此功能。

此线程信息类设计用于监视系统,不用于同步控制。

 

 

输出结果如下(多次运行,结果可能不同):

[4] Signal Dispatcher //分发处理发送给JVM信号的线程 [3] Finalizer //调用对象finalize方法的线程 [2] Reference Handler //清除Reference的线程 [1] main //main线程,用户程序入口 [ 5] Attach Listener [ 4] Signal Dispatcher [ 3] Finalizer [ 2] Reference Handler [ 1] main

 可以看到, 一个Java程序的运行不仅仅是main()方法的运行, 而是main线程和多个其他线程的同时运行。

 

2、为什么要使用多线程

  执行一个简单的”Hello,World!”, 却启动了那么多的”无关”线程, 是不是把简单的问题复杂化了? 当然不是, 因为正确使用多线程, 总是能够给开发人员带来显著的好处, 而使用多线程的原因主要有以下几点:

  (1)更多的处理器核心

  随着处理器上的核心数量越来越多, 以及超线程技术的广泛运用, 现在大多数计算机都比以往更加擅长并行计算, 而处理器性能的提升方式, 也从更高的主频向更多的核心发展。 如何利用好处理器上的多个核心也成了现在的主要问题。线程是大多数操作系统调度的基本单元, 一个程序作为一个进程来运行, 程序运行过程中能够创建多个线程, 而一个线程在一个时刻只能运行在一个处理器核心上。 试想一下, 一个单线程程序在运行时只能使用一个处理器核心, 那么再多的处理器核心加入也无法显著提升该程序的执行效率。 相反, 如果该程序使用多线程技术, 将计算逻辑分配到多个处理器核心上, 就会显著减少程序的处理时间, 并且随着更多处理器核心的加入而变得更有效率。

  (2)更短的响应时间

  有时我们会编写一些较为复杂的代码(这里的复杂不是说复杂的算法, 而是复杂的业务逻辑), 例如, 一笔订单的创建, 它包括插入订单数据、 生成订单快照、 发送邮件通知卖家和记录货品销售数量等。 用户从单击“订购”按钮开始, 就要等待这些操作全部完成才能看到订购成功的结果。 但是这么多业务操作, 如何能够让其更快地完成呢?在上面的场景中, 可以使用多线程技术, 即将数据一致性不强的操作派发给其他线程处 理(也可以使用消息队列) , 如生成订单快照、 发送邮件等。 这样做的好处是响应用户请求的线程能够尽可能快地处理完成, 缩短了响应时间, 提升了用户体验。

  (3)更好的编程模型

  Java为多线程编程提供了良好、 考究并且一致的编程模型, 使开发人员能够更加专注于问题的解决, 即为所遇到的问题建立合适的模型, 而不是绞尽脑汁地考虑如何将其多线程化。 一旦开发人员建立好了模型, 稍做修改总是能够方便地映射到Java提供的多线程编程模型上。

3、线程优先级

  现代操作系统基本采用时分的形式调度运行的线程, 操作系统会分出一个个时间片, 线程会分配到若干时间片, 当线程的时间片用完了就会发生线程调度, 并等待着下次分配。 线程分配到的时间片多少也就决定了线程使用处理器资源的多少, 而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。

  在Java线程中, 通过一个整型成员变量priority来控制优先级, 优先级的范围从1~10, 在线程构建的时候可以通过setPriority(int)方法来修改优先级, 默认优先级是5, 优先级高的线程分配时间片的数量要多于优先级低的线程。 设置线程优先级时, 针对频繁阻塞(休眠或者I/O操作) 的线程需要设置较高优先级, 而偏重计算(需要较多CPU时间或者偏运算) 的线程则设置较低的优先级, 确保处理器不会被独占。 在不同的JVM以及操作系统上, 线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定(因此,在程序中设置线程优先级的实践意义并不大,因为线程优先级的最终解释权在底层操作系统)。示例代码如下:

public class Priority { private static volatile boolean notStart = true; private static volatile boolean notEnd = true; public static void main(String[ ] args) throws Exception { List jobs = new ArrayList(); for (int i = 0; i < 10; i++) { int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY; Job job = new Job(priority); jobs.add(job); Thread thread = new Thread(job, "Thread: " + i); thread.setPriority(priority); thread.start(); } notStart = false; TimeUnit. SECONDS.sleep(10); notEnd = false; for (Job job : jobs) { System.out.println("Job Priority : " + job.priority + ",Count : " + job.jobCount); } } static class Job implements Runnable { private int priority; private long jobCount; public Job(int priority) { this. priority = priority; } public void run() { while (notStart) { Thread.yield() ; } while (notEnd) { Thread.yield() ; jobCount++; } } }

  运行该示例, 在博主笔记本上的输出如下:

Job Priority : 1, Count : 1259592 Job Priority : 1, Count : 1260717 Job Priority : 1, Count : 1264510 Job Priority : 1, Count : 1251897 Job Priority : 1, Count : 1264060 Job Priority : 10, Count : 1256938 Job Priority : 10, Count : 1267663 Job Priority : 10, Count : 1260637 Job Priority : 10, Count : 1261705 Job Priority : 10, Count : 1259967 12345678910

  从输出可以看到线程优先级没有生效, 优先级1和优先级10的Job计数的结果非常相近,没有明显差距。 这表示程序正确性不能依赖线程的优先级高低。因为操作系统可以完全不用理会Java 线程对于优先级的设定。

4、并发与并行的区别

  如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。并发系统与并行系统这两个定义之间的关键差异在于“存在”这个词。

  在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是同时“存在”的——每个线程都处于执行过程中的某个状态。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。

  “并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。

  用一个极其简单的生活实例来解释如下:   你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。   你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。   你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。   并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。

5、线程状态及其切换

  下面的这个图非常重要!你如果看懂了这个图,那么对于多线程的理解将会更加深刻。

  1、新建状态(New):新创建了一个线程对象。   2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程队列中,变得可运行,等待获取CPU的使用权。   3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。   4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会再次转到运行状态。阻塞的情况分三种:   (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中(wait会释放持有的锁)。   (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。   (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态(注意,sleep不会释放线程所持有的锁)。   5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

  Java线程在运行的生命周期中可能处于下图表中所示的6种不同的状态, 在给定的一个时刻,线程只能处于其中的一个状态。

  下面使用jstack工具(可以选择打开终端, 键入jstack或者到JDK安装目录的bin目录下执行命令), 尝试查看示例代码运行时的线程信息, 更加深入地理解线程状态。测试代码如下:

public class ThreadState { // 该线程不断地进行睡眠 static class TimeWaiting implements Runnable { @Override public void run() { while (true) { SleepUtils.second(100); } } } // 该线程在Waiting.class实例上等待 static class Waiting implements Runnable { @Override public void run() { while (true) { synchronized (Waiting.class) { try { Waiting.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } // 该线程在Blocked.class实例上加锁后, 不会释放该锁 static class Blocked implements Runnable { @Override public void run() { synchronized (Blocked.class) { while (true) { SleepUtils.second(100); } } } } public static void main(String[] args) { new Thread(new TimeWaiting (), "TimeWaitingThread").start(); new Thread(new Waiting(), "WaitingThread").start(); // 使用两个Blocked线程, 一个获取锁成功, 另一个被阻塞 new Thread(new Blocked(), "BlockedThread-1").start(); new Thread(new Blocked(), "BlockedThread-2").start(); } }

  上述示例中使用的SleepUtils代码如下:

public class SleepUtils { public static final void second(long seconds) { try { TimeUnit.SECONDS.sleep(seconds) ; } catch (InterruptedException e) { } } }

  运行该示例, 打开终端或者命令提示符, 键入“jps”, 输出如下:

16544 Jps 13700 10156 ThreadState

  可以看到运行示例对应的进程ID是10156,接着再输入“jstack 10156”,部分输出如下:

//BlockedThread-2线程阻塞在获取Blocked.class示例的锁上 "BlockedThread-2" #13 prio=5 os_prio=0 tid=0x00000000180ad800 nid=0x108c waiting for monitor entry [0x0000000018e8f000] java.lang.Thread.State: BLOCKED (on object monitor) at ThreadState$Blocked.run(ThreadState.java:35) - waiting to lock (a java.lang.Class for ThreadState$Blocked) at java.lang.Thread.run(Thread.java:745) //BlockedThread-1线程获取到了Blocked.class的锁,处于睡眠状态 "BlockedThread-1" #12 prio=5 os_prio=0 tid=0x00000000180ad000 nid=0x2740 waiting on condition [0x0000000018d8e000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) at SleepUtils.second(SleepUtils.java:6) at ThreadState$Blocked.run(ThreadState.java:35) - locked (a java.lang.Class for ThreadState$Blocked) at java.lang.Thread.run(Thread.java:745) //WaitingThread线程在Waitting实例上等待 "WaitingThread" #11 prio=5 os_prio=0 tid=0x00000000180a6000 nid=0x93c in Object.wait() [0x0000000018c8f000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on (a java.lang.Class for ThreadState$Waiting) at java.lang.Object.wait(Object.java:502) at ThreadState$Waiting.run(ThreadState.java:21) - locked (a java.lang.Class for ThreadState$Waiting) at java.lang.Thread.run(Thread.java:745) //TimeWaitingThread线程处于超时等待 "TimeWaitingThread" #10 prio=5 os_prio=0 tid=0x00000000180a5000 nid=0x8c4 waiting on condition [0x0000000018b8f000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) at SleepUtils.second(SleepUtils.java:6) at ThreadState$TimeWaiting.run(ThreadState.java:8) at java.lang.Thread.run(Thread.java:745)

  通过示例, 我们了解到Java程序运行中线程状态的具体含义。 线程在自身的生命周期中,并不是固定地处于某个状态, 而是随着代码的执行在不同的状态之间进行切换。

  下面是一张更详细的线程状态迁移图:

  从图中可以看到, 线程创建之后, 调用start()方法开始运行(这里的运行状态其实是就绪态和运行态的合集)。 当线程执行wait()方法之后, 线程进入等待状态。 进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态, 而超时等待状态相当于在等待状态的基础上增加了超时限制, 也就是超时时间到达时将会返回到运行状态。 当线程调用同步方法时, 在没有获取到锁的情况下, 线程将会进入到阻塞状态。 线程在执行Runnable的run()方法之后将会进入到终止状态。

  注意:阻塞状态是线程在进入synchronized关键字修饰的方法或代码块(尝试获取锁) 时没有拿到锁的状态,但是阻塞在java.concurrent包中Lock接口 的线程状态却是等待状态, 因为java.concurrent包中 Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。

6、Daemon线程

  Daemon线程是一种支持型线程, 因为它主要被用作程序中后台调度以及支持性工作。 这意味着, 当一个Java虚拟机中不存在非Daemon线程的时候, Java虚拟机将会退出。 可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。

  注意:Daemon属性需要在启动线程之前设置, 不能在启动线程之后设置。

  Daemon线程被用作完成支持性工作, 但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行, 示例代码如下:

public class Daemon { public static void main(String[] args) { Thread thread = new Thread(new DaemonRunner(), "DaemonRunner"); thread.setDaemon(true) ; thread.start(); } static class DaemonRunner implements Runnable { @Override public void run() { try { SleepUtils.second(10); } finally { System.out.println("DaemonThread finally run. "); } } } }  

  运行Daemon程序, 可以看到在终端或者命令提示符上没有任何输出。 main线程(非Daemon线程) 在启动了线程DaemonRunner之后随着main方法执行完毕而终止, 而此时Java虚拟机中已经没有非Daemon线程, 虚拟机需要退出。 Java虚拟机中的所有Daemon线程都需要立即终止, 因此DaemonRunner立即终止, 但是DaemonRunner中的finally块并没有执行。因此, 在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

7、线程的启动、中断与终止

  线程对象在初始化完成之后, 调用start()方法就可以启动这个线程。 线程start()方法的含义是: 当前线程(即parent线程) 同步告知Java虚拟机, 只要线程规划器空闲, 应立即启动调用start()方法的线程。

  注意:启动一个线程前, 最好为这个线程设置线程名 称, 因为这样在使用jstack分析程序或者进行问题排查时, 就会给开发人员提供一些提示, 自定义的线程最好能够起个名字。

  中断可以理解为线程的一个标识位属性, 它表示一个运行中的线程是否被其他线程进行了中断操作。 中断好比其他线程对该线程打了个招呼, 其他线程通过调用该线程的interrupt()方法对其进行中断操作。

  线程通过检查自身是否被中断来进行响应, 线程通过方法isInterrupted()来进行判断是否被中断, 也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。 如果该线程已经处于终结状态, 即使该线程被中断过, 在调用该线程对象的isInterrupted()时依旧会返回false。

  调用某个线程的interrupt()方法,将会设置该线程为中断状态,即设置为true。线程中断后的结果是死亡、还是等待新的任务或是继续运行至下一步,取决于这个程序本身。线程会不时地检测这个中断标识位,以判断线程是否应该被中断(中断标志是否为true)。它并不像stop方法那样会真的会粗暴地打断一个正在运行的线程。

  从Java的API中可以看到, 有许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法), 这些方法在抛出InterruptedException之前, Java虚拟机会先将该线程的中断标识位清除, 然后抛出InterruptedException, 此时调用isInterrupted()方法将会返回false。

  测试代码如下,首先创建了两个线程, SleepThread和BusyThread, 前者不停地睡眠, 后者一直运行, 然后对这两个线程分别进行中断操作, 观察二者的中断标识位。

public class Interrupted { public static void main(String[ ] args) throws Exception { // sleepThread不停的尝试睡眠 Thread sleepThread = new Thread(new SleepRunner() , "SleepThread"); sleepThread.setDaemon(true); // busyThread不停的运行 Thread busyThread = new Thread(new BusyRunner() , "BusyThread"); busyThread.setDaemon(true); sleepThread.start(); busyThread.start(); // 休眠5秒, 让sleepThread和busyThread充分运行 TimeUnit.SECONDS.sleep(5); sleepThread.interrupt(); busyThread.interrupt(); System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted()); System.out.println("BusyThread interrupted is " + busyThread.isInterrupted()); // 防止sleepThread和busyThread立刻退出 SleepUtils. second(2); } static class SleepRunner implements Runnable { @Override public void run() { while (true) { SleepUtils. second(10) ; } } } static class BusyRunner implements Runnable { @Override public void run() { while (true) { } } } }

  输出如下:

SleepThread interrupted is false BusyThread interrupted is true

  从结果可以看出, 抛出InterruptedException的线程SleepThread, 其中断标识位被清除了,而一直忙碌运作的线程BusyThread, 中断标识位没有被清除。

  中断状态是线程的一个标识位, 而中断操作是一种简便的线程间交互方式, 而这种交互方式最适合用来取消或停止任务。 除了中断以外, 还可以利用一个boolean共享变量来控制是否需要停止任务并终止该线程,这是最受推荐的终止一个线程(就是让一个线程彻底停止运行)的方式,使用共享变量(shared variable)来发出信号,告诉线程必须停止正在运行的任务。线程必须周期性的核查这一变量,然后有秩序地停止任务。测试代码如下:

public class Shutdown { public static void main(String[ ] args) throws Exception { Runner one = new Runner() ; Thread countThread = new Thread(one, "CountThread") ; countThread.start() ; // 睡眠1秒,main线程对Runner one进行中断, 使CountThread能够感知中断标识位的置位而结束 TimeUnit.SECONDS.sleep(1) ; countThread.interrupt() ; Runner two = new Runner() ; countThread = new Thread(two, "CountThread") ; countThread.start() ; // 睡眠1秒,main线程对Runner two进行取消, 使CountThread能够感知on为false而结束 TimeUnit.SECONDS.sleep(1) ; two.cancel() ; } private static class Runner implements Runnable { private long i; private volatile boolean on = true; @Override public void run() { while (on && ! Thread.currentThread().isInterrupted()) { i++; } System.out.println("Count i = " + i); } public void cancel() { on = false; } } }

  输出结果如下(多次运行结果可能不同):

Count i = 543487324 Count i = 540898082

  示例在执行过程中, main线程通过中断操作和cancel()方法均可使CountThread得以终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源, 而不是武断地将线程停止, 因此这种终止线程的做法显得更加安全和优雅。

8、join()方法、yield()方法和sleep()方法

  join()方法的作用:让“主线程”等待“子线程”结束之后再继续运行。这句话可能有点晦涩,我们还是通过例子去理解:

// 主线程 public class Father extends Thread { public void run() { Son s = new Son(); s.start(); s.join(); ... } } // 子线程 public class Son extends Thread { public void run() { ... } }

  上面的有两个类Father(主线程类)和Son(子线程类)。因为Son是在Father中创建并启动的,所以,Father是主线程类,Son是子线程类。在Father主线程中,通过new Son()新建一个“子线程s”。接着通过s.start()启动“子线程s”,并且调用s.join()。在调用s.join()之后,Father主线程会一直等待,直到“子线程s”运行完毕;在“子线程s”运行完毕之后,Father主线程才能接着运行。这也就是我们所说的join()的作用,让主线程等待,一直等到子线程结束之后,主线程才能继续运行。。

  sleep()、yield()、join()等是Thread类的方法(而wait()和notify()是Object类的方法)。yield()方法是停止当前线程,让同等优先权的线程运行。如果没有同等优先权的线程,那么yield()方法将不会起作用。

   sleep()使当前线程进入超时等待状态(见上面的状态转移图),所以执行sleep()的线程在指定的时间内肯定不会被执行;sleep()方法只让出了CPU,而并不会释放同步资源锁。

   sleep()方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield()方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。

   另外,sleep()方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep()方法,又没有受到 I\O 阻塞,那么,较低优先级的线程只能等待所有较高优先级的线程运行结束,才有机会运行。

 



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3