深入理解并发编程同步工具类

网站建设4年前发布
29 0 0

​今天跟大家分享一个并发编程领域中的一个知识点——同步工具类。,我将结合一个真实线上案例作为背景来展开讲解这一知识点。给大家讲清楚什么是同步工具类、适合的场景、解决了什么问题、各个实现方案的对比。希望对大家理解同步工具类这个知识点有所帮助。,我们先看一个案例:,20230306010351c9963b9037f2c029ad7196093942b584ddb885126,图一:逻辑架构图,有一个线上“人脸识别”的应用,应用首次启动要求多线程并行将存储在DB中的人脸数据(512位的double类型数组)载入到本地应用缓存中,主线程需要等待所有子线程完成任务后,才能继续执行余下的业务逻辑(比如加载dubbo组件)。,拿到这个需求,大家不妨先思考一下,如果让你来实现,你打算怎么做?思考点是什么?,让我们一起来分析一下这个需求:,首先这个需求是应用首次启动,需要用多线程并行执行任务的,充分利用CPU的多核机制,加快整体任务的处理速度。,其次大家先可以看下上述图一,多线程并行执行下,主线程需要等待所有子线程完成任务后才能继续执行余下的业务逻辑。,要实现这个需求,我们就要思考一下看有没有一种机制能让主线程等待其他子线程完成任务后,它再继续执行它余下的业务逻辑?,什么是join?,join方法是Thread类内部的一个方法,是一种一个线程等待另一个或多个线程完成任务的机制。,基本语义:,如果一个线程A调用了thread.join()方法,那么当前线程A需要等待thread线程完成任务后,才能从thread.join()阻塞处返回。,示例代码:,结果打印:,20230306010352d3fc199628fd306d22690238e54c28af908430662,原理:,20230306010353783b87f0614ce1d2436838ae2c8abc3625ca9a969,源码解析:,20230306010353a4cbeae45f3837e5df27213ff722adc06afc31212,从源码细节来看(为了方便陈述,我们假设有一个线程A调用thread.join()),我们说线程A持有了thread对象的一把锁,while循环判断thread线程是否存活,如果返回false,表示thread线程任务尚未结束,那么线程A就会被挂起,释放锁,线程状态进入等待状态,等待被唤醒。,而唤醒的更多细节是在thread线程退出时,底层调用exit方法,详见hotspot关于thread.cpp文件中JavaThread::exit部分。如下(倒数第二行):,什么是闭锁?,闭锁是一种同步工具类,可以延迟线程进度直到其达到终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,直到到达结束状态时,这扇门将会永久打开。闭锁用来确保某些任务直到其他任务都完成后才继续执行。,基本语义:,countDownLatch的构造函数接收一个int类型的参数作为计数器,比如你传入了参数N,那意思就是需要等待N个点完成。当我们调用countDown方法时,这个计数器就会减1,await方法会一直阻塞主线程,直到N变0为止。,原理:,20230306010547679939d343e36c8f81d936f388598b0d89cb3f636,适用场景:,像应用程序首次启动,主线程需要等待其他子线程完成任务后,才能做余下事情,并且是一次性的。 像作者文章开始处提的这个需求,其实比较适合用CountDownLatch这个方案,主线程必须等到子线程的任务完成,才能进一步加载其他组件,比如dubbo。,示例代码:,源码解析:,我们看下示例代码中关于latch.countDown()方法源码部分:,接下来我们看下另一个比较重要的方法即await方法部分源码:,从源码细节来看,我们知道CountDownLatch底层是继承了AQS框架,是一个自定义同步组件。,AQS的状态变量被它当做了一个所谓的计数器实现。主线程调用await方法后,发现state的值不等于0,进入同步队列中阻塞等待。子线程每次调用countDown方法后,计数器减一,直到为0。这时会唤醒处于阻塞状态的主线程,然后主线程就会从await方法出返回。,什么是栅栏?,CyclicBarrier字面意思是可循环(Cyclic)使用的栅栏(Barrier)。它的意思是让一组线程到达一个栅栏时被阻塞,直到最后一个耗时较长的线程完成任务后也到达栅栏时,栅栏才会打开,此时所有被栅栏拦截的线程才会继续执行。,基本语义:,CyclicBarrier有一个默认构造方法:CyclicBarrier(int parties),参数parties表示被栅栏拦截的线程数量。,每个线程调用await()方法告诉栅栏我已经到达栅栏,然后当前线程就会被阻塞,直到以下任一情况发生时,当前线程从await方法处返回。,在CyclicBarrier的内部定义了一个Lock对象,每当一个线程调用await方法时,将拦截的线程数减1,然后判断剩余拦截数是否为初始值parties,如果不是,进入Lock对象的条件队列等待。如果是,执行barrierAction对象的Runnable方法,然后将锁的条件队列中的所有线程放入锁等待队列中,这些线程会依次的获取锁、释放锁。,1)实现多人游戏,直到所有玩家都加入才能开始。,2)经典场景:多线程计算数据,然后汇总结算结果场景。(比如一个Excel有多份sheet数据,开启多线程,每个线程处理一个sheet,最终将每个sheet的计算结果进行汇总),示例代码:,响应结果打印:,什么是信号量?,信号量是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。,基本语义:,从Semaphore的构造方法Semaphore(int permits)来看,入参permits表示可用的许可数量。如果我们在方法内部执行操作前先执行了acquire()方法,那么当前线程就会尝试去获取可用的许可,如果获取不到,就会被阻塞(或者中途被其他线程中断),直到有可用的许可为止。,执行release()方法意味着会释放许可给Semaphore。此时许可数量就会加一。,使用场景:,Semaphore在有限**公共资源**场景下,应用比较广泛,比如数据库连接池场景。,大家可以想象一下,比如我们平时在用的C3P0、druid等数据库连接池,因为数据库连接数是有限制的,面对突如其来的激增流量,一下子把有限的连接数量给占完了,那没有获取到可用的连接的线程咋办?是直接失败吗?,我们期望的效果是让这些没获取到连接的线程先暂时阻塞一会,而不是立即失败,这样一旦有可用的连接,这些被阻塞的线程就可以获取到连接而继续工作。,示例代码:,上述需求的实现方案我例举了join、CountDownLatch、CyclicBarrier、Semaphore这几种。,期间也介绍了每种方案的实现原理、适用场景、源码解析。它们语意上有一些相似的地方,但差异性也很明显,接下来我们详细对它们进行一下对比。,首先我们说当前线程调用t.join()尽管能达到当前线程等待线程t完成任务的业务语义。但细致的区别是join方法调用后必须要等到t线程完成它的任务后,当前线程才能从阻塞出返回。而CountDownLatch、CyclicBarrier显然提供了更细粒度的控制。像CountDownLatch只要主线程将countDownLatch实例对象传递给子线程,子线程在方法内部某个地方执行latch.countDownLatch(),每调用一次计数器就会减1,直到为0,最后主线程就能感知到并从await阻塞出返回,不需要等到任务的完成。,其次我们说在当前线程方法内部,一旦出现超过2个join方法,整体代码就会变的很脏、可读性降低。反观JUC分装的CountDownLatch、CyclicBarrier等组件,通过对共享实例的操作(可以把这个实例传给子线程,然后子线程任务执行的时候调用相应方法,比如latch.countDown()) 显得更加清晰、优雅。,最后比较一下CyclicBarrier和CountDownLatch的差异性。比起CountDownLatch显然CyclicBarrier功能更多,比如支持reset方法。CountDownLatch的计数器只能使用一次,而CyclicBarrier可以多次使用,只要调用reset方法即可。(比如CyclicBarrier典型的数据统计场景,因为中途可能部分线程统计出错或外部数据的订正,可能需要重新再来一次计算,那么这个时候,CountDownLatch无能为力,而CyclicBarrier只要子线程调用reset方法即可)。,而Semaphore往往用来针对多线程并发访问指定有限资源的场景,比如数据库连接池场景。​

© 版权声明

相关文章