多线程
多线程的基本知识我是参考了以下文章
- Java多线程的基本概念: 讲了一些基本概念
- 林炳文Evankaka的多线程解析: 讲了一些多线程的经典例题和一些博主自己的总结,写的十分不错
- Synchronized及其实现原理: 简述了多线程中关键字Synchronized的实现原理
- CountDownLatch与CyclicBarrier: 多线程编程中一些控制方法
本文在以上文章的基础上,说说我自己的理解(思维回路可能比较奇怪),对一些没有提到的细节进行补充
多线程的实现方式
多线程有几种实现方式,这是好多面试题里问的,一般来说网上的答案是这样三个或者文章Java实现多线程的四个方式这样的
1 | //一般是这三个 |
这些方式有时候是三个,有时候是四个,他们有什么区别呢?先说结论: 多线程只有一个实现方式,就是实现Runnable接口, 并且使用Thread进行开启
继承Thread类
正确的理解方式 Thread类本身就是一个实现了Runnable接口的类,继承它相当于实现了Runnable 好处是在于不用使用Thread去包装Runnable了
1 | public class MyThread extends Thread { |
实现Runnable接口
需要使用Thread进行包装才能开启线程
1 | class RunnableDemo implements Runnable { |
Callable 和 FutureTask
正确的理解方式是 FutureTask本身实现了RunnableFuture,RunnableFuture继承于Runnable和Future,FutureTask可以通过Callable进行实例化,实例化的对象相当于实现了Runnable 得到了FutureTask还需要通过Thread包装才能开启线程
1 | //1. 新建一个实现了Callable的类 |
多线程解析
先来看单词
单词 | 意思 | 意义 |
---|---|---|
Thread | 线程 | 所谓多线程,就是指的多个Thread |
Runnable | 可以跑,即可以执行 | 每个Thread都需要一个可以执行的东西 |
Callable | 可以调用 | 可以调用的意思,是可以回调,相当于JS里的callback,iOS里的block |
Future | 未来 | 异步任务,配合回调函数,相当于JS里的Promise |
从上表分析
- Thread和Runnable是互相对应共同存在的,这两个才是线程操作的概念
- 而Future和Callable是另一个概念集合,属于异步和回调
他们四个都属于Concurrent的Package,也就是说都属于并发(Concurrent)包
Thread和Runnable代表线程操作
每个Thread中有一个Runnable成员target
- 如果是继承的Thread的类,在new的时候会把target指向this
- 如果是通过Runnable的Object实例化的Thread,target会指向这个Object
当调用了Thread的start方法之后,Thread会进入可执行状态(Runnable),然后在被CPU执行时,Thread调用自己的run方法,方法内执行Target的run方法
Future和Callable代表异步和回调
Future英文直译是未来的意思,也就是不是当下发生的事情,而Callable代表可以调用
- Future本身是一个对异步的控制Interface,包括取消,挂起和执行
- Callable是只有一个call方法,返回一个V范型的返回值
- FutureTask是一个Class,实现了Runnable,Future的Interface,可以通过Callable进行实例化
综上所属,所谓的第三种可以通过Callable实现多线程,就是扯淡。。。 只不过FutureTask刚好就是一个实现了Runnable的Object,所以可以用来实例化Thread,而FutureTask的实例化恰好需要Callable而已
ExecutorService是什么(为什么上文第四个多线程方法是扯淡)
ExecutorService是一个Interface,继承于Executor,其可以通过静态方法获取到实例,好像俗称 线程池 呃 我不确定
1 | ExecutorService executorService = Executors.newCachedThreadPool(); |
其本质是一个通过Future、Callable、Runnable来规划并发任务的容器,Future负责任务管理,Runnable负责任务逻辑代码,Callable负责返回值
Executor这个Interface只有一个方法,就是execute(Runnable command),执行某个可以Runnable的命令,而ExecutorService则在继承其基础上,提供了3个主要方法
1 | //提交一个Callable的对象,并且通过Future拿到返回值 |
根据上文陈述, Runnable接口只是负责线程Thread中的Target执行什么 所以不具有返回值,如果想要得到ExecutorService执行的返回值,必须使用Callable接口,并且使用Future.get()获得,所以以上3个submit分别做了以下操作
方法 | 取得返回值原理 |
---|---|
submit(Callable task) | 提交的就是一个Callable对象,所以可以得到返回值 |
submit(Runnable task, T result) | 提交的Runnable对象不具有返回值,在ExecutorService,内部使用一个FutureTask对象,用T result来实例化FutureTask的Callable对象,从而得到期望的T范型返回值 |
submit(Runnable task) | Runnable没有返回值,那就不要返回值了,执行完直接返回null |
可以看出,之所以submit(Runnable task, T result)可以获得返回值,是因为在ExecutorService内部使用了FutureTask对象,这个对象内部有callable对象来确定返回值。这也说明来Java实现多线程的四个方式文章中的所谓第四个方式是没看源码
多线程控制
使用Object锁
在文章林炳文Evankaka的多线程解析中讲到了一个经典的例题
- 建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。
使用到了Object的wait()和notify(),这里进行补充解析来阐明什么叫Object锁,基本原理请参考林炳文的文章
1 | /** |
在程序中有一行注释
1 | Thread.sleep(100); //确保按顺序A、B、C执行 |
十分重要,如果不使用Thread.sleep的话,会输出结果ACB循环而不是ABC,为什么是这样的呢?
1 | ACBACBACBACBACBACBACBACBACBACB |
Object锁顺序分析
我们在分析图标中用不同的符号代表状态
- x 代表 当前获得了Object锁还未执行打印
- ~x 代表 即将执行却由于获取不到Object锁被挂起
- !x 代表 完成了打印结果 并且释放Object锁
- ?x 代表 执行了wait() 并且释放Object锁
不加sleep(100)的情况
对象 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
A | c a | c !a | ?c !a | c ~a | c ~a | c ~a | c a | c a |
B | ~a | a ~b | a ~b | a ~b | a b | a !b | ?a !b | ~a |
C | b ~c | b ~c | b c | b !c | ?b !c | b ~c | b ~c | b ~c |
输出 | null | A | A | AC | AC | ACB | ACB | 循环 |
加了sleep(100)的情况
对象 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
A | c a | c !a | ?c !a | ?c !a | ?c !a | ?c !a | c !a | c a |
B | sleep | sleep | a b | a !b | ?a !b | ?a !b | ?a !b | ?a |
C | sleep | sleep | sleep | sleep | b ~c | b !c | ?b !c | ?b |
输出 | null | A | A | AB | AB | ABC | ACB | 循环 |
在第8步进入 B对象Thread执行了a.wait,而C对象线程执行了b.wait 之后等同于第一步的两者都在sleep,经过以上分析,我们可以得知sleep就是通过这种方式保证了ABC的顺序
如果将sleep改为1000毫秒,可以清楚的看到,第一组ABC打印使用了3秒以便进入步骤8,剩余的则是瞬时完成的
使用控制器
除了使用Object锁自己进行线程控制之外,Concurrent包内部也提供了一些控制器,比如文章CountDownLatch与CyclicBarrier中提到的
- CountDownLatch: 计数(Count)减少(Down)门挡(Latch),Latch这个单词是门闩的意思,就是关门的那个横木,从字面意思可以理解出来,这就是个计数器,一旦计数减少到某一阈值,就触发
- CyclicBarrier: 循环(Cyclic)遮挡(Barrier),Barrier这个是火车道那种一堆人一起的遮挡,意思就是必须所有人等待一起过,和Latch这个横木不一样,是周期性的遮挡的放开,所以叫Cyclic,这个周期就是你设定的第一个参数
结语
这个文章很早就写好了,一直没有发表,当时为了Java而写的,后来在iOS里也发现了相似的机制,同样具有 synchronized 关键词和上文的两个控制器,但是iOS为了追求效率,大部分称需要喜欢自己手动控制锁。。。所以就很容易死锁。。。后台不一样,后台保证稳定性是第一的,所以更注重synchronized的理解