1. 线程概述
对于Java 中的线程主要是依赖于系统的API 的实现, 这一点可以从 java.lang.Thread 源码中关键的方法都是 native 方法看出,亦可以直接查看OpenJDK 源码看出来; 对于JDK8 而言, window 和 linux 版使用的都是 1 : 1 线程模型, 即系统内核线程和轻量级进程的比是 1 : 1
- 内核线程(Kernel-Level Thread, KLT): 是由操作系统内核直接支持的线程, 这种线程由内核来完成切换, 内核通过调度器(Schedule)进行调度, 并负责将线程的任务映射到各个处理器上;
- 轻量级进程(Light Weight Process, LWP): 程序可以直接使用的一种内核线程高级接口,也就是我们通常意义上说的线程
如图所示:
优点:由内核线程的支持, 每个线程都成为一个独立的调度单元, 即线程之间不会相互阻塞影响; 使用内核提供的线程调度 功能及处理器映射, 可以完成线程的切换, 并将线程的任务映射到其他处理器上,充分利用多核处理器的优势,实现 真正的并行
缺点:同时由于基于内核线程实现, 线程的创建、关闭、同步操作都需要系统调用; 需要在用户态和内核态之间切换,代价 相对较高; 另外每个线程都需要消耗一定的内核资源,如内核线程的栈空间,所以系统支持的轻量级进程是有限的;
2. 线程状态
Java 线程整个生命周期可能会经历5个状态
- 新建(New) : 新建后未启动的线程
- 运行(Runnable): 包括运行中(Running) 和 就绪(Ready) 两种状态; 也就是正在运行,或者等待CPU 分配执行时间;
- 等待(Waiting): 无限期的等待其他线程显示唤醒
- 超时等待(Timed_Waiting): 一定时间内没有被其他线程唤醒, 由系统自动唤醒
- 阻塞(Blocked): 等待获取排他锁
- 终止(Terminated): 运行终止
3. 源码分析
3.1. native 注册
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
这段代码在很多地方都出现过,比如:
java.lang.System
java.lang.Object
java.lang.Class
其作用就是在使用 JNI 时需要向 JVM 注册,其方法名默认为 Java_<fully qualified class name>_method
;但是如果觉得这样的名字太长,这是就可以使用 registerNatives()
向 JVM 注册任意的函数名;
Thread 中的 native 方法有:
private native void start0();
private native void stop0(Object o);
public final native boolean isAlive();
private native void suspend0();
private native void resume0();
private native void setPriority0(int newPriority);
public static native void yield();
public static native void sleep(long millis) throws InterruptedException;
public static native Thread currentThread();
public native int countStackFrames();
private native void interrupt0();
private native boolean isInterrupted(boolean ClearInterrupted);
public static native boolean holdsLock(Object obj);
private native static Thread[] getThreads();
private native static StackTraceElement[][] dumpThreads(Thread[] threads);
private native void setNativeName(String name);
3.2 构造方法和成员变量
public class Thread implements Runnable {
private volatile String name; // 线程名称,如果没有指定,就通过 Thread-线程序列号 命名
private int priority; // 线程优先级,1-10 默认与父线程优先级相同(main 线程优先级为 5)
private boolean daemon = false; // 是否是守护线程
private Runnable target; // Runnable 对象
private ThreadGroup group; // 所属线程组
ThreadLocal.ThreadLocalMap threadLocals = null; // 线程本地变量
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 可继承的线程本地变量
private long tid; // 线程 tid
...
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(ThreadGroup group, Runnable target) {
init(group, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
init(null, null, name, 0);
}
public Thread(ThreadGroup group, String name) {
init(group, null, name, 0);
}
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name) {
init(group, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name, long stackSize) {
init(group, target, name, stackSize);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
g.checkAccess();
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
tid = nextThreadID();
}
...
}
可以看到左右的构造方法最终都会调用 init()
;并初始化所属线程组、名字、 Runnable、栈大小等信息;整个过程相当于配置了一个线程工厂,此时只是初始化了所有的配置,线程还没有真正创建,当然资源同样也还没有分配,只有在调用 start()
的时候线程才会真正创建;
此外可以看到线程创建过程中会有很多的权限检查,例如:
SecurityManager security = System.getSecurityManager();
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
通常情况下权限的检查默认是没有开启的,所以 security
一直都是 null
;这里需要在启动 JVM 的时候指定 -Djava.security.manager
;当然也可以指定特定的 SecurityManager
;但是在开启的时候很可能会遇到类似:java.security.AccessControlException: access denied
;权限检查失败的错误;
此时可以在 jre\lib\security\java.policy
中添加相应的权限;或者直接开启所有权限 permission java.security.AllPermission;
// jre\lib\security\java.policy
grant {
permission java.lang.RuntimePermission "stopThread";
permission java.net.SocketPermission "localhost:0", "listen";
permission java.util.PropertyPermission "java.version", "read";
...
permission java.security.AllPermission;
};
3.3 start 方法
public synchronized void start() {
if (threadStatus != 0) throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
//
}
}
}
private native void start0();
可以看到这是一个同步方法,并且同一个线程不能启动两次;这里首先将线程加入对应的线程组,再真正创建线程,如果创建失败就在线程组中标记;对应的这个 native
方法 start0
,的源码同样可以查看 openjdk\hotspot\src\share\vm\prims\jvm.cpp
,这里就不详细介绍了;
5.4 exit 方法
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
exit
方法则是由系统调用,在 Thread 销毁前释放资源;
5.5 弃用方法
public final void stop() { } // 停止线程
public final void suspend() { } // 暂停线程
public final void resume() { } // 恢复线程
- stop:停止线程会导致它解锁已锁定的所有监视器,从而产生同步问题,因为它本质上是不安全的。
- suspend:暂停线程容易出现死锁,如果目标线程在监视器上保持锁定,那么在恢复目标线程之前,任何线程都无法访问此资源。
- resume:恢复线程同样容易出现死锁, 如果 A 线程在恢复 B 线程之前锁定监视器,然后在调用 resume 恢复 B,此时 B 会尝试再次获取锁,这样就会导致死锁。
4. 线程通讯
其实所有的多线程问题,其本质都是线程之间的通讯问题,也有的说是通讯和同步两个问题(线程间操作的顺序);但我觉得同步仍然是线程之间通过某种方式进行通讯,确定各自执行的相对顺序;所以仍然可以算作是一种通讯问题;这里线程之间的通讯问题可以分成两种:
- 共享变量,类似锁对象、volatile、中断等操作都可以算是共享变量通讯;
- 消息传递,类似 wait\notify、管道等则可以算是通过消息直接传递通讯;
下面我们将介绍和 Thread 类直接相关的几种通讯,关于锁的部分之后的博客还会详细介绍
4.1 wait\notify 机制
package com.test;
import java.util.concurrent.TimeUnit;
/**
* @Descrintion:
* @Date : Created in 19:41 2019/5/13
* @
*/
public class Waitnotify {
private static boolean flag = true ;
private static final Object LOCK = new Object();
private static class Wait implements Runnable{
@Override
public void run() {
synchronized (LOCK){
try {
while (flag) {
System.out.println("flag is true , wait");
LOCK.wait();
}
}catch (Exception e){
}
System.out.println("flag is false ,running");
}
}
}
private static class Notify implements Runnable{
@Override
public void run() {
synchronized (LOCK){
System.out.println("hold lock,notify");
LOCK.notify();
flag = false;
try {
TimeUnit.SECONDS.sleep(5);
}catch (Exception e){
}
}
synchronized (LOCK){
try {
System.out.println("hold lock again, sleep");
TimeUnit.SECONDS.sleep(5);
}catch (Exception e){
}
}
}
}
public static void main(String[] args) throws Exception{
Thread waitThread = new Thread(new Wait(), "WaitThread");
waitThread.start();
TimeUnit.SECONDS.sleep(1);
Thread notifyThread = new Thread(new Notify(), "NotifyThread");
notifyThread.start();
}
}
4.2 join
package com.test;
import java.util.concurrent.TimeUnit;
/**
* @Descrintion:
* @Date : Created in 19:57 2019/5/13
* @
*/
public class JoinTest {
public static void main(String[] args)throws Exception {
Thread previous = Thread.currentThread();
for(int i = 0 ; i < 5 ; i++){
Thread thread = new Thread(new Domino(previous), String.valueOf(i));
thread.start();
previous = thread ;
}
TimeUnit.SECONDS.sleep(2);
System.out.println("terminate");
}
private static class Domino implements Runnable{
private Thread thread;
public Domino(Thread thread){
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
}catch (Exception e){
}
System.out.println("terminate");
}
}
}
每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回
4.3 interrupt
private volatile Interruptible blocker;
private final Object blockerLock = new Object();
// Set the blocker field; invoked via sun.misc.SharedSecrets from java.nio code
void blockedOn(Interruptible b) {
synchronized (blockerLock) {
blocker = b;
}
}
public void interrupt() {
if (this != Thread.currentThread()) checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
private native void interrupt0();
- 如果线程处于 WAITINF、TIMED_WAITING (正阻塞于 Object 类的 wait()、wait(long)、wait(long, int)方法,或者 Thread 类的 join()、join(long)、join(long, int)、sleep(long)、sleep(long, int)方法),则该线程的中断状态将被清除,并收到一个
java.lang.InterruptedException
; - 如果线程正阻塞于 InterruptibleChannel 上的 I/O 操作,则该通道将被关闭,同时该线程的中断状态被设置,并收到一个
java.nio.channels.ClosedByInterruptException
; - 如果线程正阻塞于 java.nio.channels.Selector 操作,则该线程的中断状态被设置,同时它将立即从选择操作返回,并可能带有一个非零值,其效果同
java.nio.channels.Selector.wakeup()
方法一样; - 如果上述条件都不成立,则该线程的中断状态将被设置;
其中 Interruptible blocker
就是在 NIO
操作的时候通过 sun.misc.SharedSecrets
设置的(其效果同反射,但是不会生成其他对象,也就是不会触发 OOM);
interrupted 、isInterrupted 方法:
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);
可以很清楚的看到他们都是通过 isInterrupted(boolean ClearInterrupted)
方法实现的,但是 interrupted
会清除中断状态,而 isInterrupted
则不会清除;
以上 interrupt 机制
就通过设置 interrupt flag
,查询中断状态,以及中断异常构成了一套完整的通讯机制;也可以看作是通过 interrupt flag
共享变量实现的,下面我们简单举例:
public class Interrupted {
public static void main(String[] args) throws Exception {
// sleepThread不停的尝试睡眠
Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
sleepThread.setDaemon(true);
// busyThread不停的运行
Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
busyThread.setDaemon(true);
sleepThread.start();
busyThread.start();
// 休眠5秒,让sleepThread和busyThread充分运行
TimeUnit.SECONDS.sleep(5);
sleepThread.interrupt();
busyThread.interrupt();
log.info("SleepThread interrupted is {}", sleepThread.isInterrupted());
log.info("BusyThread interrupted is {}", busyThread.isInterrupted());
// 防止sleepThread和busyThread立刻退出
TimeUnit.SECONDS.sleep(5);
log.info("exit");
}
static class SleepRunner implements Runnable {
@Override
public void run() {
try {
while (true) {
Thread.sleep(2000);
}
} catch (InterruptedException e) {
log.error("SleepThread interrupted is {}", Thread.currentThread().isInterrupted());
Thread.currentThread().interrupt();
log.error("SleepThread interrupted is {}", Thread.currentThread().isInterrupted());
}
log.info("exit");
}
}
static class BusyRunner implements Runnable {
@Override
public void run() {
if (1 == 1) {
while (true) {
}
}
log.info("exit");
}
}
}
- 打断的确是一个标记,对于未处于可打断状态的线程,或者没有处理打断状态的线程是没有影响的,就像 BusyThread;
- 使用 interrupt 打断睡眠线程,也的确符合上面的情况,但是因为收到
InterruptedException
的时候会清楚中断标记,所以这里可以再次设置中断标记;