一、学习自
https://www.zhihu.com/question/25532384
https://www.zhihu.com/question/19901763
http://ifeve.com/benefits/
http://www.cnblogs.com/xrq730/category/733883.html
http://www.cnblogs.com/skywang12345/p/java_threads_category.html
http://www.cnblogs.com/Qian123/p/5670304.html
https://blog.csdn.net/ghsau/article/details/17609747
https://www.cnblogs.com/lcplcpjava/p/6896904.html
二、相关基础概念(以自问自答形式)
目录
问题1.
初学java的人肯定经常听到多线程这个词,尤其是不懂得人听起来觉得,哇,好牛逼!多线程应该就是多个线程了,而线程是什么呢?相信很多人同时也听过进程这个词?那么进程与线程从名字上看我们就应该知道,这俩不是一个东西。但是都带了个“程”字,他俩是不是亲戚啊?
关于进程与线程的讨论,可以看第一个链接,知乎上不少大神做出了专业的解释。给个段子理解下:
进程是爹妈,管着众多的线程儿子
问题2.
这时候,我们应该对进程和线程有了一些基础的理解。那么平常听到的多线程是多个线程一起同时进行吗?
答案是否定的!一个处理器(cpu)在某一个时间点上永远都只能是一个线程!假设当前有一个cpu叫A,A中有a,b,c三个线程。多线程其实就是A在执行a线程一段时间后保存a线程执行的进度,然后去执行b或者c线程,然后又跳回来继续执行a线程,也就是说A在a,b,c三个线程中来回执行,并不是先把a线程完全执行结束才开始执行其他线程的,当然,比喻而已。
问题3.
为什么需要多线程?
可以看文章开头第二个链接,知乎上第一个答案很精辟。也可以看看第三个链接。
总结下好处:资源利用率更好,程序响应更快,程序设计在某些情况下更简单等。
问题4.回到目录
如何写一个线程启动的示例,让它具象化?
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class TestThread2 { public static void main(String[] args) { Thread thread = new Thread(); thread.start(); System.out.println(thread); System.out.println(thread.getName()); System.out.println(thread.getState()); System.out.println(thread.currentThread()); System.out.println(thread.currentThread().getName()); System.out.println(thread.activeCount()); try { thread.join(); thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(thread.isAlive()); thread.run(); thread.stop(); thread.yield(); thread.interrupt(); } }
|
问题5.回到目录
上述示例中列出了很多常用的方法,那么这些方法分别代表着什么意思呢?为了解决这个问题,我又需要去看源码了。
首先,Thread
类实现了Runnable
接口,点开一看,Runnable
接口里就一个run()
方法,注释的意思是当一个实现了Runnable
接口的对象创建了一个线程,在一个单独执行的线程中启动线程会调用run
方法。
1
| public abstract void run();
|
接下来回到Thread
类中,不看不知道,一看吓一跳,Thread
类里面竟然有9个构造函数!我直接列出我用到的也是最简单的默认构造函数:
1 2 3 4 5 6 7
| public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); } private static int threadInitNumber; private static synchronized int nextThreadNum() { return threadInitNumber++; }
|
init()/回到目录
默认构造函数内部调用了init()
方法,看看这个初始化方法内部实现了什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null); } private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) { 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(); * Do we have the required permissions? */ 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 (parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); this.stackSize = stackSize; tid = nextThreadID(); }
|
run和satrt源码及注释/回到目录
到这里,实例化才完成。。。是不是很复杂?没办法,我们还得继续。根据示例,接下来我调用了start()
方法来启动线程。可是这里存在一个疑问:Thread
实现了Runnable
接口,而Runnable
接口明确说明启动线程必须要有run
方法!Thread
里的确实现了run
方法,那么为什么不是直接调用run()
启动线程而是调用start()
呢?
这时候,我得分别去看这两个方法的源码以及注释了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| * 先列出run方法的源码 * 官方注释:如果一个线程使用单独的 Runnable 运行对象构成的,那么Runnable对象的run方法会被调用。否则,这个方法不会做任何事。 */ @Override public void run() { if (target != null) { target.run(); } } * run 方法的注释没有明确说要通过直接调用该方法才能启动线程,注释的潜台词是线程运行时自动调用run方法??? * 还是先看看start方法源码以及注释再做判断吧 * start方法注释第一句就是 该方法让线程开始执行,java 虚拟机调用该线程的run方法!!! * 调用该方法的后果是两个线程同时运行。 * 一个线程一旦被执行就不能重启。 */ public synchronized void start() { * 该方法不被主线程以及 “system” 线程组通过 VM 创建或设置来调用 * 0 对应状态 NEW */ if (threadStatus != 0) throw new IllegalThreadStateException(); * 通知线程组这个线程即将启动,这个线程可以被加进该组的线程列表并且线程组未启动的数量相应减少 * 我把add()列到下面 */ group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } * 我刚刚说 start0 这个方法很奇怪,就是因为没有方法体,不知道它是怎么实现的。 * 但是,仔细观察方法修饰符中有一个 native,特地上网百度了下才知道: * native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。 * Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。 * JNI是Java本机接口(Java Native Interface),是一个本机编程接口,它是Java软件开发工具箱(java Software Development Kit, SDK)的一部分。 * JNI允许Java代码使用以其他语言编写的代码和代码库。Invocation API(JNI的一部分)可以用来将Java虚拟机(JVM)嵌入到本机应用程序中, * 从而允许程序员从本机代码内部调用Java代码。 */ private native void start0(); void add(Thread t) { synchronized (this) { if (destroyed) { throw new IllegalThreadStateException(); } if (threads == null) { threads = new Thread[4]; } else if (nthreads == threads.length) { threads = Arrays.copyOf(threads, nthreads * 2); } threads[nthreads] = t; nthreads++; nUnstartedThreads--; } }
|
可算是把start
和run
两个方法的实现和注释看完了,现在对于线程为什么需要用start()
来启动应该了解了吧。这个时候,不妨看看文章开头的链接以及网上前辈们的讲解做对比,看有没有遗漏或者是否真的懂。幸亏留个心眼对比了下,还真的发现有点出入,还是自己理解的不足。
start()
会启动一个新线程,并在新线程中运行run()
方法。这个我们都知道,但是:
直接调用run()
,则会直接在当前线程中运行run()
方法,并不会启动一个新线程来运行run()
。
看来,我的英语翻译水平不够好,run()
的注释翻译时就没有理解一个独立的Runnable
对象是什么意思,下意识的认为必须是一个新线程,其实不是的,在当前线程调用run()
也是可以的,只是不会启动新线程。
run和start示例/回到目录
针对run()
直接调用还是通过start()
调用写个小例子进行对比:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class TestRunStart extends Thread { public static void main(String[] args) { TestRunStart testRunStart = new TestRunStart(); testRunStart.run(); testRunStart.start(); } @Override public void run() { System.out.println(Thread.currentThread().getName() + " is running!"); } } main is running! Thread-0 is running!
|
这个例子很明显的反映出start()
与run()
的区别了。直接调用run()
其实就是调用main
这个主线程的run()
方法而已!
getName()和Thread.currentThread().getName()/回到目录
紧接着,又有一个比较奇怪的点了。Thread
类里定义了getName()
方法,而我看到网上案例打印线程名都是用的Thread.currentThread().getName()
,这两者有区别吗?
其实在实例化时,init()
方法已经对name
进行了赋值,而getName()
方法就是返回name
值,而这个name
指的就是实例化的尚未启动的线程名。
而Thread.currentThread().getName()
从名字上来看也知道,这里得到的其实是当前正在执行这段代码的线程名!!!
另外,currentThread()
也是native
修饰的,看不到它的实现代码。
activeCount()
用来返回当前活跃线程的大致数量的,这里就不赘述了。
join()/回到目录
紧跟示例步骤,我们到达了join()
方法,这方法干嘛的呢?看看源码和注释吧!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public final void join() throws InterruptedException { join(0); } * 这个方法核心部分是调用了wait()方法,没大看懂什么意思,就为了等待? * 看看wait()方法吧 */ 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; } } } public final native void wait(long timeout) throws InterruptedException;
|
关于join()
这篇博客讲的不错:https://www.cnblogs.com/lcplcpjava/p/6896904.html。总结下:
join
方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。比如a,b两个线程都要start
时,在a.start()
之后调用a.join()
,然后才调用b.start()
,b线程会等a线程执行一段时间才会开始执行,具体等待时间看传入的值。
join
方法必须在线程调用start
方法后调用才有效。
join(0)
并不是等待0秒,而是无线等待至上一个线程执行结束后才开始执行下一个线程。
wait(),notify(),notifyAll()/回到目录
虽然看不了wait()
方法的源码实现,但是还是有必要了解一下它干什么用的?
wait()
的作用是让当前线程进入等待状态,同时,wait()
也会让当前线程释放它所持有的锁。而notify()
和notifyAll()
的作用,则是唤醒当前对象上的等待线程;notify()
是唤醒单个线程,而notifyAll()
是唤醒所有的线程。
在调用wait()
之前,线程必须获得该对象的锁,因此只能在同步方法/同步代码块中调用wait()
方法。
wait(0)
:没有指定时间,会一直等待下去直到被唤醒或者打断
wait(1000)
:1S后自动超时唤醒
上面还提到了notify()
和notifyAll()
两个方法,这两方法配合wait()
使用,那么顺便把这两个方法也研究下。
notify()
:唤醒在此对象监视器上等待的单个线程。如果同时有多个线程在等待,那么该方法会唤醒其中一个线程。
notifyAll()
:唤醒在此对象监视器上等待的所有线程。
- 和
wait()
一样,notify()
和notifyAll()
也要在同步方法/同步代码块中调用。
下面列出notify()
的注释翻译,蛮长的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| * 唤醒正在该对象监视器上等待的单个线程。如果该对象上有多个线程在等待,其中一个会被唤醒。 * 对于多个线程等待唤醒的情况下,会任意选择一个唤醒。 * 调用任何一种 wait 方法都能使对象监视器上的一个线程进入等待状态。 * 被唤醒的线程不能继续运行直到当前线程交出对象上的锁。 * 被唤醒的线程将会和该对象上其它活跃的线程竞争对象锁。 * 举个例子,唤醒线程在下一个线程锁住该对象时没有可靠的优劣势。 * 该方法只能被拥有当前对象监视器的线程调用。 * 线程成为对象监视器的主人只有三种方法: * 1.通过执行该对象 synchronized 实例方法 * 2.通过执行该对象 synchronized 代码块 * 3.对象是类的话,通过执行类的 synchronized 静态方法 * * 一段时间内只有一个线程能拥有对象的监视器。 */ public final native void notify();
|
我们看完源码后肯定在想,怎么用?他三怎么配合?来个案例吧,直观点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| public class TestWaitNotify { public static void main(String[] args) { ThreadA threadA = new ThreadA("threadA"); ThreadB threadB = new ThreadB("threadB"); threadA.start(); synchronized (threadA) { try { long start = System.currentTimeMillis(); threadA.wait(3000); long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + " wait " + (end - start)); } catch (InterruptedException e) { e.printStackTrace(); } } threadB.start(); } } class ThreadA extends Thread { public ThreadA(String name) { super(name); } public void run() { synchronized (this) { System.out.println(Thread.currentThread().getName() + " is running..."); try { long start = System.currentTimeMillis(); wait(5000); long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + " wait " + (end - start)); } catch (InterruptedException e) { e.printStackTrace(); } } } } class ThreadB extends Thread { public ThreadB(String name) { super(name); } public void run() { synchronized (this) { System.out.println(Thread.currentThread().getName() + " is running..."); long start = System.currentTimeMillis(); notify(); long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + " notify " + (end - start)); } } } threadA is running... main wait 3001 threadB is running... threadB notify 0 threadA wait 5000
|
关于这个示例的打印输出我不是很理解。仔细看我的代码,我分别在ThreadA
和ThreadB
两个类内的run
方法中调用了wait()
和notify()
方法,令我不解的是,为什么当threadA.start()
后,主线程等待了3s,threadB
线程居然在执行!!!
不是说wait()
方法会暂停线程运行且其他线程要等暂停的线程执行完成后才能执行吗?(join()
方法就是这样的效果)。ThreadA
里的run()
方法明明还调用了wait(5000)
啊,不应该等5s过后,threadB
线程才能开始执行啊?为什么threadA
暂停运行时主线程以及threadB
线程依然能运行呢???
这里,我似乎走误了一个误区!去上面仔细看看join()
的示例,明明是两个线程都在main()
里面调用,中间夹杂着join()
,这不就跟我这里的示例中在main()
里面threadB.start()
之前调用了wait(3000)
一样的结果吗!!!的确是等待了3s后才开始执行下面的啊!
但是!我不理解为什么不是等待5s去执行threadB.start()
而是等待了3s就执行!我明明有在ThreadA.class
里的run()
方法中调用了wait(5000)
,怎么没有等我threadA
执行完再去执行threadB
?难道threadA
没有霸占住 cpu 吗?notify()
起作用了吗?把上面的例子改一下对照着看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| synchronized (threadA) { try { long start = System.currentTimeMillis(); threadA.wait(3000); long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + " wait " + (end - start)); threadA.notify(); } catch (InterruptedException e) { e.printStackTrace(); } } threadA is running... main wait 3000 threadA wait 3000 threadB is running... threadB notify 0
|
对比上一个例子发现有明显变化,ThreadA.class
中的wait(5000)
并没有起作用,这是因为在main()
方法里调用了threadA.notify()
强行唤醒了threadA
线程。
而对于上一个例子中主线程等待3S后,没有等待threadA
线程执行完后,而是直接执行threadB
线程的问题。很可能因为在ThreadA.class
中调用的wait(5000)
只是让threadA
线程等待而不影响主线程。但是在main()
里面调用wait(3000)
实际上同时让主线程也等待了3S,此时主线程相当于进入“阻塞”状态。3S后主线程继续运行。
可以参考:http://www.cnblogs.com/skywang12345/p/3479224.html
一个问题,在ThreadB.class
中调用的notify()
貌似没有对threadA
线程起作用,并没有唤醒它,这是为什么呢?
很简单,ThreadB.class
中run()
方法内synchronized
代码块其实绑定的是threadB
线程,根本就跟threadA
无关呀,它获取不到threadA
的同步锁,就没法唤醒threadA
线程啊~
问题又来了,如何通过threadB.notify()
去唤醒threadA
线程?看下面我粗糙的改写代码吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public static void main(String[] args) { ThreadA threadA = new ThreadA("threadA"); ThreadB threadB = new ThreadB(threadA); threadA.start(); synchronized (threadA) { try { long start = System.currentTimeMillis(); threadA.wait(3000); long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + " wait " + (end - start)); threadB.start(); } catch (InterruptedException e) { e.printStackTrace(); } } } class ThreadB extends Thread { ThreadA threadA; public ThreadB(ThreadA threadA) { super(threadA); this.threadA = threadA; } public void run() { synchronized (threadA) { System.out.println(Thread.currentThread().getName() + " is running..."); long start = System.currentTimeMillis(); threadA.notify(); long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + " notify " + (end - start)); } } } threadA is running... main wait 3000 Thread-0 is running... Thread-0 notify 0 threadA wait 3001
|
为什么wait(),notify(),notifyAll()在 Object.class 中/回到目录
注意:wait(),notify(),notifyAll()
是Object.class里面的方法!!!
问题来了,为什么把该方法定义在 Object.class内而不是Thread.class内呢?
参考:http://www.cnblogs.com/skywang12345/p/3479224.html
重点结论:notify()
, wait()
依赖于“同步锁”,而“同步锁”是对象锁持有,并且每个对象有且仅有一个!这就是为什么notify()
, wait()
等函数定义在Object
类,而不是Thread
类中的原因。
sleep()/回到目录
可以看:http://www.cnblogs.com/skywang12345/p/3479256.html
1 2 3 4 5 6
| * 让当前执行的线程休眠,暂时停止运行指定的毫秒数。 * 受系统定时器和调度器精度的影响。 * 线程不会丢失任何监视器的所有权。和wait()不同,wait()会让出所有权 */ public static native void sleep(long millis) throws InterruptedException;
|
一个小例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class TestSleep { public static void main(String[] args) { ThreadC threadC = new ThreadC("threadC"); threadC.start(); } } class ThreadC extends Thread { public ThreadC(String name) { super(name); } public synchronized void run() { try { for(int i = 0; i < 10; i++) { System.out.printf("%s: %d\n", this.getName(), i); if(i % 4 == 0) { Thread.sleep(2000); } } } catch (InterruptedException e) { e.printStackTrace(); } } }
|
yield()/回到目录
1 2 3 4 5 6 7
| * 提示调度器当前线程能够做出让步,但是调度器可以忽略这个提示。 * 让步是一种尝试,以改善线程之间的相对进展,否则将过度利用CPU。 * 不怎么用,很少合适 * 在设计并发控制结构时,也可能是有用的,例如在 java.util.concurrent.locks 包 */ public static native void yield();
|
一个使用例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class TestYield { public static void main(String[] args) { ThreadD threadD = new ThreadD("threadD"); ThreadD threadD2 = new ThreadD("threadD2"); threadD.start(); threadD2.start(); } } class ThreadD extends Thread { public ThreadD(String name) { super(name); } public synchronized void run() { for(int i = 0; i < 5; i++) { System.out.printf("%s [%d]:%d\n", this.getName(), this.getPriority(), i); if(i % 2 == 0) { Thread.yield(); } } } } threadD2 [5]:0 threadD [5]:0 threadD2 [5]:1 threadD2 [5]:2 threadD [5]:1 threadD [5]:2 threadD2 [5]:3 threadD2 [5]:4 threadD [5]:3 threadD [5]:4
|
yield()
与wait()
区别:
wait()
是让线程由“运行状态”进入到“等待(阻塞)状态”,而yield()
是让线程由“运行状态”进入到“就绪状态”。
wait()
是会线程释放它所持有对象的同步锁,而yield()
方法不会释放锁。
interrupt()/回到目录
细看:http://www.cnblogs.com/xrq730/p/4856361.html 和 http://www.cnblogs.com/skywang12345/p/3479949.html
这个方法是Thread.class
里面的,源码也只露出了一丢丢,但是看网上的讲解,基本上内容还是蛮多的。继续套路,贴代码和注释翻译:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| private volatile Interruptible blocker; private final Object blockerLock = new Object(); * 中断线程自己,总是允许的。 * 如果线程调用了 wait,join,sleep方法进入阻塞状态,然后调用该方法会报 InterruptedException * 如果线程在 java.nio.channels.InterruptibleChannel 上操作 I/O 被阻塞,通道会被关闭, * 设置线程的中断状态并且线程会收到 java.nio.channels.ClosedByInterruptException * 如果线程在 java.nio.channels.Selector 上被阻塞,设置中断状态并且立即从选择操作中 可能 返回一个 非0 值, * 仅仅作为 java.nio.channels.Selector#wakeup 方法被调用的返回值。 * 如果没有上述条件,那么会立即设置中断状态。 * 中断死亡的线程没有任何效果。 */ public void interrupt() { if (this != Thread.currentThread()) checkAccess(); synchronized (blockerLock) { Interruptible b = blocker; if (b != null) { interrupt0(); b.interrupt(this); return; } } interrupt0(); }
|
强调一点,并不是调用interrupt()
方法就能中断线程的,它仅仅是中断状态设置为true!!!可以配合isInterrupted()
方法来中断线程。
一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class TestInterrupt { public static void main(String[] args) throws Exception { Runnable runnable = new Runnable() { @Override public void run() { while (true) { if(Thread.currentThread().isInterrupted()) { System.out.println("线程被中断了"); return; } else { System.out.println("线程没有被中断"); } } } }; Thread thread = new Thread(runnable); long start = System.currentTimeMillis(); thread.start(); Thread.sleep(3000); thread.interrupt(); System.out.println("线程中断了,程序到这里了"); long end = System.currentTimeMillis(); System.out.println(end - start); } } ... 线程没有被中断 线程没有被中断 线程中断了,程序到这里了 线程被中断了 3000
|
三、总结
总算将示例中用到的方法都过了一遍,相信大家对Thread.class
也有了比较清晰的理解了,但是,这里还是很基础的,后续还会对多线程相关知识继续学习,多线程知识点和用处还是很广的,加油!!!