14 | Lock和Condition(上):隐藏在并发包中的管程
王宝令
该思维导图由 AI 生成,仅供参考
Java SDK 并发包内容很丰富,包罗万象,但是我觉得最核心的还是其对管程的实现。因为理论上利用管程,你几乎可以实现并发包里所有的工具类。在前面《08 | 管程:并发编程的万能钥匙》中我们提到过在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,管程都是能够解决的。Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。
今天我们重点介绍 Lock 的使用,在介绍 Lock 的使用之前,有个问题需要你首先思考一下:Java 语言本身提供的 synchronized 也是管程的一种实现,既然 Java 从语言层面已经实现了管程了,那为什么还要在 SDK 里提供另外一种实现呢?难道 Java 标准委员会还能同意“重复造轮子”的方案?很显然它们之间是有巨大区别的。那区别在哪里呢?如果能深入理解这个问题,对你用好 Lock 帮助很大。下面我们就一起来剖析一下这个问题。
再造管程的理由
你也许曾经听到过很多这方面的传说,例如在 Java 的 1.5 版本中,synchronized 性能不如 SDK 里面的 Lock,但 1.6 版本之后,synchronized 做了很多优化,将性能追了上来,所以 1.6 之后的版本又有人推荐使用 synchronized 了。那性能是否可以成为“重复造轮子”的理由呢?显然不能。因为性能问题优化一下就可以了,完全没必要“重复造轮子”。
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
Java SDK中的Lock&Condition实现了管程,解决了并发编程中的互斥和同步问题。相比于synchronized,Lock提供了更多的灵活性和功能,如支持中断、超时和非阻塞地获取锁。这些功能使得Lock能够更好地满足并发编程中的需求,特别是在处理不可抢占条件时更为有效。Lock的可见性是通过Happens-Before规则和volatile变量来保证的,而ReentrantLock支持可重入,使得线程可以重复获取同一把锁。另外,ReentrantLock还提供了公平锁和非公平锁两种策略,分别影响等待队列中线程的唤醒顺序。除了并发大师Doug Lea推荐的三个最佳实践外,还可以参考一些业界广为人知的规则,如减少锁的持有时间、减小锁的粒度等。Lock&Condition的使用为并发编程提供了更多的选择和灵活性,能够更好地满足复杂的并发需求。
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 并发编程实战》,新⼈⾸单¥59
《Java 并发编程实战》,新⼈⾸单¥59
立即购买
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
全部留言(124)
- 最新
- 精选
- 我觉得:不会出现死锁,但会出现活锁
作者回复: 👍
2019-03-308120 - 小华有可能活锁,A,B两账户相互转账,各自持有自己lock的锁,都一直在尝试获取对方的锁,形成了活锁
作者回复: 👍
2019-03-30596 - xiyi存在活锁。这个例子可以稍微改下,成功转账后应该跳出循环。加个随机重试时间避免活锁
作者回复: 👍👍👍
2019-03-3077 - bing文中说的公平锁和非公平锁,是不按照排队的顺序被唤醒,我记得非公平锁的场景应该是线程释放锁之后,如果来了一个线程获取锁,他不必去排队直接获取到,应该不会入队吧。获取不到才进吧
作者回复: 是的,高手👍👍👍
2019-03-30569 - J.M.Liu1.这个是个死循环啊,有锁没群,都出不来。 2.如果抛开死循环,也会造成活锁,状态不稳定。当然这个也看场景,假如冲突窗口很小,又在单机多核的话,活锁的可能性还是很小的,可以接受
作者回复: 👍👍👍
2019-03-3035 - 森呢老师,你好,这是我第二遍研读你的课程了,每一遍都收获很大。第一次写留言有点紧张。 你上面写的jdk利用内存模型的三条规则来保证可见性,是正确的。但我觉得好像描述的理由好像不充分,我不知道我理解的对不对,请老师解答一下 我的理解应该是 :1)释放锁成功后,写state的值 (unlock>state-=1) 顺序性 2)获取锁前,读state值(state>lock)顺序性 3)传递性 unlock>lock 下面是jdk的源码 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread();//获取当前线程实例 int c = getState();//获取state变量的值,即当前锁被重入的次数 if (c == 0) { //state为0,说明当前锁未被任何线程持有 if (compareAndSetState(0, acquires)) { //以cas方式获取锁 setExclusiveOwnerThread(current); //将当前线程标记为持有锁的线程 return true;//获取锁成功,非重入 } } else if (current == getExclusiveOwnerThread()) { //当前线程就是持有锁的线程,说明该锁被重入了 int nextc = c + acquires;//计算state变量要更新的值 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc);//非同步方式更新state值 return true; //获取锁成功,重入 } return false; //走到这里说明尝试获取锁失败 }
作者回复: 感谢补充😄state必须是volatile变量,否则是不会有unlock>lock的,我比你更紧张😂
2019-07-04525 - linqwclass Account { private int balance; private final Lock lock = new ReentrantLock(); // 转账 void transfer(Account tar, int amt){ boolean flag = true; while (flag) { if(this.lock.tryLock(随机数,NANOSECONDS)) { try { if (tar.lock.tryLock(随机数,NANOSECONDS)) { try { this.balance -= amt; tar.balance += amt; flag = false; } finally { tar.lock.unlock(); } }//if } finally { this.lock.unlock(); } }//if }//while }//transfer } 感觉可以这样操作
作者回复: 点个大大的赞!不过还可以再优化一下,如果阻塞在tar.lock.tryLock上一段时间,this.lock是不能释放的。
2019-04-071525 - 海鸿突然有个问题: cpu层面的原子性是单条cpu指令。 java层面的互斥(管程)保证了原子性。 这两个原子性意义应该不一样吧? 我的理解是cpu的原子性是不受线程调度影响,指令要不执行了,要么没执行。而java层面的原子性是在锁的机制下保证只有一个线程执行,其余等待,此时cpu还是可以进行线程调度,使运行中的那个线程让出cpu时间,当然了该线程还是掌握锁。 我这样理解对吧?
作者回复: 对
2019-03-30419 - 姜戈我也觉得是存在活锁,而非死锁。存在这种可能性:互相持有各自的锁,发现需要的对方的锁都被对方持有,就会释放当前持有的锁,导致大家都在不停持锁,释放锁,但事情还没做。当然还是会存在转账成功的情景,不过效率低下。我觉得此时需要引入Condition,协调两者同步处理转账!
作者回复: 用condition会更复杂
2019-03-3014 - Q宝的宝老师,本文在讲述如何保证可见性时,分析示例--“线程 T1 对 value 进行了 +=1 操作后,后续的线程 T2 能否看到 value 的正确结果?“时,提到三条Happen-Before规则,这里在解释第2条和第3条规则时,似乎说反了,正确的应该是,根据volatile变量规则,线程T1的unlock()操作Happen-Before于线程T2的lock()操作,所以,根据传递性规则,线程 T1 的 value+=1操作Happen-Before于线程T2的lock()操作。请老师指正。
作者回复: 火眼金睛👍👍👍👍,这就改过来
2019-03-30314
收起评论