什么是线程
操作系统在运行一个应用程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。操作系统调度的最小单元就是线程,也可以说是轻量级进程 Light Weigth Process
,在一个进程中可以创建多个线程,这些线程都拥有自己的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程是在同时执行。
一个Java程序从 main()
方法开始执行,然后按照既定的代码逻辑执行,看似没有其它线程参与,但实际上Java程序天生就是多线程程序,因为执行 main()
方法的是一个名称为 main 的线程。下面使用JMX看下普通的Java程序包含哪些线程:
/**
* @auther: Zealon
*/
public class JMXTest {
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());
}
}
}
输出如下结果:
6:Monitor Ctrl-Break
5:Attach Listener
4:Signal Dispatcher
3:Finalizer
2:Reference Handler
1:main
可以看到。一个Java程序的运行不仅仅是main方法的运行,而实main线程和多个其它线程同时运行。
为什么要使用多线程
执行了一个简单的 “Hello,World”,却启动了那么多其它线程,是不是把简单的问题复杂化了?
当然不是,因为正确使用多线程,总是能够给开发人员带来显著的好处,而使用多线程的原因主要包括一下几点:
1.更多的处理器核心
随着处理器上的核心数量越来越多,以及超线程技术的广泛应用,现在大多数计算机都比以往更加擅长并行计算,而处理器性能的提升方式,也从更高的主频向更多的核心发展。如何利好处理器上的多个核心也成了现在的主要问题。
线程是大多数操作系统调度的基本单元,一个程序作为一个进程来运行,程序运行过程中能够创建多个线程,而一个线程在一个时刻只能运行在一个处理器核心上。试想一下,一个单线程程序在运行时只能使用一个处理器核心,那么再多的处理器核心加入也无法显著提升该应用程序的执行效率。
相反,如果该程序使用多线程技术,将计算机逻辑分配到多个处理器核心上,就会显著减少程序的处理时间,并且随着更多处理器核心的加入而变得更有效率。
2.更快的响应时间
有时候,我们编写一些较为复杂的代码(不是说算法的复杂,而而复杂的业务逻辑),例如一笔订单的创建它包括:插入订单数据、生成订单快照、发送邮件通知等。用户从点击“订购”按钮开始,就要等待这些操作全部完成才能看到订购成功的结果。但是这么多业务操作,如何能够让其更快完成呢?
在上面的场景中,可以使用多线程技术,将 数据一致性不强
的操作派发给其它线程处理(也可以使用消息中间件),如发邮件等。这样做的好处是响应用户请求的线程能够尽快执行完成,缩短了响应时间,提升了用户体验。
3.更好的编程模型
Java为多线程编程提供了良好、考究并且一致的编程模型,使开发人员能够更加专注于问题的解决,一旦开发人员建立好了模型,稍作修改总是能够方便地映射到Java提供的多线程编程模型上。
线程优先级
在Java线程中,通过一个整形成员变量 priority 来控制优先级,优先级的范围从 1 ~ 10,在线程构建的时候可以通过 setPriority(int) 方法来设置优先级,默认优先级是5,优先级越高的线程分配时间片的数量要多于优先级较低的线程。
设置优先级时,针对频繁阻塞(休眠或I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。
示例代码,定义3个线程,优先级分别为10、5、1,3个线程同时运行:
/**
* @auther: Zealon
*/
public class PriorityTest {
public static void main(String[] args) throws Exception{
Job job1 = new Job(10);
Thread t1 = new Thread(job1);
t1.setPriority(10);
t1.setName("优先级较高");
Job job2 = new Job(5);
Thread t2 = new Thread(job2);
t2.setPriority(5);
t2.setName("优先级平均");
Job job3 = new Job(1);
Thread t3 = new Thread(job3);
t3.setPriority(1);
t3.setName("优先级较低");
t3.start();
t2.start();
t1.start();
}
}
class Job implements Runnable{
private int prority;
public Job(int prority){
this.prority = prority;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+",prority:" + prority);
}
}
运行结果:
优先级较高,prority:10
优先级平均,prority:5
优先级较低,prority:1
输出结果可以看出,优先级高的先执行,越低的就靠后执行。
线程状态说明
Java线程在运行的声明周期中可能处于6种不同的状态,在给定的一个时刻,线程只能处于其中一种状态。
状态 | 说明 |
---|---|
NEW | 初始状态:线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态:Java线程将操作系统中的就绪和运行两种状态笼统地称作 “运行中” |
BLOCKED | 阻塞状态:表示线程阻塞于锁 |
WAITING | 等待状态:表示线程进入等待状态,进入该状态表示当前线程需要等待其它线程做出 一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态:该状态不同于WAITING ,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态:表示当前线程已经执行完毕 |
线程状态变迁图: 如图所示,线程创建之后,调用start()方法开始运行:
- 当线程执行wait()方法之后线程进入等待状态。
- 进入等待状态的线程需要依靠其它线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。
- 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入阻塞状态。
- 线程在执行Runnable的run()方法之后将会进入到终止状态。
守护线程(Daemon)
Daemon 线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程时,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。
⭐ 注意:Daemon属性需要在启动线程之前设置,不能在启动线程之后设置哦!
Daemon线程被用作完成支持性工作,但是在JVM退出时Daemon线程中的finally块并不一定会执行,示例如下代码:
public class Daemon {
public static void main(String[] args){
Thread t1 = new Thread(new DaemonRunner(),"DaemonRunner");
t1.setDaemon(true);
t1.start();
System.out.println("Daemon Thread run.");
}
static class DaemonRunner implements Runnable{
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("Daemon Thread finally run.");
}
}
}
}
运行Daemon程序,可以看到输出结果:
Daemon Thread run.
Process finished with exit code 0
Daemon Thread finally run.
并没有打印出来,main 线程在启动了线程 DaemonRunner 之后随着 main 方法执行完毕而终止,而此时Java虚拟机中已经没有非 Daemon 线程,迅即需要退出。Java虚拟机中的所有 Daemon 线程都需要立即终止,因此 DaemonRunner立即终止,但是 DaemonRunner 中的 finally 块并没有被执行。
🔑 所以,在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑!
作者: Zealon
崇尚简单,一切简单自然的事物都是美好的。