concurrent包中Lock相关的知识点整理

之前看过一些多线程的东西,今天温习了一下,阅读了Java Concurrent Programming 第四节 锁 又回顾了线程状态的流转,整理如下:

与锁相关的切入点:Lock和Condition。

Lock是一个接口,它提供了上锁(lock)和解锁(unlock)等抽象方法;

concurrent包里提供了许多不同策略的锁的实现,使用它们是为了较传统的synchronized同步块而言,使获取和使用独占资源的过程更高效。

传统的synchronized机制是,当某个线程获取到对象内建的锁(monitor lock)之后,其它线程就只能在同步块的入口处僵等,处于blocked状态。

如果等候时间很短,那问题不大;然而如果排队等候的线程很多,预期到要等候的时间较长,那么其它线程就在空转,因为blocked状态下线程不会释放所占用的系统资源,此时可以采用异步通知机制让这些线程休息,即切换到waiting状态并等待激活信号的到来,并释放所占资源。

concurrent包里的lock()方法通过LockSupport.park()来实现,即使得线程转为waiting状态。参考线程状态流转图


进一步地,为了同一把锁需要长时间等候的场景可能有多种,例如生产者和消费者之间有个传送带BlockingQueue是独占资源,每当写入Q都要获得同一把写锁。然而有两种情况(或者说状态/条件)可导致长时间等待的预期:一种是Q满了无法入队(理应使生产者等待),另一种是Q空了无法出队。(理应使消费者等待)。

Condition提供了上述长时间等待场景中分情况调度线程的能力,提供了等待(await)和激活(signal)功能(类似于传统synchronized机制中的wait和notify,注1)。

不同角色的线程得以遵循不同的Condition来调度。

至此,由一个Lock派生多个Condition的结构才阐述清楚。(关于Condition的翻译,注2)


几种抽象的锁:

ReadWriteLock:当一个线程申请读锁时,只要写入锁没有被其他线程持有,那么会立刻拥有读锁。当一个线程申请写入锁时,其他线程不能持有读锁和写入锁,否则会一直等待。写锁是排他锁,而读锁可以同时被多个线程持有。

ReentrantLock:考虑到递归的情况,有些锁需要被一个线程重复获取,就形成了“可重入锁”。

ReentrantReadWriteLock:同时具备上述两种功能。

几种涉及锁的场景:

实际编程中,对于某些常见的场景,直接面向锁编程仍然太罗嗦,所以诞生了以下几种工具类对各个场景予以直观的支持。

  • Semaphore(信号量):把有限个资源提供给多线程竞争,还提供不同的竞争策略(公平/不公平),类比停车场车位紧张的场景。
  • Mutex (互斥):即信号量为1的情况,类比只有一个洗手间的场景。
  • Latch(字面义:闭锁、门闩):等待直到n个目标线程触发了某个事件( CountDownLatch:A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.)对于CountDownLatch触发事件对应的动作是计数器减1,即CountDownLatch.countDown();注意它不能reset,因此是一次性的。类比百米冲刺的终点线,等待直到所有队员撞线后,再执行颁奖典礼。
  • Barrier(字面义:屏障、栅栏) :在几个切面上阻拦先抵达的线程使之等待,直到所有目标线程到达,以便所有线程继续从同一个切面开始并发执行(CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. )注意它可以reset,因此每个切面可以复用同一个CyclicBarrier。这篇帖子提到CyclicBarrier可以很方便的协助实现MapReduce这样的复杂的并行计算场景,真是一语中的,不过现实生活中有否形象的例子呢?我想到了诺曼底登陆。假设德军在诺曼底有n道防线,击溃每一道防线都需要不同数量的盟军士兵登陆集结后发起冲锋;因为当时盟军已经获得了海路的绝对控制权,士兵被源源不断地送上岸,同时也会在战斗中牺牲。上述例子中,每个盟军士兵都是一个线程,而防线即Barrier,每道防线等待特定数量的活着的线程抵达集结,便被攻破,紧接着收缩形成下一道防线,相当于reset Barrier溃败的阈值。


注1:wait和await有什么区别?
这篇文章描述了传统的wait的作用, 这篇文章试图描述两者的不同。

本质上都是使线程释放锁并进入wait状态,主要的区别有二。一是相比synchronized,lock可以派生出多个condition触发await,语义上更直观;二是调用传统的wait后,要么通过notify随机激活某个线程,要么通过notifyAll激活所有线程允许其竞争锁;而await后支持更丰富的锁竞争策略,如公平的(按await的顺序排队)和不公平的等等。

注2:大多数 文章里都 把condition翻译成“条件”,感觉生硬。 condition也有情况,状况,状态(a state of being)的意思,如 race condition, wiki翻译为”竞争条件“,我觉得 应该直译成 竞争状态 ”(意在强调多线程互相竞争状态下带来的危害); 意译为 竞态危害 (所以才会有个别名 race hazard )或 并发安全问题 英文wiki1 节就提到了什么情况下竞争状态可以无害( non-critical )。看了许多对 race condition 的解释,大部分讲的都是并发情况下由于未知的读写执行顺序导致变量值不稳定,即 这篇文章里讲到的第 1 sequencing problems(一下就联想到了修正后的 JMM 里的happens-before 法则,都是关于 sequence)但罕有文章提到 race condition的第2点, locking problems。(依稀记得以前看过 老赵的一篇文章,讲尾递归写的不好导致live lock,属race condition的范畴。) 


===========================

最近小结了 数据库的事务隔离级别,事务有ACID四个维度,隔离性和隔离级别是相通的,一下联想到 ReadWriteLock的实现应该属于Read Committed(共享读锁+排他写锁)。当然,还取决于怎么用,如果直到事务结束才释放共享读锁,而不是在事务中读取完资源立即释放读锁,也可以看成是Repeatable Read。可参考这篇文章 5.1.2.1. Isolation levels 有相关描述。


另,注意到Doug Lea较早的concurrent包实现,ReadWriteLock有多个实现,主要是提供了读写锁竞争时的不同策略,摘录于此:

尽管读-写锁定的基本操作是直截了当的,但实现仍然必须作出许多决策,这些决策可能会影响给定应用程序中读-写锁定的效果。这些策略的例子包括:

    • 在 writer 释放写入锁定时,reader 和 writer 都处于等待状态,在这时要确定是授予读取锁定还是授予写入锁定。Writer 优先比较普遍,因为预期写入所需的时间较短并且不那么频繁。Reader 优先不太普遍,因为如果 reader 正如预期的那样频繁和持久,那么它将导致对于写入操作来说较长的时延。公平或者“按次序”实现也是有可能的。
    • 在 reader 处于活动状态而 writer 处于等待状态时,确定是否向请求读取锁定的 reader 授予读取锁定。Reader 优先会无限期地延迟 writer,而 writer 优先会减少可能的并发。
    • 确定是否重新进入锁定:可以使用带有写入锁定的线程重新获取它吗?可以在保持写入锁定的同时获取读取锁定吗?可以重新进入写入锁定本身吗?
    • 可以将写入锁定在不允许其他 writer 干涉的情况下降级为读取锁定吗?可以优先于其他等待的 reader 或 writer 将读取锁定升级为写入锁定吗?

当评估给定实现是否适合您的应用程序时,应该考虑所有这些情况。

互联网大多是读多写少的场景,因此才采用了现在的默认实现吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值