文章目录
- 4.1 线程简介
- 4.1.1 什么是线程
- 4.1.2 为什么要使用多线程
- 4.1.3 线程优先级
- 4.1.4 线程的状态
- 4.1.5 Daemon 线程
Java从诞生开始就明智地选择了内置对多线程的支持,这使得Java语言相比同一时期的其他语言具有明显的优势。线程作为操作系统调度的最小单元,多个线程能够同时执行这将显著提升程序性能,在多核环境中表现得更加明显。但是,过多地创建线程和对线程的不当管理也容易造成问题。本章将着重介绍Java并发编程的基础知识,从启动一个线程到线程间不同的通信方式,最后通过简单的线程池示例以及应用(简单的Web服务器)来串联本章所介绍的内容。
4.1 线程简介
4.1.1 什么是线程
现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。
一个Java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上Java程序天生就是多线程程序,因为执行main0方法的是一个名称为main的线程。可以使用JMX来查看一个普通的Java程序包含哪些线程。示例代码如下:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
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());
}
}
}
Java Management Extensions(JMX)是一种Java标准,用于管理和监控Java应用程序,特别是分布式系统。它提供了一种标准化的方式来管理应用程序的各种方面,包括性能监控、配置更改、事件通知等。目前JMX最常用的就是用来做JAVA程序的监控,市面上常见的Java 监控框架基本都是基于JMX来实现的。
输出如下所示(输出内容可能不同)。
[6] Monitor Ctrl-Break
[5] Attach Listener
[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,优先级高的线程分配时间片的数量要多于优先级低的线程。设置线程优先级时,针对频繁阻塞(休眠或者IO操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。示例代码如下:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
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<>();
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 : 4701096
Job Priority : 1, Count : 4519326
Job Priority : 1, Count : 5447983
Job Priority : 1, Count : 5545905
Job Priority : 1, Count : 4658417
Job Priority : 10, Count : 5173026
Job Priority : 10, Count : 6420976
Job Priority : 10, Count : 6464159
Job Priority : 10, Count : 5162462
Job Priority : 10, Count : 5045491
Thread.yield()
方法在Java中是用来使得当前正在执行的线程暂停,并允许其他线程有机会运行。这并不意味着线程终止或进入等待状态,而只是放弃了当前的时间片,让CPU可以调度其他同优先级的线程来运行。
这个方法主要用于线程间的轻微协调,比如在一个线程中频繁检查某个条件是否满足,但又不希望一直占用CPU资源,这时可以让线程调用yield()
来让出CPU给其他线程,一旦条件满足或者线程调度器重新选择该线程,它将再次开始执行。
需要注意的是,yield()
只会让出同优先级的线程执行,如果当前没有同优先级的可运行线程,那么当前线程会立即重新获得CPU控制权并继续执行。
从输出可以看到线程优先级没有生效,优先级1和优先级10的Job计数的结果非常相近,没有明显差距。这表示程序正确性不能依赖线程的优先级高低。
注意:线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。书本作者环境是mac,java版本是1.7,所有 Java线程优先级均为5(通过jstack查看),对线程优先级的设置会被忽略。另外,尝试在 Ubuntu 14.04环境下运行该示例,输出结果也表示该环增忽略了线程优先级的设置。
4.1.4 线程的状态
Java线程在运行的生命周期中可能处于下表所示的6种不同的状态,在给定的一个时刻,线程只能处于其中的一个状态。
java线程的状态:
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <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>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <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,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态宠统地称作“运行中” |
BLOCKED | 阻塞状态,表示线程阻于锁 |
WAITING | 等待状态,表示线程进人等待状态,进人该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
TIMED_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
下面我们使用jstack工具(可以选择打开终端,键人jstack或者到JDK安装目录的bin目录下执行命令),尝试查看示例代码运行时的线程信息,更加深人地理解线程状态,示例代码如下:
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 {
@Override
public void run() {
synchronized (Blocked.class) {
while (true) {
SleepUtils.second(100);
}
}
}
}
}
import java.util.concurrent.TimeUnit;
public class SleepUtils {
public static final void second(long seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行ThreadState类。
打开cmd,使用jps得到进程号
D:\>jps
14372
14900 Jps
11292 ThreadState
6044 Launcher
使用jstack 进程id查看堆栈信息
D:\>jstack 11292
2024-07-03 00:41:57
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.271-b09 mixed mode):
"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x000002b0fafdc000 nid=0x3f70 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"BlockedThread-2" #15 prio=5 os_prio=0 tid=0x000002b09304c000 nid=0x3078 waiting on condition [0x000000e66a4ff000]
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 com.xin.demo.threaddemo.bookdemo.SleepUtils.second(SleepUtils.java:8)
at com.xin.demo.threaddemo.bookdemo.ThreadState$Blocked.run(ThreadState.java:45)
- locked <0x00000000db721998> (a java.lang.Class for com.xin.demo.threaddemo.bookdemo.ThreadState$Blocked)
at java.lang.Thread.run(Thread.java:748)
"BlockedThread-1" #14 prio=5 os_prio=0 tid=0x000002b093061800 nid=0x27b8 waiting for monitor entry [0x000000e66a3ff000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.xin.demo.threaddemo.bookdemo.ThreadState$Blocked.run(ThreadState.java:45)
- waiting to lock <0x00000000db721998> (a java.lang.Class for com.xin.demo.threaddemo.bookdemo.ThreadState$Blocked)
at java.lang.Thread.run(Thread.java:748)
"WaitingThread" #13 prio=5 os_prio=0 tid=0x000002b09305a800 nid=0x94c in Object.wait() [0x000000e66a2ff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000db71e758> (a java.lang.Class for com.xin.demo.threaddemo.bookdemo.ThreadState$Waiting)
at java.lang.Object.wait(Object.java:502)
at com.xin.demo.threaddemo.bookdemo.ThreadState$Waiting.run(ThreadState.java:30)
- locked <0x00000000db71e758> (a java.lang.Class for com.xin.demo.threaddemo.bookdemo.ThreadState$Waiting)
at java.lang.Thread.run(Thread.java:748)
"TimeWaitingThread" #12 prio=5 os_prio=0 tid=0x000002b093021800 nid=0x2cd4 waiting on condition [0x000000e66a1ff000]
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 com.xin.demo.threaddemo.bookdemo.SleepUtils.second(SleepUtils.java:8)
at com.xin.demo.threaddemo.bookdemo.ThreadState$TimeWaiting.run(ThreadState.java:18)
at java.lang.Thread.run(Thread.java:748)
"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x000002b092ed2800 nid=0x3228 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x000002b092e12000 nid=0x3ca4 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000002b092e09000 nid=0x638 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000002b092e01800 nid=0x409c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000002b092dfc000 nid=0x283c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000002b092df7000 nid=0x2ff8 runnable [0x000000e669afe000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x00000000db6afad8> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x00000000db6afad8> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:53)
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000002b092864800 nid=0x2d84 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000002b0fe4ae000 nid=0x718 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000002b0fb07b800 nid=0x17e0 in Object.wait() [0x000000e6697fe000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000db308ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x00000000db308ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000002b0fb075000 nid=0x10fc in Object.wait() [0x000000e6696fe000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000db306c00> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x00000000db306c00> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=2 tid=0x000002b0fe400800 nid=0x3730 runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x000002b0faff5800 nid=0x1c04 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x000002b0faff8000 nid=0xf94 runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000002b0faff9000 nid=0x2ba0 runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x000002b0faffa800 nid=0x3fdc runnable
"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x000002b0faffc800 nid=0x3fb0 runnable
"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x000002b0faffd800 nid=0x19e4 runnable
"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x000002b0fb000800 nid=0xebc runnable
"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x000002b0fb001800 nid=0x3698 runnable
"VM Periodic Task Thread" os_prio=2 tid=0x000002b092e35800 nid=0x127c waiting on condition
JNI global references: 12
通过示例,我们了解到Java程序运行中线程状态的具体含义。线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java线程状态变迁如下图所示:
由上图中可以看到,线程创建之后,调用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块并不一定会执行,代码示例如下:
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立即终止,但是DeamonRunner中的在finally块并改有执行。
注意:在构建 Daemon 线程时,不能依靠finally块中的内容来确保执行关闲或清理资源的逻辑。