最近参加了一家公司的面试,不知道为啥如今公司面试都喜欢安排在下午2点,应该是他们刚刚午休结束吧,没办法只能牺牲本身的午休时间,好不容易通过一个多小时的地铁终于到了目标公司,人事的小姑娘直接把我领到会议室给了一个笔试卷子就撤了,那就开始作题目吧。java
第一题是关于线程并发的,直接就让我犯了迷糊:面试
// 写出这段程序的最后输出结果 public class ThreadJoinTest { public static void main(String[] args) throws Exception { // TODO Auto-generated method stub // 定义两个锁对象。 Object lock1 = new Object(); Object lock2 = new Object(); Thread thread1 = new Thread() { @Override public void run() { // 开始线程主体以前先获取锁对象 'lock1' 。 synchronized(lock1) { // 打印线程开始执行信息。 System.out.println("thread1 start"); try { // 休眠一分钟,模拟耗时任务。 Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } // 获取锁对象 'lock2',注意此时线程仍然持有锁 'lock1', // 也就是说线程是在持有锁'lock1'的前提下尝试获取锁对象'lock2'。 synchronized(lock2) { System.out.println("thread1 end"); } // 线程释放锁对象'lock2'。 } // 线程释放锁对象'lock1'。 } }; Thread thread2 = new Thread() { @Override public void run() { // 开始线程主体以前先获取锁对象'lock2'。 synchronized(lock2) { // 打印线程开始执行信息。 System.out.println("thread2 start"); try { // 休眠一分钟,模拟耗时任务。 Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } // 在线程持有锁对象'lock2'的状况下尝试获取锁对象'lock1'。 synchronized(lock1) { System.out.println("thread2 end"); } // 释放锁对象'lock1'。 } // 释放锁对象'lock2'。 } }; // 启动线程 thread1.start(); thread2.start(); thread1.join(); thread2.join(); // 主线程休眠两分钟,模拟耗时任务。 Thread.sleep(2000); // 打印主线程结束信息。 System.out.println("main thread end"); } } 复制代码
我看到题目的第一印象觉得考察的线程并发和死锁问题,本身认为程序的执行过程以下:bash
thread1
先启动执行并获取了锁对象lock1
,这时会直接打印thread1 start
,而后休眠了1分钟来模拟耗时任务;thread2
也已经启动并获取了锁对象lock2
,这时会直接打印thread2 start
,而后休眠了1分钟来模拟耗时任务;thread1
休眠结束准备继续往下执行,须要获取锁对象lock2
,因为这时线程thread2
持有了锁lock2
,因此线程thread1
因为没法获取锁对象处于阻塞状态;thread2
休眠结束准备继续往下执行,须要获取锁对象lock1
,因为这是线程thread1
持有了锁lock1
,因此线程thread2
因为没法获取锁对象处于阻塞状态;thread1
持有lock1
等待lock2
,与此同时thread2
持有lock2
等待lock1
,明显处于死锁状态,因此这两个线程谁也没法继续向下执行;thread1
和thread2
处于死锁状态,都没法继续向下执行,那主线程就会得到执行的机会,进而打印main thread end
。综上,我最后给出的结果是:markdown
thread1 start
thread2 start
main thread end
复制代码
在和面试官当面交流的时候,特地谈到这个题目,面试官说这道题主要考察的是join
的用法,显然我对这个没有正确的认识,最后给的答案天然也是错误的。并发
既然花了一下午的时间去参加了面试,至少要有一点点的收获吧,否则岂不是在浪费生命,因此回来仍是稍微查了下Thread.join()
的含义和用法,咱们直接来看下源码里对这个方法的描述。app
// Thread.java /** * Waits for this thread to die. * * <p> An invocation of this method behaves in exactly the same * way as the invocation * * <blockquote> * {@linkplain #join(long) join}{@code (0)} * </blockquote> * * @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 void join() throws InterruptedException { join(0); } 复制代码
源码里对这个方法的描述只有简单的一句话“等待这个线程的消亡”,也就说一个线程在调用另外一个线程的join
方法后就要等待这个线程消亡后才能继续往下执行,至关于把并发的线程在这个时间点变成串行执行序列了。ide
在理解了这点后,再回过头来看看上面的题目,在thread1
和thread2
的死锁等待方面的分析都是正确的,关键点在于主线程在这以后是否还能够继续往下执行。因为在主线程中调用了thread1.join()
和thread2.join()
,就代表主线程必须等待这两个线程执行完才能继续执行,但thread1
和thread2
已经处于死锁状态,是不可能消亡的,这也就致使主线程没法继续下去了,因此最后的输出结果应该是:函数
thread1 start
thread2 start
复制代码
我本身也在回来以后运行过这段代码,结果和分析的一致,也算弄明白了Thread.join()
是咋回事了。oop
在弄明白Thread.join()
的用法和含义是否是就圆满结束了?固然不是,咱们尽量地了解其内部的实现原理。学习
简单来讲就是要知道两个问题:
Thread.join()
以后中止执行,直到另外一个线程消亡的?一切来源于代码,咱们天然要到代码去寻找答案,仍是再来看下Thread.join()
的声明和定义:
// Thread.java /** * Waits for this thread to die. */ public final void join() throws InterruptedException { // 直接调用另外一个重载函数。 join(0); } /** * 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. * */ 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()) { // 等待时间没有直接使用参数指定的 millis,缘由是为了保持退出循环的可能。 long delay = millis - now; if (delay <= 0) { break; } // 当线程还处于存活状态时,就等待一段时间。 wait(delay); // 更新 now 时间信息,是为了等待时间结束后,再次进到这个循环时可以因为 delay <= 0 而直接退出循环。 now = System.currentTimeMillis() - base; } } } 复制代码
这个函数的代码量并不大,逻辑也比较容易理解,就是在线程A中调用线程B的join()
方法后,这个线程A就会处于对线程B的wait
状态,根据传入的参数不一样能够处于一直等待也能够只等待一段时间。
既然线程A在调用线程B的join
方法后就会处于wait
状态,那线程A又是在什么时候恢复执行的呢?这里只介绍不带参数的join
方法,即一直等待的状况。从join
方法的介绍中可知,要等到线程B的消亡,线程A才能恢复,这是如何实现的呢?
// Thread.java /** * This method is called by the system to give a Thread * a chance to clean up before it actually exits. */ 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
方法来进行一些回收操做,从代码能够看到除了group.threadTerminated()
以外都是一些置空操做,极可能起到恢复做用的逻辑就藏在group.threadTerminated()
里面,这里的group
是ThreadGroup
的实例,是线程在初始化的时候建立的,能够简单理解为这个线程属于这类线程组的。
直接来看ThreadGroup.threadTerminated()
的代码:
/** * Notifies the group that the thread {@code t} has terminated. * * <p> Destroy the group if all of the following conditions are * true: this is a daemon thread group; there are no more alive * or unstarted threads in the group; there are no subgroups in * this thread group. * * @param t * the Thread that has terminated */ void threadTerminated(Thread t) { synchronized (this) { remove(t); if (nthreads == 0) { // 唤醒全部的等待线程。 notifyAll(); } if (daemon && (nthreads == 0) && (nUnstartedThreads == 0) && (ngroups == 0)) { destroy(); } } } 复制代码
很明显,在线程被销毁的时候会调用notifyAll()
来唤醒全部等待线程,因此线程A才能在线程B消亡的时候恢复运行。
其实Thread
里面除了join()
方法,还有一个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(); 复制代码
这个方法是个native
方法,咱们没法直接看到它的内部实现,那就看下它的声明,里面提到两点重要信息:
直接给出一个简单的例子:
public class ThreadYieldTest { public static void main(String[] args) { // TODO Auto-generated method stub YieldThread thread1 = new YieldThread("thread_1"); YieldThread thread2 = new YieldThread("thread_2"); thread1.start(); thread2.start(); } private static class YieldThread extends Thread { private String name; public YieldThread(String name) { super(name); this.name = name; } @Override public void run() { for (int i = 0; i < 50; i ++) { System.out.println(name + " : " + i); if (i == 25) { // 在线程执行到一半的时候,调用 yield 方法尝试放弃执行。 System.out.println(name + ": yield"); Thread.yield(); } } } } } 复制代码
有兴趣的同窗能够屡次运行这段程序看看结果,从结果也能够发现并非每次thread1
或者thread2
在执行yield
后另外一个线程均可以获取处理器进而开始执行的,正是因为这个不肯定性,不建议你们在代码里用这个方法来控制线程之间的执行。
经过一个简单的面试题,可以学习到一点知识,对本身也是一种提高,感受本身平时对这些基础问题的思考太少了,在埋头解决bug
之余,仍是要注意学习积累知识的。