4.1 线程简介
4.1.1 什么是线程
现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。
一个Java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上Java程序天生就是多线程程序,因为执行main()方法的是一个名称为main的线程。下面使用JMX来查看一个普通的Java程序包含哪些线程,如代码清单4-1所示。
代码清单4-1 MultiThread.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()); } } }
输出如下所示(输出内容可能不同)。
[4] Signal Dispatcher // 分发处理发送给JVM信号的线程 [3] Finalizer // 调用对象finalize方法的线程 [2] Reference Handler // 清除Reference的线程 [1] main // main线程,用户程序入口
可以看到,一个Java程序的运行不仅仅是main()方法的运行,而是main线程和多个其他线程的同时运行。
4.1.2 为什么要使用多线程
执行一个简单的“Hello,World!”,却启动了那么多的“无关”线程,是不是把简单的问题复杂化了?当然不是,因为正确使用多线程,总是能够给开发人员带来显著的好处,而使用多线程的原因主要有以下几点。
(1)更多的处理器核心
随着处理器上的核心数量越来越多,以及超线程技术的广泛运用,现在大多数计算机都比以往更加擅长并行计算,而处理器性能的提升方式,也从更高的主频向更多的核心发展。如何利用好处理器上的多个核心也成了现在的主要问题。
线程是大多数操作系统调度的基本单元,一个程序作为一个进程来运行,程序运行过程中能够创建多个线程,而一个线程在一个时刻只能运行在一个处理器核心上。试想一下,一个单线程程序在运行时只能使用一个处理器核心,那么再多的处理器核心加入也无法显著提升该程序的执行效率。相反,如果该程序使用多线程技术,将计算逻辑分配到多个处理器核心上,就会显著减少程序的处理时间,并且随着更多处理器核心的加入而变得更有效率。
(2)更快的响应时间
有时我们会编写一些较为复杂的代码(这里的复杂不是说复杂的算法,而是复杂的业务逻辑),例如,一笔订单的创建,它包括插入订单数据、生成订单快照、发送邮件通知卖家和记录货品销售数量等。用户从单击“订购”按钮开始,就要等待这些操作全部完成才能看到订购成功的结果。但是这么多业务操作,如何能够让其更快地完成呢?
在上面的场景中,可以使用多线程技术,即将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),如生成订单快照、发送邮件等。这样做的好处是响应用户请求的线程能够尽可能快地处理完成,缩短了响应时间,提升了用户体验。
(3)更好的编程模型
Java为多线程编程提供了良好、考究并且一致的编程模型,使开发人员能够更加专注于问题的解决,即为所遇到的问题建立合适的模型,而不是绞尽脑汁地考虑如何将其多线程化。一旦开发人员建立好了模型,稍做修改总是能够方便地映射到Java提供的多线程编程模型上。
4.1.3 线程优先级
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。
在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定,示例如代码清单4-2所示。
代码清单4-2 Priority.java
public class Priority { private static volatile boolean notStart = true; private static volatile boolean notEnd = true; public static void main(String[] args) throws Exception { List<Job> jobs = new ArrayList<Job>(); 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
从输出可以看到线程优先级没有生效,优先级1和优先级10的Job计数的结果非常相近,没有明显差距。这表示程序正确性不能依赖线程的优先级高低。
注意 线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。笔者的环境为:Mac OS X 10.10,Java版本为1.7.0_71,经过笔者验证该环境下所有Java线程优先级均为5(通过jstack查看),对线程优先级的设置会被忽略。另外,尝试在Ubuntu 14.04环境下运行该示例,输出结果也表示该环境忽略了线程优先级的设置。
4.1.4 线程的状态
Java线程在运行的生命周期中可能处于表4-1所示的6种不同的状态,在给定的一个时刻,线程只能处于其中的一个状态。
表4-1 Java线程的状态
下面我们使用jstack工具(可以选择打开终端,键入jstack或者到JDK安装目录的bin目录下执行命令),尝试查看示例代码运行时的线程信息,更加深入地理解线程状态,示例如代码清单4-3所示。
代码清单4-3 ThreadState.java
public class ThreadState { 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(); } // 该线程不断地进行睡眠 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 { public void run() { synchronized (Blocked.class) { while (true) { SleepUtils.second(100); } } } } }
上述示例中使用的SleepUtils如代码清单4-4所示。
代码清单4-4 SleepUtils.java
public class SleepUtils { public static final void second(long seconds) { try { TimeUnit.SECONDS.sleep(seconds); } catch (InterruptedException e) { } } }
运行该示例,打开终端或者命令提示符,键入“jps”,输出如下。
611 935 Jps 929 ThreadState 270
可以看到运行示例对应的进程ID是929,接着再键入“jstack 929”(这里的进程ID需要和读者自己键入jps得出的ID一致),部分输出如下所示。
// BlockedThread-2线程阻塞在获取Blocked.class示例的锁上 "BlockedThread-2" prio=5 tid=0x00007feacb05d000 nid=0x5d03 waiting for monitor entry [0x000000010fd58000] java.lang.Thread.State: BLOCKED (on object monitor) // BlockedThread-1线程获取到了Blocked.class的锁 "BlockedThread-1" prio=5 tid=0x00007feacb05a000 nid=0x5b03 waiting on condition [0x000000010fc55000] java.lang.Thread.State: TIMED_WAITING (sleeping) // WaitingThread线程在Waiting实例上等待 "WaitingThread" prio=5 tid=0x00007feacb059800 nid=0x5903 in Object.wait() [0x000000010fb52000] java.lang.Thread.State: WAITING (on object monitor) // TimeWaitingThread线程处于超时等待 "TimeWaitingThread" prio=5 tid=0x00007feacb058800 nid=0x5703 waiting on condition [0x000000010fa4f000] java.lang.Thread.State: TIMED_WAITING (sleeping)
通过示例,我们了解到Java程序运行中线程状态的具体含义。线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java线程状态变迁如图4-1示。
图4-1 Java线程状态变迁
由图4-1中可以看到,线程创建之后,调用start()方法开始运行。当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行Runnable的run()方法之后将会进入到终止状态。
注意 Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。
4.1.5 Daemon线程
Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。
注意 Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。
Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行,示例如代码清单4-5所示。
代码清单4-5 Daemon.java
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块中的内容来确保执行关闭或清理资源的逻辑。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论