1 为什么要使用多线程
使用多线程的原因主要有以下几点:
(1)更有效的运用多核心处理器
一个线程在一个时刻只能运行在一个处理器核心上,所以单线程程序在同一时刻只能使用一个处理器核心,而多个线程在同一时刻可以使用多个处理器核心,显然能更加有效的运用多核心处理器。
(2)更快的响应时间
一些较为复杂的代码可以使用多线程技术,即将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),如生成订单快照、发送邮件等。这样做的好处是响应用户请求的线程能够尽可能快地处理完成,缩短了响应时间,提升了用户体验。
(3)更好的编程模型
Java为多线程编程提供了良好、考究并且一致的编程模型,使开发人员能够更加专注于问题的解决,即为所遇到的问题建立合适的模型,而不是绞尽脑汁地考虑如何将其多线程化。一旦开发人员建立好了模型,稍做修改总是能够方便地映射到Java提供的多线程编程模型上。
2 线程优先级
(1)通过一个整型成员变量priority来控制优先级,优先级高的线程分配时间片的数量要多于优先级低的线程。
(2)可以通过setPriority(int)方法来修改优先级,优先级的范围从1~10,默认优先级是5。
(3)设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。
(4)线程优先级不能作为程序正确性的依赖,有以下两点原因:1 在数量基数不大的情况下优先级的区分并不明显,甚至出现优先级低的线程先于优先级高的执行的情况。2 有些操作系统会忽略Java对线程优先级的设定,例如:Mac OS X 10.10,Java版本为1.7.0_71,环境下和Ubuntu 14.04环境下,所有Java线程优先级均为5,对线程优先级的设置会被忽略。
3 线程的状态
Java线程在运行的生命周期中可能处于下表所示的6种不同的状态,在给定的一个时刻,线程只能处于其中的一个状态。
Java线程状态变迁如下图示:
注意:(1)Java将操作系统中的运行和就绪两个状态合并称为运行状态。(2)阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。
4 Daemon线程
(1)Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。
(2)当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。
(3)可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。
注意: Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。
(4)Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块
并不一定会执行,所以不能依靠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块并没有执行。
5 构造线程
一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个能够运行的线程对象就初始化好了,在堆内存中等待着运行。java.lang.Thread中对线程进行初始化的部分的代码如下:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
// 当前线程就是该线程的父线程
Thread parent = currentThread();
this.group = g;
// 将daemon、priority属性设置为父线程的对应属性
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
this.name = name.toCharArray();
this.target = target;
setPriority(priority);
// 将父线程的InheritableThreadLocal复制过来
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal
.createInheritedMap(parent.inheritableThreadLocals);
// 分配一个线程ID
tid = nextThreadID();
}
6 启动线程
线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。
注意:启动一个线程前,最好为这个线程设置线程名称,因为这样在使用jstack分析程
序或者进行问题排查时,就会给开发人员提供一些提示,自定义的线程最好能够起个名字。