引荐文章:
# Android进阶宝典 — 并发编程之JMM模型和锁机制

在上文的末尾,为了处理sychronized的堵塞性问题,引出了CAS算法,意图便是为了进步功率然后达到核算精确的意图。其实在Android开发中,一向追求的便是响应速度,因而CAS算法是首选。

那么在CAS算法中是怎么确保核算的精确性呢?这儿能够做一个总结:

Android进阶宝典 -- 并发编程之线程池

首要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 自定义线程池

从上面小节中,大概了解了线程池的作业流程,其实便是一个生产者和顾客的规划思维,主线程通过生产使命,线程池来消费使命。

Android进阶宝典 -- 并发编程之线程池

所以根据上图的思维,规划一个线程池。

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的时分,启异步恳求办法中就运用到了高并发、高吞吐量的线程池,并合作堵塞行列一起运用,在实际的项目开发中,咱们或许暂时用不到线程池,可是在一些框架源码中却是经常见到,这也有利于咱们理解其间的思维。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。