基本原理
1 重量级锁通过对象内部的监视器(monitor)实现的,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现
2 操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高
3 Java对象头存在monitor这个对象,在hotspot虚拟机中,通过ObjectMonitor类(C++实现的类)来实现monitor
4 wait和notify是用来让线程进入等待状态以及使得线程唤醒的两个操作,都是必须通过结合synchronized,即在synchronized的方法或者代码块中去调用;
5 调用wait() 首先会获取监视器锁,获得成功后,会让线程进入等待状态,并且进入等待队列并且释放锁;然后当其他线程调
(也有说是等待set,不是queue)
6 notify或者notifyall以后,会选择从等待队列中唤醒任意一个线程
7 而执行完notify方法以后,并不会立马唤醒线程,原因是当前线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令之后( 这一细节非常重要, 不然就会出现理解上的黑洞,面试的时候容易被问死,但是问死,被鄙视,但是自己却仍然不知道。。(因为大部分面试官不会告诉你答案,只会心里嘲笑你暗讽你的无知, 我曾经因为这个很多次面试失败)),也就是被释放之后,处于等待队列的线程就可以开始竞争锁了。
notify执行之后立马唤醒线程吗
其实hotspot里真正的实现是退出同步块的时候才会去真正唤醒对应的线程,不过这个也是个默认策略,也可以改的,在notify之后立马唤醒相关线程。
参考 https://www.jianshu.com/p/6b9f260470a2 感觉到7 竟然是部分正确的~~
(可见,wait或者 notify 都不是一个单一的简单的操作,有一定的规则)
8 锁对象,就是被线程监视的对象,可以是任意的对象或者类,甚至class对象(比如Integer.class这样的)也可以;但是一般要求是简单的类,不要有复杂的逻辑,使用复杂的类作为锁对象不是不可以,而是完全没必要,不然实例化它会比较好性能; 通常使用Object作为对象,这样使用: private Object lock;.. lock = new Object();
9 对于任何的锁对象,其实是利用了Object的底层实现,Object的底层是比较复杂的,在特定时候, Object对象可以创建两个关键的对象:即所谓的线程等待队列(又称同步队列,又称waitSet)、线程
10 线程等待获取锁的时候,进入线程等待队列,貌似是无序的。被唤醒的时候,貌似也是无序的,看起来就像是随机的一样!
我对上图其实有所不解;不知道上面的对象Object是什么东东,感觉应该和 监视器(Monitor)是同一个东西吧。感觉不太准确!!!
11 synchronized关键字会涉及到一些jvm指令,比如monitor.enter,monitor.exit 这个其实又是结合了Object的native即C++来实现的,具体还比较复杂, 所以这一切才会显得总是你们的扑朔迷离~@!暂不细表
上图画得不咋地,但是可以看到有 一个cxq(又称是_EntryList), 一个WaitSet,分别是 线程获取对象锁失败 和 线程执行锁对象的wait方法之后 自动进入的区域;线程从waitSet 进入cxq 应该是其他线程notify的结果, 就是说线程被notify只能仅仅表明线程已经ready了,即表示可以重新去获取锁; 但是呢,这不是重新进入? 就是说不会重新执行wait 之前的同步代码, 而是接着wait 代码指令执行下一个指令!!
12 wait 有个2个重载的有参数的方法,会自动释放锁。 相当于自动唤醒,其实是底层实现的 :
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
public final native void wait(long timeout) throws InterruptedException;
13 一个 synchronized就限定死了 其限制的代码块最多只能有一个线程在执行, 绝对不会超过一个!! 这就是为什么wait() 方法使得当前线程陷入等待状态的同时,必须要释放锁,使得其他线程可以进入(然后进行其他操作), 不然其他线程就永远进入此代码块! 那么此时此线程也无法得到唤醒,那么相当于所有线程都无法再次执行此代码块; 那么后果很严重啊!!
14 wait() 必须配合notify()、notifyAll() 来使用,否则 线程永远无法醒来~~
15 假设有三个线程执行了obj.wait( ),那么obj.notifyAll( )则能全部唤醒tread1,thread2,thread3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,tread1,thread2,thread3只有一个有机会获得锁继续执行,例如tread1,其余的需要等待thread1释放obj锁之后才能继续执行。
———— 理解这个非常关键!!也是非常难注意到的!! 就是说比如三个线程t1,t2,t3; t1 首先获得了锁,然后执行到了wait语句,陷入了等待( 又有说法是进入另外一个 wait 临界区~~~),然后释放了锁,然后t2 得以进来,但是也执行到了wait,于是也陷入了等待,同理t3,甚至可能还有很多其他的线程; 那么当某一个时刻, 另外一个线程t999进来了(因为此时其他的线程都陷入了等待,所以都没有占有锁,都释放了锁)( 可能是 同一个锁对象的 其他的代码块),它没有进行等待,它进行一系列操作之后,准备通知其他的线程, 但是不知道通知几个 线程比较好, 于是调用了 notifyAll 通知所有其他线程, 可不可以呢? 可以的吧; jvm 并没有限制,那么其他线程会在当前线程执行完毕当前同步代码块,退出所谓的临界区之后(why, 下文有讲到),其他所有的线程得以有机会获得锁对象,但是 他们依然得进行竞争, 也就是说 尽管他们都似乎被notify了,但是锁对象 只能有一个线程获得,是这样吗??? 如果是这样,那么是哪个线程呢? 我估计是随机唤醒了一个(其实可能有一个竞争锁的过程),那么它执行完后呢,其他被notify的线程继续竞争锁, 然后竞争获胜的得以继续执行,以此类推; 那么 假设此时还有线程处于 synchronized处, 也就是还没进入同步代码块,根本还没到 wait 方法这一行, 那么两个地方等待的线程都是等待,但是他们的状态其实是不太一样的, 那么他们是怎么一个竞争关系呢?
16 当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此,thread1,thread2,thread3虽被唤醒,但是仍无法获得obj锁。直到调用线程退出synchronized块,释放obj锁后,thread1,thread2,thread3中的一个才有机会获得锁继续执行。
17 wait notify 最终底层的park/unpark方法实现的? 不是native 的c++ 吗? 其实park 底层也是native 也是c++吧
notifyAll是怎么实现全唤起所有线程
或许大家立马想到这个简单,一个for循环就搞定了,不过在JVM里没实现这么简单,而是借助了monitorexit,上面提到了当某个线程从wait状态恢复出来的时候,要先获取锁,然后再退出同步块,所以notifyAll的实现是调用notify的线程在退出其同步块的时候唤醒起最后一个进入wait状态的线程,然后这个线程退出同步块的时候继续唤醒其倒数第二个进入wait状态的线程,依次类推,同样这这是一个策略的问题,JVM里提供了挨个直接唤醒线程的参数,不过都很罕见就不提了。
参考 https://www.jianshu.com/p/6b9f260470a2 这里的唤醒似乎是有序的,我还以为是随机的呢~从而感觉说法6 也是有疑问的, 世界观被颠覆~~
wait的线程是否会影响load
这个或许是大家比较关心的话题,因为关乎系统性能问题,wait/nofity
是通过JVM里的 park/unpark
机制来实现的,在Linux下这种机制又是通过pthread_cond_wait/pthread_cond_signal
来玩的,因此当线程进入到wait状态的时候其实是会放弃cpu的,也就是说这类线程是不会占用cpu资源
wait和notify为什么要放在synchronized里面
wait方法的语义有两个,
释放当前的对象锁、
使得当前线程进入阻塞队列,
而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。( wait 必须先 获取对象锁,然后才能够 释放,这个很好理解吧。 )
notify也一样,它是唤醒一个线程,所以需要知道待唤醒的线程在哪里,就必须找到这个对象获取这个对象的锁然后去到这个对象的等待队列去唤醒一个线程。(知道了锁对象,就可以获取对象的锁(有点绕啊),反正只要获取到了锁,就可以获取锁对象对应的正在等待的同步队列。但因为synchronized需要结合锁对象使用, 单独也无法使用一个锁对象,也就是说没有其他的关键字了,使用synchronized是最恰当的。所以必须要结合synchronized来调用锁对象的notify方法)
为什么 条件+wait 调用的时候, 需要while 而不是if ?
在while循环里而不是if语句下使用wait,这样,会在线程暂停恢复后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。
—— 这里说的其实是需要进行某些条件判断然、满足一定条件后才进行wait的时候 需要注意的事项;不过 这里写得过于简略了,感觉实际情况可能比这个更加复杂,没有搞清楚原理的话,其实就是没理解,面试的时候容易被问死,就是说不清。 那么我的理解是:参看上面的15条,如果 t1,t2,t3 被t999唤醒了,那么它们虽然不会同时执行,但是他们已经是处于即将执行下一行语句的状态,假设t1 竞争或者得以立即执行,那么t1 执行完后 可能会改变条件,假设接下来是t2 开始执行,但是如果条件是if 包围wait的话,那么 t2 是不是应该立即执行下一行呢? 那么t2 毫无疑问的会跳出这个if 语句块,不会进行再次判断, 那么这个时候 可能就会有一定问题, 什么问题呢? 就是说 if 的条件已经不满足了,这样就会导致逻辑上的错误; 从而引起bug!! 但是如果我们把if 改为while, 那么就不会有这个问题!! 因为 线程被唤醒之后,下一个语句即使不是条件判断,但是它一定还会进行一次的 条件判断, 如果发现条件已经改变了, 那么它还是会进行wait!!~~
———————— ~~我的文章也许看起来很啰嗦,但是如果你仔细看, 你一定会有所收获吧~~ ————————
部分摘抄于:
https://blog.csdn.net/YYZZHC999/article/details/100543520
另外参考:
https://blog.csdn.net/achellies/article/details/7094467
https://blog.csdn.net/jianiuqi/article/details/53448849
https://www.jianshu.com/p/6b9f260470a2
https://www.bbsmax.com/A/nAJv1eEYzr/ 这个牛逼,到到了c++ 源码层面,果然程序员还是c++的最厉害。
https://www.cnblogs.com/geektcp/p/10589507.html 对于这样的选手,只能甘拜下风了
https://www.cnblogs.com/kundeg/p/8422557.html 都是大神