Java高并发深度解析(一)

November 7, 2018 3 min read Author: Yu

要说高并发,不可避免要说到Thread类,先从Thread的构造方法说起:

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);
}

Thread拥有多达10个构造方法,这里拎出以上4个最常用的方法进行分析。在这些构造方法中最终都调用了一个私有的init方法,查看源码中的javadoc可以发现,这个方法中完成了对线程所在组,线程名称的初始化,Runnable实例的传入等。如果在初始化Thread时没有指定线程名,对应的构造方法中的init方法也会用”Thread-” + nextThreadNum()为线程生成一个缺省的名称;同理,若在初始化时没有指定线程组,会与父线程保持在一个线程组:

if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager,
ask the security manager what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}

Thread中最常用的方法非run()start()莫属,在许多教材和讲授中都提到实现多线程有两种方法,要么重写run()方法,要么传入一个Runnable实例,这种说法是不严谨的,查看run()方法的源码可以发现,内部实现非常简单,若target不为空,则调用其run方法:

@Override
public void run() {
if (target != null) {
target.run();
}
}

这个target是什么呢?看看前面Thread的构造方法就能知道,target就是传入的Runnable实例,也就是说,Runnable接口只是为了模块化编程,并没有增加任何实现多线程的新途径,也就是说实现多线程只有一种办法,就是在run方法中写入要执行的内容。在重写run方法或者传入了Runnable实例后,run方法里面已经有事可做了,但是开辟线程执行内容调用的是start方法而不是run方法,start()源码如下:

public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}

在start中又调用了start0方法,查看源码可得知start0方法是一个本地方法,这个方法才是真正开辟线程执行相关内容的方法,不过与JNI有关的实现这里暂且不表。

还有一点非常重要,就是Thread中各个方法的调用时机,start方法的调用时机是一个分界线,在这个调用之后,一些方法将变得不可用,例如在线程开启之后但未结束时将线程设置为守护线程,会抛出IllegalThreadStateException异常,但是在线程执行结束后调用就不会出现问题(但是实际中估计没人会这样做,这样很傻)。还有一个比较重要的问题,假如我写了一个线程执行了一系列任务,在执行完毕后我又想再次执行一遍,我可以再次调用该线程的start方法来再一次执行吗?

答案当然是不行的,start0中的某些底层的过程是不可逆的,这导致了一个线程对象不能重复执行,如果尝试在线程执行过程中或执行结束后再次调用start方法时,依然会抛出IllegalThreadStateException。线程的主要状态有Running,Runnable,Blocked,Terminated,除了Terminated之外,线程在其他三个状态是可以互相转换的,一旦线程从任意状态切换为Terminated,就意味着线程彻底终止,不会再转为其他状态,所以在线程结束后不能通过调用start再次执行。

而setPriority以及setName方法虽然也调用了一些底层的本地方法,但是并未改变线程状态,所以在线程执行中或执行结束时调用并不会产生异常,但是上面提到过的守护线程setDaemon方法也没有改变运行状态,为什么不能在线程运行或者结束时调用呢?很简单,setDaemon方法不改变运行状态,但是决定着运行状态,试想:假设JVM中还有一个或多个非守护线程在执行(或许还持有锁,并且main线程已经执行完毕),这时候突然将这个/些非守护线程变为守护线程,此时JVM会突然退出吗?还是Crash掉?线程会正常的交出锁吗?哦 这简直太混乱了。

实际上,Java早已想到了如上情况的存在(那当然了),在Thread类中有一个threadStatus变量,在start、setName等方法中均出现此变量的影子,start方法中的第一句便是如此:

if (threadStatus != 0)
throw new IllegalThreadStateException();

setName同理:

this.name = name;
if (threadStatus != 0) {
//本地方法
setNativeName(name);
}

可以看出,这个flag是为了标记线程的运行状态,以便在不同状态调用方法时有不同的应对方案。

以上解析了Thread类中的一些内部机制,后面会用大量篇幅来分析并发中的一些并发中实际问题以及解决方案。