前言
在《java线程池源码阅读》发出后很多小伙伴问到了Thread相关的问题。因为Thread是并发的基础,所以今天我们围绕一些问题来对Thread进行源码级别的了解。
除了小伙伴问的问题,笔者还整理了一下,涵盖以下问题:
- run()方法和start()方法的区别?
- 异步线程想要返回结果咋办?
- 线程死锁啥时候发生的?
- 线程的生命周期是怎样的?
- sleep、join、wait的区别?
interrupt()、stop的区别- 线程假死啥时候发生,怎么排查?
下面我们还是先对Thread做一个基本了解。
Thread
关于线程的基础知识,可以看下笔者之前的文章《操作系统之进程管理》
线程是程序中的执行线程。JVM允许应用程序同时运行多个执行线程。
每个线程都有优先级。优先级较高的线程优先于优先级较低的线程执行。每个线程也可以标记为守护线程,也可以不标记为守护线程。当在某个线程中运行的代码创建新的线程对象时,新线程的优先级最初设置为与创建线程的优先级相等,并且仅当创建线程是守护线程时,该线程才是守护线程。
当JVM时,通常只有一个非守护进程线程(它通常调用某个指定类的main方法)。JVM将继续执行线程,直到发生以下任一情况:
- 调用Runtime类的exit方法并且安全管理器已允许执行exit操作。
- 所有非守护线程都已死亡,要么是通过调用run方法返回,要么是通过抛出传播到run方法之外的异常。
Tomcat Web应用服务器为啥能一直运行呢?
根据前面的非守护线程的介绍,很让人能想到服务为啥能一直运行,原因是其中有的线程在做“无限”循环,这个循环如果你不手动结束它它就永远不会结束,因此Tomcat能够一直在后台运行。
我们可以跟踪Tomcat的Bootstrap的main方法。你会发现最终有一个异步方法中有一个while循环:
public final class StandardServer extends LifecycleMBeanBase implements Server {
/**
* Wait until a proper shutdown command is received, then return.
* This keeps the main thread alive - the thread pool listening for http
* connections is daemon threads.
*/
@Override
public void await() {
// Negative values - don't wait on port - tomcat is embedded or we just don't like ports
if (getPortWithOffset() == -2) {
// undocumented yet - for embedding apps that are around, alive.
return;
}
if (getPortWithOffset() == -1) {
try {
awaitThread = Thread.currentThread();
while(!stopAwait) {
try {
Thread.sleep( 10000 );
} catch( InterruptedException ex ) {
// continue and check the flag
}
}
} finally {
awaitThread = null;
}
return;
}
// Set up a server socket to wait on
try {
awaitSocket = new ServerSocket(getPortWithOffset(), 1,
InetAddress.getByName(address));
} catch (IOException e) {
log.error(sm.getString("standardServer.awaitSocket.fail", address,
String.valueOf(getPortWithOffset()), String.valueOf(getPort()),
String.valueOf(getPortOffset())), e);
return;
}
try {
awaitThread = Thread.currentThread();
// Loop waiting for a connection and a valid command
while (!stopAwait) {
ServerSocket serverSocket = awaitSocket;
if (serverSocket == null) {
break;
}
// Wait for the next connection
Socket socket = null;
StringBuilder command = new StringBuilder();
try {
InputStream stream;
long acceptStartTime = System.currentTimeMillis();
try {
socket = serverSocket.accept();
socket.setSoTimeout(10 * 1000); // Ten seconds
stream = socket.getInputStream();
} catch (SocketTimeoutException ste) {
// This should never happen but bug 56684 suggests that
// it does.
log.warn(sm.getString("standardServer.accept.timeout",
Long.valueOf(System.currentTimeMillis() - acceptStartTime)), ste);
continue;
} catch (AccessControlException ace) {
log.warn(sm.getString("standardServer.accept.security"), ace);
continue;
} catch (IOException e) {
if (stopAwait) {
// Wait was aborted with socket.close()
break;
}
log.error(sm.getString("standardServer.accept.error"), e);
break;
}
// Read a set of characters from the socket
int expected = 1024; // Cut off to avoid DoS attack
while (expected < shutdown.length()) {
if (random == null) {
random = new Random();
}
expected += (random.nextInt() % 1024);
}
while (expected > 0) {
int ch = -1;
try {
ch = stream.read();
} catch (IOException e) {
log.warn(sm.getString("standardServer.accept.readError"), e);
ch = -1;
}
// Control character or EOF (-1) terminates loop
if (ch < 32 || ch == 127) {
break;
}
command.append((char) ch);
expected--;
}
} finally {
// Close the socket now that we are done with it
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
// Ignore
}
}
// Match against our command string
boolean match = command.toString().equals(shutdown);
if (match) {
log.info(sm.getString("standardServer.shutdownViaPort"));
break;
} else {
log.warn(sm.getString("standardServer.invalidShutdownCommand", command.toString()));
}
}
} finally {
ServerSocket serverSocket = awaitSocket;
awaitThread = null;
awaitSocket = null;
// Close the server socket and return
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
// Ignore
}
}
}
}
}
这里存在一个volatile的boolean变量来控制一个while循环,而这个变量只要不被设置为false则永远循环下去,它的作用是不断通过ServerSocket来获取TCP数据报中的“shutdown”指令,也就是你主动关闭tomcat的执行逻辑。
Thread的字段和构造函数
/* 线程名 */
private volatile String name;
/* 优先级 */
private int priority;
private Thread threadQ;
private long eetop;
/* 是否single_step执行此线程 */
private boolean single_step;
/* 该线程是否是守护线程 */
private boolean daemon = false;
/* JVM 状态 */
private boolean stillborn = false;
/* 将运行的任务. */
private Runnable target;
/* 线程分组 */
private ThreadGroup group;
/* 此线程的上下文类加载器 */
private ClassLoader contextClassLoader;
/* 此线程继承的AccessControlContext */
private AccessControlContext inheritedAccessControlContext;
/* 用于自动给匿名线程编号 */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
/* 与此线程相关的ThreadLocal值。此map由ThreadLocal类维护 */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* 与此线程相关的InheritableThreadLocal值。此map由ThreadLocal类维护。
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
/*
* 此线程请求的堆栈大小,如果创建者未指定堆栈大小,则为0。
* 这取决于VM如何利用这个数字;有些虚拟机会忽略它。(JVM会忽略)
*/
private long stackSize;
/*
* 本机线程终止后持续存在的JVM私有状态。
*/
private long nativeParkEventPointer;
/*
* 线程 ID
*/
private long tid;
/* 用于生成线程 ID */
private static long threadSeqNumber;
/*
* tools的Java线程状态,初始化为指示线程“尚未启动”
*/
private volatile int threadStatus = 0;
private static synchronized long nextThreadID() {
return ++threadSeqNumber;
}
/**
* The argument supplied to the current call to
* java.util.concurrent.locks.LockSupport.park.
* Set by (private) java.util.concurrent.locks.LockSupport.setBlocker
* Accessed using java.util.concurrent.locks.LockSupport.getBlocker
*/
volatile Object parkBlocker;
/* The object in which this thread is blocked in an interruptible I/O
* operation, if any. The blocker's interrupt method should be invoked
* after setting this thread's interrupt status.
*/
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 final static int MIN_PRIORITY = 1;
/**
* 线程默认分配的优先级。
*/
public final static int NORM_PRIORITY = 5;
/**
* 线程可以拥有的最大优先级
*/
public final static int MAX_PRIORITY = 10;
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) {
init(group, target, name, stackSize);
}
run()方法和start()方法的区别?
可以看到,根据构造函数传过来的Runnable对象在这里使用,Thread本身也是实现了Runnable接口,所以就相当于把任务交给Thread了让它来执行。
public void run() {
if (target != null) {
target.run();
}
}
使该线程开始执行;JVM调用该线程的run方法。
结果是两个线程同时运行:当前线程(从调用start方法返回)和另一个线程(执行其run方法)。
多次启动线程是不合法的。特别是,线程一旦完成执行,就不能重新启动。——所以这里使用了synchronized对方法进行修饰。
public synchronized void start() {
/**
* 对于VM创建/设置的主方法线程或“系统”组线程,不会调用此方法。
* 将来添加到此方法的任何新功能可能也必须添加到VM中。——意思就是这里只关注执行任务
*
* 0表示线程处于"NEW"状态。
*/
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) {
/* 不做什么。如果start0抛出了一个可丢弃的,那么它将被传递到调用堆栈中 */
}
}
}
本地方法会开启一个线程来调用Thread的run方法。
private native void start0();
所以很明显,run方法就是一个普通的方法,和你平时调用对象的方法一样,是由Caller线程执行。start方法会调用本地方法新建一个线程来执行任务,所以会说是两个线程同时运行。
线程的生命周期是怎样的?
我们可以看到线程的状态值是threadStatus,可以发现它的相关赋值代码是找不到,应该是JVM来控制的。不过我们能找到get方法:
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
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;
}
}
既然有枚举我们就可以看看官方对各个状态的定义。
NEW
尚未启动的线程处于此状态。
RUNNABLE
在JVM中执行的线程处于这种状态,但它可能正在等待来自操作系统的其他资源,例如处理器。
BLOCKED
被阻止等待监视器锁定的线程处于此状态。比如等待一个synchronized锁。
WAITING
无限期等待另一个线程执行特定操作的线程处于此状态。
以下会触发该状态:
- Object#wait():等待另一个线程调用此对象的notify或者notifyAll方法。
- Thread#join()
- LockSupport#park()
TIMED_WAITING
等待另一个线程执行操作的线程在指定的等待时间内处于此状态。
以下会触发该状态:
- Thread.sleep
- Object.wait(J)
- Thread.join(J)
- LockSupport#parkNanos
- LockSupport#parkUntil
TERMINATED
已退出的线程处于此状态。
读者可以根据定义自行实验,下面的图能大致描述不同状态的转变。
sleep、join、wait的区别?
sleep和wait的区别
- sleep是位于Thread类中的,wait是位于Object类中的。
- sleep是让线程进行定时休眠会导致当前线程处于TIMED_WAITTING状态;wait是等待另一个线程调用对象的notify(唤醒正在该对象监视器上等待的单个线程。)或者notifyAll(唤醒正在该对象监视器上等待的所有线程。)方法,指定超时就会导致线程处于TIMED_WAITTING状态,不指定就会导致线程处于WAITTING状态。
- wait需要给对象加上synchronized锁,如果不加会抛出
InterruptedException异常。需要如下:
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
synchronized (obj) {
... // create condition
obj.notify();
}
为啥wait需要和notify使用同一个synchronized?
这里设进行notify/notifyAll的线程为A线程,wait的线程为B线程。
因为在该模式下需要先等B线程先生产,A线程再消费,如果B和A同时发生的话,很可能B还没生产好A就去消费了个寂寞,后面B生产好了也没线程消费了,所以需要上锁同步。至于为啥synchronized要使用obj而不是其他的对象,因为调用obj.wait()之后就可以释放锁,通知wait停止阻塞后应该会重新加上。
如下:
@SneakyThrows
public static void main(String[] args) {
Object lock = new Object();
Thread thread1 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
synchronized (lock) {
long current = System.currentTimeMillis();
System.out.println("准备进行等待");
lock.wait();
System.out.println("等待完成,等待了" + (System.currentTimeMillis() - current));
}
}
});
thread1.start();
Thread.sleep(2000);
synchronized (lock) {
System.out.println("准备进行通知");
lock.notify();
System.out.println("通知完成");
}
}
输出:
准备进行等待
准备进行通知
通知完成
等待完成,等待了2006
证实下B线程被通知后是不是会重新加上锁:
@SneakyThrows
public static void main(String[] args) {
Object lock = new Object();
Thread thread1 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
synchronized (lock) {
long current = System.currentTimeMillis();
System.out.println("准备进行等待");
lock.wait();
System.out.println("等待完成,等待了" + (System.currentTimeMillis() - current));
Thread.sleep(2000);
}
}
});
thread1.start();
Thread thread2 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
long current = System.currentTimeMillis();
Thread.sleep(2500);
synchronized (lock) {
System.out.println("tthread2获得了锁,执行消耗了" + (System.currentTimeMillis() - current));
}
}
});
thread2.start();
Thread.sleep(2000);
synchronized (lock) {
System.out.println("准备进行通知");
lock.notify();
System.out.println("通知完成");
}
}
输出:
准备进行等待
准备进行通知
通知完成
等待完成,等待了2013
thread2获得了锁,执行消耗了4012
可以看到需要等thread1彻底完成后再释放锁,thread2才能获得锁。
再证实下notify是不是不会管线程有没有消费通知:
@SneakyThrows
public static void main(String[] args) {
Object lock = new Object();
Thread thread1 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
Thread.sleep(1000);
synchronized (lock) {
long current = System.currentTimeMillis();
System.out.println("准备进行等待");
lock.wait();
System.out.println("等待完成,等待了" + (System.currentTimeMillis() - current));
}
}
});
thread1.start();
synchronized (lock) {
System.out.println("准备进行通知");
lock.notify();
System.out.println("通知完成");
}
}
输出:
准备进行通知
通知完成
准备进行等待
一直等待
总结
可以发现wait和notify/notifyAll是一个粗糙的生产消费模式,notify不会管等待的线程有没有生产完,只管消费,就算没有就过去了,后面来了也不消费。所以一般不使用它。
底层的逻辑应该是wait方法会释放obj的对象头中的锁标记,把等待的线程的信息保存下来,notify/notifyAll会去保存信息的位置找有没有因为obj#wait导致阻塞的线程,有的话就直接唤醒(如果有阻塞了多个那就一个一个唤醒,因为需要持有锁)并把锁归还。
sleep和join的区别
根据系统计时器和调度程序的精度和准确性,使当前执行的线程休眠(暂时停止执行)指定的毫秒数。线程不会失去任何监视器的所有权(不会像wait一样释放锁)。
public static native void sleep(long millis) throws InterruptedException;
等待此线程死亡的时间最多为毫秒。超时0表示永远等待。使用一个循环条件为this.isAlive内部调用this.wait。当一个线程终止时this.notifyAll方法是被调用。建议应用程序在线程实例上不使用wait、notify或者notifyAll。
public final synchronized void join(long millis)
throws InterruptedException {
//当前时间戳
long base = System.currentTimeMillis();
long now = 0;
//等待时间不能小于0
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
//无限等待
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
// 防止wait时间不足
now = System.currentTimeMillis() - base;
}
}
}
可以看到sleep是用来实当前线程进行一段时间的休眠,只会导致线程状态处于TIMED_WAITING。而join是用来等待当前线程死亡内部使用wait,线程终止的时候会调用notify/notifyAll,肯定不是用来等待自己的线程死亡,一般用在另一个线程等其他线程死亡。
interrupt、stop的区别
stop
强制停止线程执行。
thread所代表的线程将被迫停止无论它在做什么反常的操作,并将新创建的ThreadDeath对象作为异常抛出。
可以看到stop不建议使用了,因为它会强制线程停止,如果持有锁锁也不会释放。
@Deprecated
public final void stop() {
SecurityManager security = System.getSecurityManager();
//如果安装了安全管理器,则调用其checkAccess方法,并将其作为参数。这可能会导致(在当前线程中)引发SecurityException。
if (security != null) {
checkAccess();
//如果此线程与当前线程不同(即,当前线程正在尝试停止自身以外的线程),则会另外调用安全管理器的checkPermission方法。
if (this != Thread.currentThread()) {
security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
}
}
// 0表示线程的状态为NEW,它不能为非NEW,因为持有锁。
if (threadStatus != 0) {
resume(); // 如果线程被挂起,则唤醒线程;没有别的办法
}
// VM可以停止所有状态的线程
stop0(new ThreadDeath());
}
interrupt
中断线程(只是设置中断标识)。 除非当前线程正在中断自身(这是始终允许的),否则会调用此线程的checkAccess方法,这可能会导致抛出SecurityException。
如果该线程在调用对象类的wait()、wait(long)或wait(long,int)方法,或该类的join()、join(long)、join(long,int)、sleep(long)或sleep(long,int)方法时被阻塞,那么它的中断状态将被清除,并接收到InterruptedException。
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // 只是设置中断标识
b.interrupt(this);
return;
}
}
interrupt0();
}
只能中断处于RUNNABLE状态的线程,可以验证下:
Thread thread2 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
while (true) {
System.out.println("111");
}
}
});
thread2.start();
thread2.interrupt();
System.out.println("thread2 isInterrupted:" + thread2.isInterrupted());
输出:
thread2 isInterrupted:true
中断TIMED_WAIT状态的:
Thread thread2 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
Thread.sleep(2000);
System.out.println("thread2完成");
}
});
thread2.start();
thread2.interrupt();
System.out.println("thread2 isInterrupted:" + thread2.isInterrupted());
thread2 isInterrupted:false
Exception in thread "Thread-1" java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.study.jvm.Demo$2.run(Demo.java:73)
at java.lang.Thread.run(Thread.java:748)
总结
stop是强制的不安全,interrput只是发送信号,中断处于RUNNABLE状态的线程其实并没有终止,中断处于阻塞或者等待状态的线程会终止并抛出异常。
异步线程想要返回结果咋办?-FutureTask
直接上代码
FutureTask task = new FutureTask<Boolean>(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
Thread.sleep(1000);
return true;
}
});
Thread thread = new Thread(task);
thread.start();
long current = System.currentTimeMillis();
task.get();
System.out.println("获取结果消耗:" + (System.currentTimeMillis() - current));
get会一直阻塞等待结果。
get的加锁和释放
这里简单说下
这里调用了LockSupport.park和ReentrantLock的AQS阻塞一样。那么解除阻塞应该是run方法内部,见下:
线程死锁啥时候发生的?
直接上代码模拟:
Object lock1 = new Object();
Object lock2 = new Object();
Thread thread1 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
synchronized (lock1) {
System.out.println("thread1成功持有lock1");
Thread.sleep(1000);
synchronized (lock2) {
System.out.println("thread1成功持有lock2");
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
synchronized (lock2) {
System.out.println("thread2成功持有lock2");
Thread.sleep(1000);
synchronized (lock1) {
System.out.println("thread2成功持有lock1");
}
}
}
});
thread1.start();
thread2.start();
输出:
thread1成功持有lock1
thread2成功持有lock2
一直在等待。
线程假死啥时候发生,怎么排查?
假死—线程存在但是没有一直没有任何表现,比如:
- 没有日志输出
- 不进行任何作业 大概率是阻塞等待,或者出现了死锁。
可以使用VisualVM、jconsole等等进行排查。查看《Java服务相关问题排查》。