Java 并发
进程&线程:一个 Java 程序的运行是 main 线程和多个其他线程同时运行
进程:程序的一次执行过程,是系统运行程序的基本单位
进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
线程:比进程更小的执行单位
一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
Java创建线程的方法
多线程入门:Java创建线程有很多种方式啊,像实现Runnable、Callable接口、继承Thread类、创建线程池等等,不过这些方式并没有真正创建出线程,严格来说,Java就只有一种方式可以创建线程,那就是通过new Thread().start()创建。而所谓的Runnable、Callable……(Callable实际上就是Runnable的封装体)对象,这仅仅只是线程体(线程是执行线程体的容器,线程体是一个可运行的任务),也就是提供给线程执行的任务,并不属于真正的Java线程,它们的执行,最终还是需要依赖于new Thread()……
其他方式,要么是在封装Thread.start(),要么是在创建线程体,而这个所谓的线程体,更接地气的说,应该是“多线程任务”。大家都说Java有三种创建线程的方式!并发编程中的惊天骗局!
一个更直观的比喻,想象一个餐厅厨房:
- 线程 就是厨房里的 厨师。厨师拥有灶台、刀具等资源,是一个“执行者”。
- 线程体 就是 菜谱上的一道菜的具体做法(比如“鱼香肉丝”的步骤)。它定义了要切什么菜、放什么调料、炒多久。
- 你可以招聘一个 厨师(创建线程),让他去执行 “鱼香肉丝”的做法(线程体)。
- 你甚至可以让 三个厨师(三个线程),同时去执行 同一道“鱼香肉丝”的做法(同一个线程体),做给不同的客人。
线程的生命周期和状态
操作系统的线程主要有以下三个状态:
在操作系统中,线程被视为轻量级的进程,所以线程状态其实和进程状态是一致的。

- 就绪状态(ready):线程正在等待使用 CPU,经调度程序调用之后进入 running 状态。
- 执行状态(running):线程正在使用 CPU。
- 等待状态(waiting): 线程经过等待事件的调用或者正在等待其他资源(如 I/O)。
Java 线程只可能处于下面 6 种不同状态的其中一个状态:
// Thread.State 源码
public enum State {
NEW,//初始状态,线程被创建出来但没有被调用 start()
RUNNABLE,//运行状态,线程被调用了 start() 等待运行的状态
BLOCKED,//阻塞状态,需要等待锁释放
WAITING,//等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)
TIMED_WAITING,//超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待
TERMINATED;//终止状态,表示该线程已经运行完毕
}
NEW
处于 NEW 状态的线程此时尚未启动。这里的尚未启动指的是还没调用 Thread 实例的start()方法。
private void testStateNew() {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState()); // 输出 NEW
}
从上面可以看出,只是创建了线程而并没有调用 start 方法,此时线程处于 NEW 状态。
关于 start 的两个引申问题
- 反复调用同一个线程的 start 方法是否可行?
- 假如一个线程执行完毕(此时处于 TERMINATED 状态),再次调用这个线程的 start 方法是否可行?
要分析这两个问题,我们先来看看
start()的源码:
// 使用synchronized关键字保证这个方法是线程安全的
public synchronized void start() {
// threadStatus != 0 表示这个线程已经被启动过或已经结束了
// 如果试图再次启动这个线程,就会抛出IllegalThreadStateException异常
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 将这个线程添加到当前线程的线程组中
group.add(this);
// 声明一个变量,用于记录线程是否启动成功
boolean started = false;
try {
// 使用native方法启动这个线程
start0();
// 如果没有抛出异常,那么started被设为true,表示线程启动成功
started = true;
} finally {
// 在finally语句块中,无论try语句块中的代码是否抛出异常,都会执行
try {
// 如果线程没有启动成功,就从线程组中移除这个线程
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
// 如果在移除线程的过程中发生了异常,我们选择忽略这个异常
}
}
}
可以看到,在start()内部,有一个 threadStatus 变量。如果它不等于 0,调用start()会直接抛出异常。
接着往下看,有一个 native 的 start0() 方法。这个方法并没有对threadStatus进行处理。到这里我们仿佛拿这个 threadStatus 没辙了,通过 debug 再看一下:
@Test
public void testStartMethod() {
Thread thread = new Thread(() -> {});
thread.start(); // 第一次调用
thread.start(); // 第二次调用
}
在 start 方法内部的最开始打断点:
- 第一次调用时 threadStatus 的值是 0。
- 第二次调用时 threadStatus 的值不为 0。
查看当前线程状态的源码:
// Thread.getState方法源码:
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
// sun.misc.VM 源码:
// 如果线程的状态值和4做位与操作结果不为0,线程处于RUNNABLE状态。
// 如果线程的状态值和1024做位与操作结果不为0,线程处于BLOCKED状态。
// 如果线程的状态值和16做位与操作结果不为0,线程处于WAITING状态。
// 如果线程的状态值和32做位与操作结果不为0,线程处于TIMED_WAITING状态。
// 如果线程的状态值和2做位与操作结果不为0,线程处于TERMINATED状态。
// 最后,如果线程的状态值和1做位与操作结果为0,线程处于NEW状态,否则线程处于RUNNABLE状态。
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}
还记得我们引申的两个问题吗?
- 反复调用同一个线程的 start 方法是否可行?
- 假如一个线程执行完毕(此时处于 TERMINATED 状态),再次调用这个线程的 start 方法是否可行?
结合上面的源码可以得到的答案是:
- 都不行,在调用 start 之后,threadStatus 的值会改变(
threadStatus !=0),再次调用 start 方法会抛出 IllegalThreadStateException 异常。 - threadStatus 为 2 代表当前线程状态为TERMINATED。
RUNNABLE
表示当前线程正在运行中。处于 RUNNABLE 状态的线程在 Java 虚拟机中运行,也有可能在等待 CPU 分配资源。 我们来看看 Thread 源码里对 RUNNABLE 状态的定义:
/**
* 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.
*/
也就是说,Java 线程的RUNNABLE状态其实包括了操作系统线程的ready和running两个状态。
BLOCKED
阻塞状态。处于 BLOCKED 状态的线程正等待锁的释放以进入同步区。 我们用 BLOCKED 状态举个生活中的例子: 假如今天你下班后准备去食堂吃饭。你来到食堂仅有的一个窗口,发现前面已经有个人在窗口前了,此时你必须得等前面的人从窗口离开才行。 假设你是线程 t2,你前面的那个人是线程 t1。此时 t1 占有了锁(食堂唯一的窗口),t2 正在等待锁的释放,所以此时 t2 就处于 BLOCKED 状态。
WAITING
等待状态。处于等待状态的线程变成 RUNNABLE 状态需要其他线程唤醒。
调用下面这 3 个方法会使线程进入等待状态:
Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;Thread.join():等待线程执行完毕,底层调用的是 Object 的 wait 方法;LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。LockSupport
我们延续上面的例子继续解释一下 WAITING 状态:
你(消费者)走到窗口,发现今天没有菜(队列空)。你跟阿姨说:“那我先等着,等菜来了再叫我。” 然后你主动走到旁边(wait(),释放窗口锁)。后厨(生产者)做好菜后,阿姨喊你(notify()),你回来继续排队打饭。当你(t2)调用 wait() 释放窗口锁后,后面排队的人(比如 t3)就能获得锁,继续打饭。 详细过程:
假设窗口前排队顺序是:t2(你)、t3(后面的人)、t4...
1. t2 拿到锁,正在打饭。
2. t2 发现没菜,主动调用 wait() → 释放锁,进入 WAITING 状态,站到旁边(不再占用窗口)。
3. 此时窗口空出来了,锁被释放。
4. 排在后面的 t3(BLOCKED 状态)立刻竞争到锁,变成 RUNNABLE,开始打饭。
5. t3 打完饭释放锁,t4 接着打…… 完全不受你影响。
6. 直到某时刻,有人(生产者)调用了 notify() 或 notifyAll(),你(t2)被唤醒。
- 但注意:唤醒后你并不立即打饭,而是进入 BLOCKED 状态,重新排队。
- 等窗口再次空出来,你才能重新获得锁,从 wait() 之后继续执行。
再来一个例子–生产者-消费者队列 假设有一个任务队列(比如快递柜):
- 消费者线程(取快递的人):如果柜子是空的,他就得等。
- 生产者线程(放快递的人):往柜子里放一个快递,然后通知等待的消费者。 但是要注意:
wait()的语义是“释放锁并等待”,被唤醒后必须重新获取锁,才能安全地继续访问共享数据。- 不一定谁先执行,取决于 JVM 的线程调度策略(公平锁与非公平锁),t2 和 t4 都有可能先执行。
TIMED_WAITING
超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。 调用如下方法会使线程进入超时等待状态:
Thread.sleep(long millis):使当前线程睡眠指定时间;Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;Thread.join(long millis):等待当前线程最多执行 millis 毫秒,如果 millis 为 0,则会一直执行;LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;LockSupportLockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;
我们继续延续上面的例子来解释一下 TIMED_WAITING 状态: 到了第二天中午,又到了饭点,你还是到了窗口前。 突然间想起你的同事叫你等他一起,他说让你等他十分钟他改个 bug。 好吧,那就等等吧,你就离开了窗口。很快十分钟过去了,你见他还没来,你想都等了这么久了还不来,那你还是先去吃饭好了。 这时你还是线程 t1,你改 bug 的同事是线程 t2。t2 让 t1 等待了指定时间,此时 t1 等待期间就属于 TIMED_WATING 状态。 t1 等待 10 分钟后,就自动唤醒,拥有了去争夺锁的资格。
TERMINATED
终止状态。此时线程已执行完毕。
线程状态的转换
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。

BLOCKED 与 RUNNABLE 状态的转换
我们在上面说过:处于 BLOCKED 状态的线程在等待锁的释放。假如这里有两个线程 a 和 b,a 线程提前获得了锁并暂未释放锁,此时 b 就处于 BLOCKED 状态。我们来看一个例子:
@Test
public void blockedTest() {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "a");
Thread b = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "b");
a.start();
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出?
System.out.println(b.getName() + ":" + b.getState()); // 输出?
}
// 同步方法争夺锁
private synchronized void testMethod() {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
初看之下,大家可能会觉得线程 a 会先调用同步方法,同步方法内又调用了Thread.sleep()方法,必然会输出 TIMED_WAITING,而线程 b 因为等待线程 a 释放锁所以必然会输出 BLOCKED。
其实不然,有两点需要值得大家注意:
- 一是在测试方法
blockedTest()内还有一个 main 线程 - 二是启动线程后执行 run 方法还是需要消耗一定时间的。 测试方法的 main 线程只保证了 a,b 两个线程调用 start 方法(转化为 RUNNABLE 状态),如果 CPU 执行效率高一点,还没等两个线程真正开始争夺锁,就已经打印此时两个线程的状态(RUNNABLE)了。 当然,如果 CPU 执行效率低一点,其中某个线程也是可能打印出 BLOCKED 状态的(此时两个线程已经开始争夺锁了)。 下面是我执行了几次的结果对比:

这时你可能又会问了,要是我想要打印出 BLOCKED 状态我该怎么处理呢?
BLOCKED 状态的产生需要两个线程争夺锁才行。那我们处理下测试方法里的 main 线程就可以了,让它“休息一会儿”,调用一下Thread.sleep()方法。
这里需要注意的是 main 线程休息的时间,要保证在线程争夺锁的时间内,不要等到前一个线程锁都释放了你再去争夺锁,此时还是得不到 BLOCKED 状态的。
我们把上面的测试方法 blockedTest 改动一下:
public void blockedTest() throws InterruptedException {
······
a.start();
Thread.sleep(1000L); // 需要注意这里main线程休眠了1000毫秒,而testMethod()里休眠了2000毫秒
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出?
System.out.println(b.getName() + ":" + b.getState()); // 输出?
}
运行结果如下所示:

在这个例子中两个线程的状态转换如下
- a 的状态转换过程:RUNNABLE(
a.start()) -> TIMED_WATING(Thread.sleep())->RUNABLE(sleep()时间到)->BLOCKED(未抢到锁) -> TERMINATED - b 的状态转换过程:RUNNABLE(
b.start()) -> BLOCKED(未抢到锁) ->TERMINATED
斜体表示可能出现的状态, 大家可以在自己的电脑上多试几次看看输出。同样,这里的输出也可能有多钟结果。
WAITING 状态与 RUNNABLE 状态的转换
根据转换图我们知道有 3 个方法可以使线程从 RUNNABLE 状态转为 WAITING 状态。我们主要介绍下Object.wait() 和Thread.join() 。
Object.wait()
调用wait()方法前线程必须持有对象的锁。
线程调用wait()方法时,会释放当前的锁,直到有其他线程调用notify()/notifyAll()方法唤醒等待锁的线程。
需要注意的是,其他线程调用notify()方法只会唤醒单个等待锁的线程,如有有多个线程都在等待这个锁的话不一定会唤醒到之前调用wait()方法的线程。
同样,调用notifyAll()方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。
Thread.join()
调用join()方法,会一直等待这个线程执行完毕(转换为 TERMINATED 状态)。
我们再把上面的例子线程启动那里改变一下:
public void blockedTest() {
······
a.start();
a.join();
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出 TERMINATED
System.out.println(b.getName() + ":" + b.getState());
}
要是没有调用 join 方法,main 线程不管 a 线程是否执行完毕都会继续往下走。 a 线程启动之后马上调用了 join 方法,这里 main 线程就会等到 a 线程执行完毕,所以这里 a 线程打印的状态固定是TERMINATED。 至于 b 线程的状态,有可能打印 RUNNABLE(尚未进入同步方法),也有可能打印 TIMED_WAITING(进入了同步方法)。
TIMED_WAITING 与 RUNNABLE 状态转换
TIMED_WAITING 与 WAITING 状态类似,只是 TIMED_WAITING 状态等待的时间是指定的。
Thread.sleep(long)
使当前线程睡眠指定时间。需要注意这里的“睡眠”只是暂时使线程停止执行,并不会释放锁。时间到后,线程会重新进入 RUNNABLE 状态。
Object.wait(long)
wait(long)方法使线程进入 TIMED_WAITING 状态。这里的wait(long)方法与无参方法 wait()相同的地方是,都可以通过其他线程调用notify()或notifyAll()方法来唤醒。
不同的地方是,有参方法wait(long)就算其他线程不来唤醒它,经过指定时间 long 之后它会自动唤醒,拥有去争夺锁的资格。
Thread.join(long)
join(long)使当前线程执行指定时间,并且使线程进入 TIMED_WAITING 状态。
我们再来改一改刚才的示例:
public void blockedTest() {
······
a.start();
a.join(1000L);
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出 TIEMD_WAITING
System.out.println(b.getName() + ":" + b.getState());
}
这里调用a.join(1000L),因为是指定了具体 a 线程执行的时间的,并且执行时间是小于 a 线程 sleep 的时间,所以 a 线程状态输出 TIMED_WAITING。
b 线程状态仍然不固定(RUNNABLE 或 BLOCKED)。
线程中断
在某些情况下,我们在线程启动后发现并不需要它继续执行下去时,需要中断线程。目前在 Java 里还没有安全方法来直接停止线程,但是 Java 提供了线程中断机制来处理需要中断线程的情况。 线程中断机制是一种协作机制。需要注意,通过中断操作并不能直接终止一个线程,而是通知需要被中断的线程自行处理。 简单介绍下 Thread 类里提供的关于线程中断的几个方法:
Thread.interrupt():中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为 true(默认是 flase);Thread.isInterrupted():测试当前线程是否被中断。Thread.interrupted():检测当前线程是否被中断,与isInterrupted()方法不同的是,这个方法如果发现当前线程被中断,会清除线程的中断状态。
在线程中断机制里,当其他线程通知需要被中断的线程后,线程中断的状态被设置为 true,但是具体被要求中断的线程要怎么处理,完全由被中断线程自己决定,可以在合适的时机中断请求,也可以完全不处理继续执行下去。
RUNNABLE和RUNNING&READY
由上图(图源:挑错 |《Java 并发编程的艺术》中关于线程状态的三处错误)可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态(图源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
为什么 JVM 没有区分这两种状态呢?
现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin 式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。
- 当线程执行
wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。 - TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过
sleep(long millis)方法或wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。 - 当线程进入
synchronized方法/块或者调用wait后(被notify)重新进入synchronized方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。 - 线程在执行完了
run()方法之后将会进入到 TERMINATED(终止) 状态。 相关阅读:线程的几种状态你真的了解么?
Q:程序计数器为什么是私有的?
程序计数器主要有下面两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
Q:虚拟机栈和本地方法栈为什么是私有的?
- 虚拟机栈: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
Q:一句话简单了解堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存);方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
Q:Java和OS的线程区别:现在的 Java (1.2+)线程的本质其实就是操作系统的线程。
JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。由于绿色线程和原生线程比起来在使用时有一些限制(比如绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核),在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。
- 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。
- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。
总结:用户线程创建和切换成本低,但不可以利用多核。内核态线程创建和切换成本高,可以利用多核。
线程模型:用户线程和内核线程之间的关联方式
常见的线程模型有这三种:
- 一对一(一个用户线程对应一个内核线程)
- 多对一(多个用户线程映射到一个内核线程)
- 多对多(多个用户线程映射到多个内核线程)
在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个 Java 线程对应一个系统内核线程。Solaris 系统是一个特例(Solaris 系统本身就支持多对多的线程模型),HotSpot VM 在 Solaris 上支持多对多和一对一。具体可以参考 R 大的回答: JVM 中的线程模型是用户级的么?