Runnale接口
我们看Thread类的定义知道,它实现了Runable接口
public class Thread implements Runnable {
...
}
而Runnable接口的定义如下:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
它只有一个抽象方法run。同时,该接口还被@FunctionalInterface注解标注,说明它是一个函数式接口(@FunctionalInterface是java 1.8版本之后引入的)。这意味着我们可以使用Lambda表达式来创建Runnable接口的实例,这个我们到后面再举例。
线程创建
在java中,创建一个线程,有且仅有一种方式:
创建一个Thread类实例,并调用它的start方法。
这写在了java语言规范中(参见The Java Language Specification, Java SE 8 Edition, P659,chapter17):
Threads are represented by the Thread class. The only way for a user to create a thread is to create an object of this class; each thread is associated with such an object. A thread will start when the start() method is invoked on the corresponding Thread object.
构造函数
要创建一个Thread类的实例自然要通过构造函数,Thread的public构造函数有8个之多,但是他们本质上都调用了同一个init函数:
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
init(null, null, name, 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target) {
init(group, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(ThreadGroup group, String name) {
init(group, null, 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);
}
可见,这八个public类型的构造函数只不过是给init的方法的四个参数分别赋不同的值, 这四个参数分别是:
- ThreadGroup g(线程组)
- Runnable target (Runnable 对象)
- String name (线程的名字)
- long stackSize (为线程分配的栈的大小,若为0则表示忽略这个参数)
而init方法又调用了另一个init方法,设置了
AccessController,以及inheritThreadLocals参数:
/**
* Initializes a Thread with the current AccessControlContext.
* @see #init(ThreadGroup,Runnable,String,long,AccessControlContext,boolean)
*/
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
init(g, target, name, stackSize, null, true);
}
//上面那个init方法最终调用了下面这个方法:
/**
* Initializes a Thread.
*
* @param g the Thread group
* @param target the object whose run() method gets called
* @param name the name of the new Thread
* @param stackSize the desired stack size for the new thread, or
* zero to indicate that this parameter is to be ignored.
* @param acc the AccessControlContext to inherit, or
* AccessController.getContext() if null
* @param inheritThreadLocals if {@code true}, inherit initial values for
* inheritable thread-locals from the constructing thread
*/
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
...
}
init方法中有一些关于线程组和访问控制上下文的设置,这里我们暂时就不深入讨论了。 所以综上来看,我们最常用的也就两个参数:
- Runnable target (Runnable 对象)
- String name (线程的名字)
而对于线程的名字,其默认值为
"Thread-" + nextThreadNum(), nextThreadNum方法又是什么呢:
/* For autonumbering anonymous threads. */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
可见,它就是一个简单的递增计数器,所以如果创建线程时没有指定线程名,那线程名就会是:
Thread-0, Thread-1, Thread-2, Thread-3, ...
至此,我们看到,虽然Thread类的构造函数有这么多,但对我们来说真正重要的参数只有一个
Runnable target (Runnable 对象)
所以创建一个线程实例最重要的是要传入一个Runnable类型对象。
既然是Runnable类型,那么这个target必然是实现了Runnable接口的,也就是说该对象一定覆写了run方法。
我们知道,Thread类本身也实现了Runnable接口,所以它必然也覆写了run方法,我们先来看看它的run方法:
@Override
public void run() {
if (target != null) {
target.run();
}
}
可以看到,这个run方法仅仅是调用了target对象的run方法,如果我们在线程构造时没有传入target(例如调用了无参构造函数),那么这个run方法就什么也不会做。
启动线程
线程对象创建完了之后,接下来就是启动一个线程,在java中,启动一个线程必须调用线程的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 */
}
}
}
这个方法本质是调用了native的start0()方法,但是它的注释部分说明一些很重要的信息:
这个方法使得线程开始执行,并由JVM来执行这个线程的run方法,结果就是有两个线程在并发执行,一个是当前线程,也就是调用了Thread#start方法的线程,另一个线程就是当前thread对象代表的线程,它执行了run方法。
也就是说,这个Thread类实例代表的线程最终会执行它的run方法,而上面的分析中我们知道,它的run做的事就是调用Runnable对象的run方法,如果Runnable对象为null, 就啥也不做:
@Override
public void run() {
if (target != null) {
target.run();
}
}
有的同学就要问了,绕了一大圈,忙了大半天,最后不就是为了执行target对象的run方法吗?为什么我们不直接调用target的run方法?这一层层的调用究竟是为了啥? 答案是:
为了使用多线程 !
我们知道,Thread类从定义上看就是个普通的java类,是什么魔法让它从一个普通的java类晋升为一个可以代表线程的类呢?是native方法!
如果我们直接调用target对象的run方法,或者Thread类的run方法,那就是一个普通调用,因为run方法就是普普通通的类方法,与我们平时调用的其他类方法没有什么不同,这并不会产生多线程。
但是,如果我们调用了start方法,由于它内部使用了native方法来启动线程,它将导致一个新的线程被创建出来, 而我们的Thread实例, 就代表了这个新创建出来的线程, 并且由这个新创建出来的线程来执行Thread实例的run方法。
实战
说了这么多理论的东西,下面让我们通过一个实战来加深理解。java官方文档给我们提供了两种创建线程的方法.
方法1:继承Thread类,覆写run方法
首先我们自定义一个继承自Thread的类,并覆写run方法:
public class CustomizedThread extends Thread {
public void run() {
System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我是定义在CustomizedThread类中的run方法。");
}
}
然后我们创建类的实例,并调用start方法启动这个线程:
public class CustomizedThreadTest {
public static void main(String[] args) {
System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我在main方法里");
CustomizedThread myThread = new CustomizedThread();
myThread.start();
}
}
执行结果:
[main线程]: 我在main方法里
[Thread-0线程]: 我是定义在CustomizedThread类中的run方法。
可见,这里有两个线程,一个是main线程,它执行了main方法,一个是Thread-0线程,它是我们自定义的线程,它执行了run方法。
如果我们不通过start方法来运行线程会有什么不同呢:
public class CustomizedThreadTest {
public static void main(String[] args) {
System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我在main方法里");
CustomizedThread myThread = new CustomizedThread();
//myThread.start();
myThread.run();
}
}
这里我们直接调用自定义线程的run方法,看看结果有什么不同:
[main线程]: 我在main方法里
[main线程]: 我是定义在CustomizedThread类中的run方法。
可见,这次只有一个main线程,由main线程执行了我们自定义线程类的run方法,并没有新的线程产生。 其实这个时候,CustomizedThread的run方法就是一个普普通通的类的普普通通的方法,与我们平时定义的方法并没有什么特别之处。
有的同学要问了,上面不是说创建一个线程最重要的是传入一个Runnable对象吗? 我没有看到Runnable对象啊? 别急,我们来分析一下:
首先,我们的CustomizedThread继承自Thread类,则我们会调用父类的无参构造函数:
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
这个构造函数中,target对象为null;
然后,我们使用了myThread.start(),因为我们在子类中没有定义start方法,所以,这个方法来自父类,而Thread类的start方法的作用我们已经讲过,它将新建一个线程,并调用它的run方法,这个新建的线程的抽象代表就是我们的CustomizedThread,所以它的(CustomizedThread的)run方法将会被调用。
那么,如果我们的子类没有覆写run方法呢? ,那自然是继承Thread类自己的run方法了:
@Override
public void run() {
if (target != null) {
target.run();
}
}
而Thread类的run方法调用的又是target对象的run方法,而target对象现在为null, 所以这个方法啥也不做。
所以到这里我们就很清晰了,创建一个线程最重要的是定义一个run方法,这个run方法要么通过继承Thread类的子类覆写,要么通过直接构造Thread类时传入一个Runnable的target对象。无论它由子类覆写提供还是由target对象提供,start方法最终都会新建一个线程来执行这个run方法。
方法2:通过Runnable接口创建线程类
我们先来看官方的例子:
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
//The following code would then create a thread and start it running:
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
这个例子中首先定义了一个PrimeRun类实现了Runnable接口,接着实例化出一个对象p,并将这个对象作为参数传递给Thread类的构造方法。
这种方法看上去好像复杂了好多,但其实就是通过新建Thread类的对象来创建线程。它本质上就是传递一个Runnable对象给Thread的构造函数,所以我们完全可以用匿名类,又因为Runnable是一个函数接口,所以上面的代码完全可以被简写,我们来看一个例子:
public class CustomizedThreadTest {
public static void main(String[] args) {
System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我在main方法里");
Thread myThread = new Thread(() -> System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我是传递给Thread类的Runnable对象的run方法"));
myThread.start();
}
}
代码输出:
[main线程]: 我在main方法里
[Thread-0线程]: 我是传递给Thread类的Runnable对象的run方法
这里,myThread是我们new出来的Thread类的实例,我们调用了Thread类的构造函数:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
传入了一个Runnable对象,这个Runnable对象由lambda表达式表示。我们最后调用了 myThread.start()来启动这个线程,通过上一节的分析我们知道,start方法会调用run方法,而thread类的run方法最终会调用target对象的run方法,而target对象的run方法就是我们传进来的lambda表达式。上面这个例子其实等效于下面这种写法:
public class CustomizedThreadTest {
public static void main(String[] args) {
System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我在main方法里");
Thread myThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我是传递给Thread类的Runnable对象的run方法");
}
});
myThread.start();
}
}
可见函数式接口和lambda表达式使我们的书写变得简洁多了。
线程状态
在Thread类中, 线程状态是通过threadStatus属性以及State枚举类实现的:
在Thread类中, 线程状态是通过threadStatus属性以及State枚举类实现的:
/* Java thread status for tools,
* initialized to indicate thread 'not yet started'
*/
private volatile int threadStatus = 0;
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* 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.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
/**
* Returns the state of this thread.
* This method is designed for use in monitoring of the system state,
* not for synchronization control.
*
* @return this thread's state.
* @since 1.5
*/
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
从源码中可以看出, 线程一共有6种状态, 其状态转换关系如下图所示:
值得一提的是,从状态的定义中可以看出,RUNNABLE状态包含了我们通常所说的
running和ready两种状态。
常用方法
源码中currentThread定义如下:
/**
* Returns a reference to the currently executing thread object.
*
* @return the currently executing thread.
*/
public static native Thread currentThread();
可见,它是一个静态方法,并且是一个native方法,返回的是当前正在执行的线程。
爱思考的同学可能就要问了,现在咱都多核CPU了,同一时刻可以有多个线程跑在不同的CPU核心上,那当前正在执行的线程有多个,到底返回的是哪一个呢?
其实,这里"当前正在执行的线程"指的是当前正在执行这段代码的线程。
我们知道,线程是CPU调度的最小单位,任意一段代码总得由一个线程执行,所以该方法返回的是正在执行Thread.currentThread这行代码的线程,例如:
public class ThreadTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}
}
输出:
main
我们知道当一个Java程序启动以后,有一个线程就会立马跑起来,这就是通常所说的Main线程,main线程将会执行java的入口方法main方法,所以当前正在执行Thread.currentThread()方法的线程就是main线程。
sleep
谈起sleep方法, 被问的最多的两个问题就是:
- Thread.sleep() 与 Thread.currentThread().sleep() 有什么区别?
- Thread.sleep() 和 Object.wait()有什么区别?
这些问题的答案, 你在源码里都能找得到。我们直接来看源码:
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds, subject to
* the precision and accuracy of system timers and schedulers. The thread
* does not lose ownership of any monitors.
*
* @param millis
* the length of time to sleep in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static native void sleep(long millis) throws InterruptedException;
可见, sleep方法也是一个静态方法, 并且是native方法, 从注释Causes the currently executing thread to sleep中可以看出, 它作用于当前正在执行的线程, 所以上面那个问题我们就能回答了:
Thread.sleep() 与 Thread.currentThread().sleep() 没有区别
如果硬要说他们有什么区别的话, 那就是一个是用类直接调用静态方法, 一个是用类的实例调用静态方法.
另外, 上面的注释中还有一句非常重要的话:
The thread does not lose ownership of any monitors.
也就是说, 虽然sleep函数使当前线程让出了CPU, 但是, 当前线程仍然持有它所获得的监视器锁, 这与同时让出CPU资源和监视器锁资源的wait方法是不一样的。
sleep方法还有另外一个版本:
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds plus the specified
* number of nanoseconds, subject to the precision and accuracy of system
* timers and schedulers. The thread does not lose ownership of any
* monitors.
*
* @param millis
* the length of time to sleep in milliseconds
*
* @param nanos
* {@code 0-999999} additional nanoseconds to sleep
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative, or the value of
* {@code nanos} is not in the range {@code 0-999999}
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static void sleep(long millis, int nanos) throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
这个方法多加了纳秒级别的延时参数, 但是我们看源码就知道, 这个多加的纳秒级别的延时并没有什么用, 最终该函数还是调用了上面的单参数native sleep方法, 延时还是毫秒级别的, 多出来的参数最多是让当前毫秒级别的延时增加1毫秒.
还记得我们上次讲的wait方法吗?我们来对比下:
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
怎么样?是不是很像?两者只不过在从纳秒向毫秒的进位处有细微的差别,我猜这个不统一是历史原因导致的。
另外,值得一提的是,wait有无参的wait()方法,它调用的是wait(0),表示无限期等待,而sleep并没有无参数的版本,那么sleep(0)代表什么呢?
这一点在源码里面并没有提及,但是通过猜测sleep方法的定义我们知道,它是让出CPU 0毫秒,这听上去好像没有什么意义,但其实调用Thread.sleep(0)的当前线程确实被“冻结”了一下,让其他线程有机会优先执行。也就是说当前线程会释放一些未用完的时间片给其他线程或进程使用,就相当于一个让位动作,这看上去就和下面要说的yield方法很像了。
yield
既然上面谈到了sleep(0)方法, 就不得不提yield方法了:
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* <p> Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
* <p> It is rarely appropriate to use this method. It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();
yield方法也是一个native方法, 从它的注释可以看出A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint. 它对于CPU只是一个建议, 告诉CPU, 当前线程愿意让出CPU给其他线程使用, 至于CPU采不采纳, 取决于不同厂商的行为, 有可能一个线程刚yield出CPU, 然后又立马获得了CPU。与之相对, sleep方法一定会让出CPU资源, 并且休眠指定的时间, 不参与CPU的竞争.
所以调用yield方法不会使线程退出RUNNANLE状态,顶多会使线程从running 变成 ready,
但是sleep方法是有可能将线程状态转换成TIMED_WAITING的。、
isAlive
isAlive方法用于检查线程是否还活着,它是一个native方法,但不是静态方法,也就是说它必须被线程的实例所调用。
其实大家可以思考一下它为什么不是静态方法,因为静态方法一般都是作用于当前正在执行的线程,既然是“当前正在执行”,那必然是Alive的,所以作为静态方法调用并没有意义。
/**
* Tests if this thread is alive. A thread is alive if it has
* been started and has not yet died.
*
* @return <code>true</code> if this thread is alive;
* <code>false</code> otherwise.
*/
public final native boolean isAlive();
join
join方法是另一个能将线程状态转换成WAITING或者TIMED_WAITING的,它和wait方法一样,有三个版本,我们一个个来看:
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 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);
now = System.currentTimeMillis() - base;
}
}
}
这段源码注释的开头部分就告诉了我们join方法的作用:
Waits at most {@code millis} milliseconds for this thread to die. A timeout of {@code 0} means to wait forever.
也就是说,该方法等待this thread终止,最多等指定的时间,如果指定时间为0,则一直等。
这里有两个问题需要弄清楚:
- 谁在等
this thread终止? this thread指的是哪个线程?
为了便于说明,我们直接来看一个例子:
public class JoinMethodTest {
private static void printWithThread(String content) {
System.out.println("[" + Thread.currentThread().getName() + "线程]: " + content);
}
public static void main(String[] args) {
printWithThread("开始执行main方法");
Thread myThread = new Thread(() -> {
printWithThread("我在自定义的线程的run方法里");
printWithThread("我马上要休息1秒钟, 并让出CPU给别的线程使用.");
try {
Thread.sleep(1000);
printWithThread("已经休息了1秒, 又重新获得了CPU");
printWithThread("我休息好了, 马上就退出了");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
try {
myThread.start();
printWithThread("我在main方法里面, 我要等下面这个线程执行完了才能继续往下执行.");
myThread.join();
printWithThread("我在main方法里面, 马上就要退出了.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上面的例子中,我们在main方法中调用了 myThread.join(),注意上面这段代码有两个线程,一个是执行main方法的线程,一个是我们自定义的myThread线程,所以上面的两个问题的答案是:
- main线程在等
this thread的终止,因为我们在main方法中调用了myThread.join() this thread线程指的是myThread线程,因为我们在myThread对象上调用了join方法。
上面这段代码的执行结果为:
[main线程]: 开始执行main方法
[main线程]: 我在main方法里面, 我要等下面这个线程执行完了才能继续往下执行.
[Thread-0线程]: 我在自定义的线程的run方法里
[Thread-0线程]: 我马上要休息1秒钟, 并让出CPU给别的线程使用.
[Thread-0线程]: 已经休息了1秒, 又重新获得了CPU
[Thread-0线程]: 我休息好了, 马上就退出了
[main线程]: 我在main方法里面, 马上就要退出了.
从运行结果可以看出,虽然myThread线程(即Thread-0线程)中途让出了CPU, main线程还是必须等到其执行完毕了才能继续往下执行,我们现在修改一下代码,让main线程最多等0.5秒,即将myThread.join()改为myThread.join(500);,则结果如下:
[main线程]: 开始执行main方法
[main线程]: 我在main方法里面, 我要等下面这个线程执行完了才能继续往下执行.
[Thread-0线程]: 我在自定义的线程的run方法里
[Thread-0线程]: 我马上要休息1秒钟, 并让出CPU给别的线程使用.
[main线程]: 我在main方法里面, 马上就要退出了.
[Thread-0线程]: 已经休息了1秒, 又重新获得了CPU
[Thread-0线程]: 我休息好了, 马上就退出了
我们看到,由于main线程最多等待myThread 0.5秒,在myThread休眠的一秒内,它就不等了,继续往下执行,而随后myThread抢占到CPU资源继续运行。
通过列子有了感性的认识后,我们再来看源码,首先看join(0)部分:
public final synchronized void join(long millis) throws InterruptedException {
...
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
...
}
...
}
这是一个自旋操作,注意,这里的isAlive和wait(0)方法都是线程实例的方法,在上面的例子中就是myThread的方法,Thread虽然是一个线程类,但只是特殊在它的native方法上,除此之外,它就是个普通的java类,而java中所有的类都继承自Object类,所以Thread类继承了Object的wait方法,myThread作为线程类的实例,自然也有wait方法。
我们之前说wait方法的时候提到过,执行wait方法必须拿到监视器锁,并且必须在同步代码块中调用,这里我们检查join方法发现,它确实被synchronized关键字修饰,并且是一个非静态方法,所以它使用的是当前对象实例的监视器锁(this)。
好像开始复杂了,我们从头到尾捋一捋(注意了!敲黑板了!这段比较绕! ):
- 首先我们要明确,这里牵涉到两个线程,一个是main线程,一个是我们自定义的myThread线程(即例子里的Thread-0)。
- 我们在main方法中调用了
myThread.join(),main方法由main线程执行,所以执行myThread.join()这行代码的“当前线程”是main线程。 - join方法是一个同步方法,使用的是对象锁(this 锁),即myThread对象所关联的监视器对象。
- main线程必须首先拿到join方法的监视器锁才能进入同步代码块。
- main线程进入同步代码块后会首先检查
myThread线程是否还存活,注意,这里的isAlive是myThread线程的方法,它是检查myThread线程是否还活着,而不是当前线程(当前线程是执行isAlive方法的线程,即main线程)。 - 如果myThread线程还存活,(main线程)就无限期等待,并让出监视器锁,进入
WAITING状态。 - 当main线程从
WAITING状态被唤醒后(通过notify,notifyAll或者是假唤醒), 将继续竞争监视器锁,当成功获得监视器锁后,他将从调用wait的地方恢复,继续运行。由于wait方法在while循环中,则它将继续检查myThread线程是否存活,如果还是没有终止,则继续挂起等待。 - 可以看出,退出这个“自旋”状态的唯一途径就是
myThread线程终止运行(或者有中断异常抛出)。
有的细心的同学可能就要问了: 要是没有人调用notify或者notifyAll,也没有假唤醒状态的发生,那main线程不就一直被wait(0)方法挂起了吗?这样以来不就连检测myThread线程是否存活的机会都没有吗?这样即使myThread终止了,也无法退出啊。
关于这一点,注释中其实是做了解释的:
As a thread terminates the {@code this.notifyAll} method is invoked.
我们知道,wait(0)方法的监视器锁就是myThread对象(this), 而当myThread终止执行时,this.notifyAll会被调用,所以所有等待this锁的线程都会被唤醒,而main线程就是等待在这个监视器锁上的线程,因此myThread运行结束时,main线程会从wait方法处被唤醒。
另外,注释中还多加了一句:
It is recommended that applications not use {@code wait}, {@code notify}, or {@code notifyAll} on {@code Thread} instances.
这个推荐还是很有必要的,至于为什么,就给大家留作思考题吧<( ̄︶ ̄)>
不过我这里再啰嗦一句,一定要分清执行代码的线程和方法所属的线程类所代表的线程!
例如,在上面的例子中:
myThread.join()是myThread对象的方法,但是执行这个方法的是main线程;isAlive()是myThread对象的方法,但是执行这个方法的是main线程,而这个方法检测是myThread线程是否活着wait(0)是myThread对象的方法,但是执行这个方法的是main线程,它使得main线程挂起,但是main线程是在myThread对象代表的monitor上挂起。
这里最重要的是区分“myThread对象”和“myThread线程” ,myThread对象有时候代表了myThread线程,例如myThread对象的isAlive方法,检测的就是它代表的myThread线程是否活着,但是其实大多数时候,myThread对象就是普通的java对象,这个对象的方法通常也都是由其他线程(例如上面例子中的main线程)来执行的,对于我们自定义的线程来说(例如上面的myThread线程),通常由它自己执行的方法就只有传进入的run方法了。
再回到上面的例子,从上面的分析中可以看出,join(0)方法实现了一定程度上的线程同步,即当前线程只有等join方法所属的线程对象所代表的线程终止执行了才能继续往下执行,否则将一直挂起等待。
这一点也说明使用join(0)是很危险的,因为如果myThread线程因为得不到资源一直被挂起,而main线程又在等待myThread线程终止,则程序永远会停在那里,无法终止,所以源码中提供了限时等待的版本:
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
...
if (millis == 0) {
...
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
与无限期等待不同的是,限时等待只等待指定时间,如果指定的时间到了就直接从循环中跳出来,使用的wai方法也是限时wait的版本,定时时间到了之后,main线程会被自动唤醒。上面的代码是自解释的,我就不再赘述了。
接下来我们再来看看其他两个版本的join方法:
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis, int nanos) throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
join(millis);
}
可见,其他两个版本最终调用的都是我们分析的第一版本,这和wait方法,sleep方法很像,至于为什么wait方法和join方法都提供了无参方法而sleep方法没有,我个人认为是为了保持语义的一致性:
wait()和join()分别和wait(0)和join(0)等价,他们都代表了无限期等待,而sleep(0)并不代表无限期等待,所以sleep方法没有无参的形式,以防止语义上的混乱。除这点之外,这三个方法在两个参数的版本XXX(long millis, int nanos)中的实现,都大同小异。
另外最后一点值得注意的是,我们在join方法中只调用了isAlive方法检测线程是否存活,并没有启动这个线程,也就是说,如果我们想要实现当前线程等待myThread线程执行完成之后再执行的效果,就必须在调用myThread.join()之前调用myThread.start()让线程先跑起来,否则join方法发现isAlive为false会立即退出,myThread线程就不会被执行,大家可以将myThread.start()注释掉自己跑一跑试试看。
(完)