问题:
sleep方法没有释放锁:不让出资源
wait方法释放了锁:使得其他线程可以使用同步控制块或者方法
sleep不释放锁 线程是进入阻塞状态还是就绪状态?
sleep是不是还占着CPU,是互斥还是同步?
链接:https://www.zhihu.com/question/23328075/answer/665978836
来源:知乎
首先说,虽然大家用Java Thread的api,但实际上Thread是OS提供的抽象和功能。这么理解会让整个问题更清楚。千万不要从类啊,静态方法之类的角度去看待这个问题。这是Java设计上比较不可取的地方。
一个Thread是指“是操作系统能够进行运算调度的最小单位,以及相关资源的集合“。那么既然是可以调度的,线程本身就能“被调度”或者“暂停被调度”。所谓sleep是指让线程“暂停被调度一段时间”,或者学术一点的词叫“挂起”一段时间。整个sleep过程除了修改“挂起“状态之外,不会动任何其他的”资源“。这些资源包括任何持有的任何形式的锁。
比如A线程如果先抢到一个锁,然后B线程因为A线程抢到了就等着。接着A sleep了。B无论如何没有任何机会去拿到这个锁。你可以认为这样就是你预期的,也可以认为这样实际上因为A的实现,B的执行被卡住,浪费了CPU。因为你当你用了sleep的时候就意味着你想要让当前线程不考虑其他线程的感受,只是自己暂时不干活而已。因此,对于问题:
sleep不释放锁 线程是进入阻塞状态还是就绪状态?
答案是进入阻塞状态,确切的说Thread在Java的状态TIMED_WAITING(但这个状态其实并没那么重要,可以认为是java的内部细节,用户不用太操心)。往下一层,在不同OS上底层的sleep的实现细节不太一样。但是大体上就是挂起当前的线程,然后设置一个信号或者时钟中断到时候唤醒。sleep后的的Thread在被唤醒前是不会消耗任何CPU的(确切的说,大部分OS都会这么实现,除非某个OS的实现偷懒了)。这点上,wait对当前线程的效果差不多是一样的,也会暂停调度,等着notify或者一个超时的时间。期间CPU也不会被消耗。
对于问题
wait方法释放了锁:使得其他线程可以使用同步控制块或者方法?
这里需要多解释一点。
wait,和notify/notifyall是一套。他们是用来做多线程之间相互同步的工具的。这里不想死抠“同步”/“互斥”的字眼。只是当你用了wait/notify/notifyall之后,就意味着你主观意图上是想让某个多个线程之间相互的“talk”,然后协商决定谁该执行那段代码。这和sleep那种自顾自的方式完全不同。
为啥要有wait/notify/notifyall呢?snychronized也可以协商啊。因为synchrnoized太简单了,只能最基本的的同步的工作。但是有的时候需要根据特殊的条件来判定是不是应该进入几端代码。比如blocking queue,consume的代码对于空的队列就不能consume,得wait,直到有人produce;而produce代码对于满的队列不能produce,也得wait,直到有人consume。wait就是解决这类问题的。wait要解决的关键点是,要判断条件就要获取锁,而判断条件本身又是能否获取锁的条件。鸡和蛋的怪圈。
于是Java是搞出了synchronized -- wait的结构,即“获取锁-判断-判断不满足就释放锁-然后等通知重来”,用特定的代码形式来实现“条件同步”:
synchronized { // 获取锁 while (判定条件) wait(); // 如果条件不满足就释放锁,并且等着 // ... 要进入的代码 notify(); // 或者notifyAll,通知其他wait者可以重来 }
这个写法尽管能工作,但看起来蹩脚,还很容易出错。如果不写synchronized,就是运行时错误IllegalMonitorStateException;万一忘记了while或者忘记notify,代码正确性就可能有问题,并且连运行时错误都没有。造成这些的也许是因为java本身没把函数当作First Class Citizen造成的。如果换个语言,也许就可以这么写:
lock.waitOnCond(判定条件, () -> { // ... 要进入的代码 });
这样心智负担会小,也更优雅。
顺便说下,wait的参数支持时间是为了允许编写代码避免永久等下去。比如可以实现一下插队列等待最多30s,进不去就向上层报错这种逻辑。但这还是解决wait自己场景的问题,和sleep的等待一段时间应用场景不同。wait要和synchronized搭配使用,而synchronized里有sleep就是定时炸弹了。
回到现实开发中,直接用sleep的机会很少的。大多数情况下,写代码都是希望代码执行的越快越好,不会说故意sleep一下。如果有的话,可能是:
- “模拟等待一个慢速操作“,这种经常用于测试里的mock。这时用sleep没啥毛病。
- 想等待一个慢速api的结果,比如发一个请求,等一会,看看结果来没来。这种一般可以用Future定时等待的功能或者“join“等工具来处理。
而对于wait/notify/notifyall,我建议任何初学者都只作为学习用途,不要放到生产代码里。实际上wait/notify/notifally代表了一组最基础的同步原语,极度容易出错造成各种死锁,活锁等问题,极难测试和保证正确性(比如调整一行代码的顺序就正常,否则就死锁;或者忘记写了notify,结果wait永远持续下去)。用锁来做同步编写代码简直就是万恶之源。如果一定要用,那么请注意:
1)自己对并发编程非常熟悉,已经是准专家或更高级别;
2)有完备的测试工具和分析工具,以及大把的测试时间(数个月或者数年);
3)编写的是一套并发支持工具,而非直接用作业务逻辑控制。
归根到底线程+锁这种并发模型是多种并发模型里最基础的,最接近底层实现的,也是最不好用的。一般的并发性编码,最好是寻求如Future/Promise,Actor,Fork & Join,协程等方式来做,即便是Java本身不支持,混搭一点点kotlin,clojure,scala,然后使用他们的并发模型是极好的。如果对各种并发模型感兴趣,可以看看那本《七周七并发模型》。
顺便扩展一下
- Java的sleep api的形式并不太好,Thread.sleep其实是Thread.currentThread().sleep()的意思。见过很多初学者问“Thread.sleep到底sleep的是谁”这种问题。但是如果真的anotherThread.sleep()又不行。sleep实际上又只能暂停当前线程。这点上就能看出所谓OOP语言的表达能力令人郁闷之处。
- 任何Object都可以wait/notify这个设计非常无语。Java希望把并发控制做到每一个Object里。其实这样做即难以理解,又给每个Object增加了Overhead。想要明确使用Thread+锁,那么就明确的表达锁。JDK5之后,有了JUC,用明确的“XXXXLock”对象来实现同步。这个方式清楚明白的多;或者,如果想让Object作为并发的隔离单元。Object之间并发安全,Object内部保证不会并发。这其实就是基于messaging的Actor模型。Java的做法夹在中间两边不讨好。