引荐文章:
# Android进阶宝典 — 并发编程之JMM模型和锁机制
在上文的末尾,为了处理sychronized的堵塞性问题,引出了CAS算法,意图便是为了进步功率然后达到核算精确的意图。其实在Android开发中,一向追求的便是响应速度,因而CAS算法是首选。
那么在CAS算法中是怎么确保核算的精确性呢?这儿能够做一个总结:

首要CAS算法是会存储主内存中的旧值,然后完结核算之后并不会直接刷入主内存,而是会首要比较主内存和旧值是否共同,假如共同说明主内存的值没有被其他线程修正正,则直接刷入主内存;假如不共同,则作业内存缓存该地址的数据失效,需求从头获取,从头核算。
1 无锁并发与有锁并发
所以,已然加锁之后会影响功率,那么无锁(像CAS)就一定会进步功率吗?其实万物都不是必定的。
关于sychronized加锁的场景下,由于线程在没有获取锁对象的时分会堵塞等候获取,导致开释CPU资源然后发生线程的上下文切换;而无锁的状态下,线程是一向在运行的,只不过会由于主内存中的值与旧值不共同导致重试,可是线程是一向在高速运行的。
那么CAS一定是习惯一切的场景吗?其实不是,关于线程数量少的场景下,例如与CPU核数共同,这种情况下CAS一定是功率最高的;可是假如是10000个线程的场景下,CAS反而功率会下降,由于频频地重试和从头读取,都会消耗额定的CPU资源。
所以CAS是根据达观锁的思维,我不怕别的线程来修正成果,由于CAS有重试机制能确保核算的精确性;可是sychronized是根据失望锁的思维,在加锁后不允许其他线程修正内存值,只有当锁开释之后,才干修正。
那么假如创立的线程数超越CPU的核心数,那么CAS算法还会高效吗?其实有了线程池,就能够尽或许地限制最大线程数,确保CAS算法的高效。
2 核心线程数的考量
首要咱们需求知道为什么会出现线程池这个东西?首要假如咱们有10000个使命,那么必定不会拓荒10000个线程去处理,所以出现了线程池的概念,利用有限的线程数处理无限的使命。
2.1 线程饥饿
虽然线程池能够处理无限的使命,可是假如使命的类型是不一样的,例如有2000个使命,其间A使命类型有1000个,B使命类型有1000个,当时线程池中有10个作业线程。
private static void testThreadPool(){
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
executorService.submit(()->{
System.out.println("上菜--");
});
}
for (int i = 0; i < 10; i++) {
executorService.submit(()->{
System.out.println("做菜--");
});
}
}
假如作业线程悉数都在履行A使命,B使命处理闲置状态,可是B使命会对A使命有调度,或许说有依靠项,或许会导致概念上的“死锁”。例如上面的比如,正常应该是先做菜完事后才干上菜,可是由于履行的先后顺序,导致上菜在等做菜完,可是做菜又不能先履行。因而假如这个时分有一个新的线程履行了做菜的操作,那么上菜就能正常履行了。
所以由于线程饥饿的问题,所以在运用线程池的时分,关于有相互关联的使命来说,不能放在同一线程池中处理,需求新拓荒线程池处理。
2.2 核心线程数
当然线程池并不是代表着线程数越多,它的功率就越高,假如一股脑创立一堆线程真实运作的时分反而用不上,才是真实糟蹋系统资源,所以需求根据咱们的业务场景来决议核心线程数的巨细。
(1)CPU密集型使命
关于CPU密集型使命,例如for循环,一般选用CPU核数 + 1就能实现CPU的最优利用率,为什么要+1呢,便是由于或许存在系统故障导致线程停止,那么额定的这个线程就能够顶上去。
(2)IO密集型使命
这种一般发生在频频地读写IO操作,或许从数据库读写数据,这个时分CPU会闲置下来,能够运用多线程来进步功率
因而这儿总结一个公式:核心线程数 = CPU核数 * CPU利用率 * 核算时长百分比 / 等候时长百分比
例如建议一个网络恳求,从建议恳求到拿到成果中间是需求等候时长的,假设等候时长占比为20%,那么核算时长为80%,当时CPU核数为4核,那么核心线程数 = 4 * 100% * 80% / 20% = 16
3 自定义线程池
从上面小节中,大概了解了线程池的作业流程,其实便是一个生产者和顾客的规划思维,主线程通过生产使命,线程池来消费使命。

所以根据上图的思维,规划一个线程池。
3.1 使命行列
使命行列,首要的作用便是存储异步使命提供给线程池调用使命,所以在其内部是保护了一个行列
//保护异步使命的行列
val dequeTask = ArrayDeque<T>()
val mLock = ReentrantLock()
//条件变量 行列满了
val fullWaitSet = mLock.newCondition()
//行列空了
val emptyWaitSet = mLock.newCondition()
//行列的容量 默认为4个
val dequeSize = AtomicInteger(4)
这个行列随时或许满,随时或许空,因而当行列满的时分,便不能增加使命,需求等到行列不满;当行列空的时分,不能取使命,需求等到行列不为空的时分,因而需求两个条件变量Condition。
interface IBlockingQueue<T> {
fun addTask(t: T)
fun removeTask(): T?
}
关于行列来说,首要有两个操作,一个是取操作,一个是增加操作。
override fun addTask(t: T) {
//线程安全操作
mLock.lock()
try {
//判别行列巨细是否超限
while (dequeTask.size >= dequeSize.get()) {
//需求等到行列不满的时分
fullWaitSet.await()
}
//假如没有超限
dequeTask.add(t)
//已然能增加数据,证明行列不是空的了
emptyWaitSet.signal()
} finally {
mLock.unlock()
}
}
override fun removeTask(): T? {
mLock.lock()
var target: T? = null
try {
//判别行列是否为空
while (dequeTask.size == 0) {
//假如为空,需求等到不为空的时分
emptyWaitSet.await()
}
//假如行列不为空,那么能够取数据
target = dequeTask.removeFirst()
//已然取出了数据,那么行列一定不是满的,则能够增加数据,告诉fullWaitSet能够跳出循环了
fullWaitSet.signal()
} finally {
mLock.unlock()
}
return target
}
所以在增加或许获取的时分,依靠两个条件变量,当一方能够履行时,告诉另一方履行,两者属于相互依靠,有消费就有增加。
3.2 线程池
从小节最初的图中能够看到,除了使命行列之外,还有便是一个线程调集,当履行使命时,首要会分配给线程调集中的核心线程履行。
class MyThreadPool {
//使命行列
private val blockingQueue: MyBlockingQueue<Runnable> by lazy {
MyBlockingQueue()
}
//线程调集
private val works: HashSet<Work> by lazy {
HashSet()
}
//留意这儿能够动态装备,demo中暂时写死了
private val coreSize = AtomicInteger(4)
fun execute(task: Runnable) {
//假如当时有闲暇的核心线程能够运用
if (works.size < coreSize.get()) {
val work = Work(task)
work.start()
works.add(work)
} else {
//否则就往使命行列中塞
blockingQueue.addTask(task)
}
}
inner class Work(var task: Runnable?) : Thread() {
override fun run() {
super.run()
while (task != null) {
try {
task?.run()
}finally {
task = blockingQueue.removeTask()
}
}
}
}
}
看下execute办法,当有闲暇线程时,使命会交给线程当即履行,而假如没有闲暇的线程,那么就会塞进使命行列中。
咱们看下Work这个内部类,是承继自Thread,当履行run办法时,咱们能够看到是有一个while循环的,会不断从使命行列中去使命履行。
其实关于线程池的运用,前面咱们在介绍OkHttp的时分,启异步恳求办法中就运用到了高并发、高吞吐量的线程池,并合作堵塞行列一起运用,在实际的项目开发中,咱们或许暂时用不到线程池,可是在一些框架源码中却是经常见到,这也有利于咱们理解其间的思维。