Java并发编程基础构建模块(05)——同步工具类

        前面介绍了同步容器类,阻塞队列等,这里说说同步工具类,同步工具类主要是根据自身的状态来协调线程的控制流,阻塞队列就可以作为同步工具类,其他的同步工具类还包括:信号量(Semaphore)、闭锁(Latch)、栅栏(Barrier)等,也可以自己创建工具。

        同步工具类大体原理有3点:

        1、 用一些属性来表示某个状态,这些状态决定执行同步工具类的线程是等待,还是继续执行;

        2、 提供了一些操作这些状态的方法;

        3、 还提供另一些方法,用于高效地等待同步工具类进入到预期状态(线程是无法控制具体怎么执行的,只能修改状态,线程执行到可以改变的时候根据状态做出改变)。

        下面说说刚才提到了信号量、栅栏、闭锁等:

        信号量(Semaphore):信号量维护了一个固定容量的许可集,每一个许可被获取后则不可用,直到这个许可被释放,当获取许可时发现没有可用的许可,会阻塞当前线程,直到有可用的许可为止(或者被中断)。Semaphore构造时需要指定数量和是否公平(公平情况下谁先阻塞,就会优先给谁,但是性能没非公平的好),acquire方法获取许可,release方法释放许可,可以一次获取多个,也可以释放多个。

        信号量主要作用其实就是对资源访问的一个限制,比如限制同时访问某个资源的操作数量,或者限制同时执行某个操作的数量,常用于实现某种资源池,或者对容器加边界。如下代码实现一个池:

        

        再来说说闭锁,闭锁(Latch):一种同步工具类,可以延迟线程进度(其实就是阻塞线程)直到到终止状态。通俗理解,闭锁相当于一扇门:闭锁到达结束状态前,门一直关闭,任何线程都无法通过(阻塞),当到达结束状态时,门开,所有线程马上通过(继续执行),闭锁结束,门以后一直处于敞开状态(就是一次性的门)。

        比如线程S,它需要等线程A,B,C都执行完毕后,再继续执行,此时闭锁就可以方便的实现这个功能。闭锁的主要使用场景:

        1、 开/关锁存器,如确保某个操作所需要资源都被初始化之后才继续执行;确保某个服务在其所依赖的其他服务都已经启动后才启动;等待某个操作的所有参与者全部就绪再继续执行等。

        2、在一些应用场合中,为了速度或其他等原因,需要将一个问题分解成N个部分去解决,或将一个大任务分解成多个小任务同时并行去执行,也就是由原来单线程执行一个大任务分解成一堆多线程并行执行的小任务,全部执行完毕后(整个问题都解决了),再继续向下执行。

        CountDownLatch就是JDK中一个非常好的闭锁实现,CountDownLatch有一个整数计数器,初始化时必须设置一个值(大于0),await方法会使当前线程阻塞,等待直到计数器为0再继续向下执行,countDown方法能减少计数器。如上面提到的线程S,它需要等线程A,B,C都执行完毕后再继续执行,可以创建一个计数器是3的CountDownLatch,S使用await方法等待,线程A,B,C每个执行完毕后可以countDown一下,ABC都执行完毕后,计数器为0,S继续向下执行。

        举个实际开发中例子,我们给法院做软件,法院周边系统需要从法院的审判系统(法院的主系统)中抽取案件信息(说白了就是信息抽取操作),需要抽取民事、刑事、执行3类案件,单线程抽取一次时间很长(比如每类案件抽取1小时,就要花3小时),由于各类案件相互独立,就可以多线程一起抽取,3个线程一起抽取就快了很多(可能1个小时多点就结束了),但是主线程必须等待3类案件全部抽取结束,才继续执行后续的案件的整合、计算等操作。示例代码如下:

        

        其实,还有FutureTask也可以用于闭锁,FutureTask其实就是Future和Callable的结合体,相当于有3个状态:等待运行,运行中,运行完成。因为实现了Future,所以调用get方法可以阻塞当前线程,直到运行FutureTask的线程计算出结果为止(还有个2个方法停止,FutureTask抛出异常,或者当前线程被中断),如果已经计算出结果了,get方法会马上返回这个结果。这样就可以实现类似所需要的资源都被初始化之后才继续执行;确保某个服务在其所依赖的其他服务都已经启动后才启动等情况。比较简单的实现方式就是启动所有依赖操作的线程后,逐个去get,没执行完则阻塞,执行完的给结果,最终所有结果的获得了,也不耽误时间。

        其实FutureTask多用于一些时间比较长的计算,主线程可以在完成自己的任务后,再去获取结果。相比CountDownLatch,两者使用场景不同,没法比较,但是针对闭锁功能来说,CountDownLatch更像一个锁,而FutureTask只能说“能实现闭锁功能”,但是某些场景,如需要返回值时,FutureTask能更方便的获取结果(直接get就行),而CountDownLatch只能将每个线程的计算结果放到共享区域(线程间资源共享)。

        最后说说栅栏,栅栏(Barrier):它允许一组线程互相等待,直到到达某个公共屏障点(common barrier point)。类似于闭锁,它能够阻塞一组线程直到某个事件的发生,与闭锁不同之处在于闭锁用于等待事件,而栅栏用于等待其他线程。一个是根据事件的,一个是根据线程的。

        CyclicBarrier是栅栏的一种实现,它在释放等待线程后可以重用,也就是说它是循环的栅栏。其实这里可以看出来,CyclicBarrier与CountDownLatch比较相似,都能实现线程之间的相互等待,由于CountDownLatch的countDown()不会引起阻塞,所以CountDownLatch更多应用于主线程等待所有子线程结束后再继续执行的情况,是基于事件的,一次性的;而CyclicBarrier计数达到指定后会重新循环使用,所以CyclicBarrier可以用在所有子线程之间互相等待多次的情形,是基于线程的,可以循环的。

        CyclicBarrier常用于并行迭代算法,这种算法通常将一个问题拆分成一系列的相互独立的子问题,而且需要反复执行,反复再某个点处理结果(如果执行1次,CountDownLatch比较合适)。举个例子,处理大位图图片,需要逐个像素点的读取并转换,整个图片所有像素点读取并转换完毕后,需要整体处理,此时单线程处理比较慢,使用多线程,每个线程处理图片的一部分(读取和转换),整个图像像素点都读取和转换完毕后,再整体处理,处理完一张图片后,马上处理下一张。代码如下:

        图片处理主类:

        

        处理每部分的线程:

        

        其实Exchanger也是一种形式的栅栏,他是一种双向的栅栏,可以看成SynchronousQueue的双向形式,主要用于双方在栅栏位置交换数据,当双方的执行时间不对称时,Exchanger会阻塞先到的一方,等待另一方完成。当一个线程调用exchange方法时,如果有其他线程之前调用过exchange方法,则会进行交换,如果没有其他线程调用过,则阻塞当前线程,直到有其他线程调用exchange进行交换为止,这块就不详说了。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值