线程基础
1. 线程简介
1.1 什么是线程
现代操作系统调度的最小单元是线程,也叫轻量级进程,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。
一个 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());
}
}
}
输出如下:
[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分发处理给JVM信号的线程
[3] Finalizer //调用对象finalize方法的线程
[2] Reference Handler //清除reference线程
[1] main //main线程,程序入口
1.2 为什么使用多线程
- 充分利用现代计算机的多核处理器
- 更快的响应时间
- 更好的编程模型
其实第三点应该就回答了为什么上面一个单线程简单 Java 程序需要开这么多线程:多线程的引入,每个线程各司其职,最终这个程序运行的模型就简单有条理。假设一下所有这些逻辑都糅杂在一个线程里实现,那这代码该有多难看?
1.3 线程优先级
现代操作系统基本采用时分的形式调度运行的线程,线程分配到多少时间片就决定了线程使用的处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。
但是,线程优先级并不能作为程序正确的依赖,因为操作系统可以完全不理会 Java 线程对于优先级的设定。
1.4 线程的状态
Java 线程状态的变迁从图中可以看到,Java 将操作系统中运行和就绪两个状态合并为运行状态 (RUNNNABLE) 。阻塞状态是线程阻塞在进入 synchronized 关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在 java.concurrent 包中 Lock 接口的线程状态确是等待状态,因为 java.concurrent 包中的 Lock 接口对于阻塞的实现均使用了 LockSupport 类中的相关方法。
通过 jps
和 jstack
两个命令我们可以查看到 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 {
@Override
public void run() {
synchronized (Blocked.class){
while (true){
SleepUtils.second(100);
}
}
}
}
}
public class SleepUtils {
public static final void second(long seconds){
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
jstack 部分结果:
PS C:\Users\Administrator> jstack 18896
2018-12-15 15:40:36
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.111-b14 mixed mode):
"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x0000000003382800 nid=0x46dc waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"BlockedThread-2" #15 prio=5 os_prio=0 tid=0x000000001ef2b800 nid=0x18fc waiting for monitor entry [0x00000000206af000]
java.lang.Thread.State: BLOCKED (on object monitor)
"BlockedThread-1" #14 prio=5 os_prio=0 tid=0x000000001ef27000 nid=0x4598 waiting on condition [0x00000000205ae000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
"WaitingThread" #13 prio=5 os_prio=0 tid=0x000000001ef26000 nid=0x18d4 in Object.wait() [0x00000000204af000]
java.lang.Thread.State: WAITING (on object monitor)
"TimeWaitingThread" #12 prio=5 os_prio=0 tid=0x000000001ef24800 nid=0x1a60 waiting on condition [0x00000000203ae000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
1.5 Daemon 线程
Daemon 线程翻译成中文就是守护线程,用 jstack
命令可以看到一个普通 Java 程序都带有守护线程,比如
"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001ee0c800 nid=0x3214 runnable [0x000000001fcae000]
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001edcc000 nid=0x2d7c waiting on condition [0x0000000000000000]
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001ed7c000 nid=0x47e4 runnable [0x0000000000000000]
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001ed60800 nid=0x3618 in Object.wait() [0x000000001f23f000]
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000003478800 nid=0x3154 in Object.wait() [0x000000001ed3f000]
对于守护线程的使用,有一点要注意的是,不要依赖守护线程中的 finally 块来关闭或者清理资源,因为只剩下守护线程时,程序会直接终止,而不会继续执行 finally 中的语句。
2. 启动和终止线程
2.1 构造线程
线程不是凭空 new 出来的,它也需要要初始化 (init)。初始化线程是为线程提供所需要属性,如线程所属的线程组、线程优先级、是否是 Deamon 线程等信息。初始化线程的过程中,子线程的空间分配参考父线程父线程,如子线程继承了父线程是否为 Deamon、优先级和加载资源的 contextClassLoader 以及可继承的 ThreadLocal,同时还会分配一个唯一的 ID 来标识这个子线程。
2.2 启动线程
启动线程时,最好给线程设置一个有意义的名字,这样通过 jstack 等工具排查问题时会给开发人员提供一些提示。
2.3 理解中断
中断是线程的一个标志位。其他线程调用某个线程的 interrupt() 方法就好比给该线程打了个招呼,希望该线程进行中断。
需要被中断的线程,通过判断中断标志来进行中断响应,具体方式是调用 isInterrupted() 来进行判断是否被中断,也可以通过 interrupted() 方法来进行判断中断,不过这个方法的副作用是会复位中断标志位。
另外,即使线程被中断过,调用该线程对象的 isInterrupted() 方法仍会返回 false,因为从 Java API 中可以看到,许多声明抛出 InterruptedException 的方法(例如 sleep),在抛出中断异常前,会先复位中断标志位。
运行下面的例子:
public class Interrupted {
public static void main(String[] args) throws InterruptedException {
// 不停尝试睡眠
Thread sleepThread = new Thread(new SleepRunner(), "sleepThread");
sleepThread.setDaemon(true);
// busyThread 不停地运行
Thread busyRunner = new Thread(new BusyRunner(), "busyRunner");
busyRunner.setDaemon(true);
sleepThread.start();
busyRunner.start();
// 休眠 5 秒,让 sleepThread 和 busyThread 充分运行
TimeUnit.SECONDS.sleep(5);
sleepThread.interrupt();
busyRunner.interrupt();
System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
System.out.println("BusyThread interrupted is " + busyRunner.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){
}
}
}
}
得到结果
java.lang.InterruptedException: sleep interrupted
SleepThread interrupted is false
at java.lang.Thread.sleep(Native Method)
BusyThread interrupted is true
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at SleepUtils.second(SleepUtils.java:12)
at Interrupted$SleepRunner.run(Interrupted.java:32)
at java.lang.Thread.run(Thread.java:745)
可以看到,sleep 方法在抛出 InterruptedException 异常前,已经把中断标记复位了。
2.4 过期的 suspend()、resume() 和 stop()
这三个方法都被标记为不建议使用,原因是不安全、不能保证释放资源。
suspend() 方法在进入睡眠时,不会释放已经占有的资源(比如锁),这样容易引发死锁。stop() 在终结一个线程时,不会保证线程资源的正常释放,导致程序容易陷入不确定的状态。
对于 suspend-resume 可以使用 等待/通知 机制来替代。
2.5 安全地终止线程
可以模仿 interrupt 的方式,手动实现一个变量来控制是否需要停止任务并终止该线程。通过让线程自己终止,而给线程充分的时间去销毁、释放资源。
3. 线程间通讯
3.1 volatile 和 synchronized 关键字
Java 支持多个线程同时访问一个对象或者对象的成员变量,这些成员变量分配的内存是在共享内存中,但是每个线程可以拥有自己的一份独立的拷贝,目的是加速程序的执行,所以,在程序的执行过程中,一个线程看到变量不一定是最新的。
使用 volatile 或 synchronized 可以让线程看到最新的变量,而不会读取到旧值。具体实现方法是:
- volatile
volatile 关键字的一个作用就是,类似于给成员变量加上了一个读写锁,写入新值,通知其他线程对该变量的缓存失效,达到刷新缓存的效果。
但是过多的使用 volatile 是不必要的,会降低程序的执行效率。
- synchronized:
synchronized 有两种使用方式
- 同步块
- 修饰方法
它的实现类似于操作系统中的管程,保证同一时刻,只有一个线程处于方法或者同步块中,从而保证了同一时刻只有一个线程在同步块或方法中(临界区),但是多了一个功能:其他线程拿到锁后会刷新缓存,从而保证了线程对变量访问的可见性和排他性。
下面通过一段简单的代码,用 javap
命令看看 synchronized 的在 .class 文件中是如何实现的。
public class Synchronized {
public static void main(String[] args) {
// 对 Synchronized Class 对象进行加锁
synchronized (Synchronized.class){
}
// 静态同步方法,对 Synchronized Class 对象加锁
m();
}
public static synchronized void m(){
}
}
执行 javap -v
命令后,部分输出如下:
public static void main(java.lang.String[]);
0: ldc #2 // class Synchronized
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
15: invokestatic #3 // Method m:()V
18: return
public static synchronized void m();
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
}
可以看出,同步块是使用 monitorenter 和 monitorexit 指令实现的,同步方法是通过方法上的修饰符 ACC_SYNCHRONIZED 完成的。这两种方式本质都是进行排他性的获取对象的监视器。
3.2 等待/通知机制
等待/通知机制,是指一个线程 A 调用了对象 O 的 wait() 方法进入等待状态,而另一个线程调 B 调用了对象 O 的 notify() 或者 notifyAll() 方法,线程 A 收到通知后从对象 O 的 wait() 方法返回,进而执行后续操作。
Java 内置的等待/通知的相关方法
方法名称 | 描述 |
---|---|
notify() | 通知一个在对象上等待的线程,使其从 wait() 方法返回,而返回的前提是该线程获取到了对象的锁 |
notifyAll() | 通知所有等待在该对象上的线程 |
wait() | 调用该方法的线程进入 WAITING 状态,只有等待另外的线程的通知或者被中断才会返回,需要注意,调用 wait() 方法后,会释放对象的锁 |
wait(long) | 超时等待一段时间,这里的参数时间是毫秒,也就是长达 n 毫秒,如果没有通知就超时返回 |
wait(long,int) | 对于超时时间更细粒度的控制,可以达到纳秒 |
一个样例
public class WaitNotify {
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread waitThread = new Thread(new Wait(), "WaitThread");
waitThread.start();
TimeUnit.SECONDS.sleep(1);
Thread notifyThread = new Thread(new Notify(), "NotifyThread");
notifyThread.start();
}
static class Wait implements Runnable {
@Override
public void run() {
// 加锁,拥有 lock 的 Monitor
synchronized (lock) {
// 当条件不满足时,继续 wait,同时释放了 lock 的锁
while (flag){
try {
System.out.println(Thread.currentThread() + " flag is true. wait @ " +
new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 条件满足时,完成工作
System.out.println(Thread.currentThread() + " flag is false. running @ " +
new SimpleDateFormat("HH:mm:ss").format(new Date()));
}
}
}
}
static class Notify implements Runnable {
@Override
public void run() {
synchronized (lock){
// 加锁,拥有 lock 的Monitor
// 直到当前线程释放了 lock 后,WaitThread 才能从 wait 方法中返回
System.out.println(Thread.currentThread() + " hold lock. notify @ " +
new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.notifyAll();
flag = false;
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 再次加锁
synchronized (lock){
System.out.println(Thread.currentThread() + " hold lock again. sleep @ " +
new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
输出如下:
Thread[WaitThread,5,main] flag is true. wait @ 10:48:59
Thread[NotifyThread,5,main] hold lock. notify @ 10:49:00
Thread[WaitThread,5,main] flag is false. running @ 10:49:05
Thread[NotifyThread,5,main] hold lock again. sleep @ 10:49:05
可以看到, wait() 方法会释放线程持有的对象锁。从 wait() 方法返回后,并不具有获取锁的更高优先级,仍是跟其他线程一起公平竞争对象的锁。
3.3 等待/通知的经典范式
范式分为两部分,分别针对等待方(消费者) 和通知方 (生产者)。
等待方遵循如下原则:
- 获取对象的锁。
- 如果条件不满足,那么调用对象的 wait() 方法,被通知后仍要检查条件。
- 条件满足则执行对应的逻辑。
对应的伪代码如下:
synchronized(对象){
while (条件不满足){
对象.wait();
}
对应的处理逻辑
}
通知方遵循如下原则
- 获得对象的锁
- 改变条件
- 通知所有等待在对象上的线程。
对应的伪代码如下:
synchronized (对象){
改变条件
对象.notifyAll();
}
3.4 Thread.join() 的使用
如果一个线程 A 执行了 Thread.join() 语句,其含义是:当前线程 A 等待 thread 线程终止后才从 thread.join() 返回。
Thread.join() 的返回涉及到等待/通知机制。下面是 JDK 中的 Thread.join() 方法的源码(进行了部分调整)
// 加锁当前线程对象
public final synchronized void join() throws InterruptedException {
// 条件不满足,继续等待
while (isAlive()){
wait(0);
}
// 条件符合,方法返回
}
当线程终止时,会调用线程自身的 notifyAll() 方法,会通知所有等待在该线程对象上的线程。可以看到 join() 方法的逻辑结构于 3.3 中的等待/通知经典范式一致,即加锁、循环和处理逻辑 3 个步骤。