了解一个东西最好的方式,就是去看源码,只要源码不会骗你。
解惑:
下面罗列的这些文章,看一遍,反正每篇文章的出发点都不同,多少都有点问题,有的说的也互相冲突。
- 一题带你彻底理解 sleep() 和 wait()
- 多线程小抄集(断)
- 深入JVM锁机制1-synchronized
- 深入JVM锁机制2-Lock
- 一张图让你看懂JAVA线程间的状态转换
- 并发编程锁之ReentrantLock总结
- 并发编程锁之synchronized总结
- 死磕Synchronized底层实现--重量级锁
- Java的wait()、notify()学习三部曲之一:JVM源码分析
- JVM源码分析之Object.wait/notify实现
下面都是我原来不成熟的想法,没删是提醒自己要懂得感恩。
昨天看了一篇朋友圈的分享,解释sleep()和wait(),题目做对了,但是文章的解释罗列了一堆网上的copy,看的我云里雾里,写文章的人啊,要对自己写的负责啊,哪怕是语法上也一定要准确啊。
一题带你彻底理解 sleep() 和 wait()
反正我读的时候有2个地方糊涂了。
比如:
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中.
我看了2遍才理解,你好好看说的确实没问题,就是两个它放在一句上了,一下子就把人整串了。
今天参考别的文章又看了一遍,这话懂的人确实读了没毛病,如果给不懂的人看的话,确实像我一样,先看的时候会整串了,我觉得修改成下面这样会好一些:
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中;获得执行权限的线程在执行过程中调用wait()方法,它才会重新回到等待池中.
还有:
(notify 后,当前线程不会马上释放该对象锁,wait 所在的线程并不能马上获取该对象锁,要等到程序退出 synchronized 代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁)
wait 所在的线程并不能马上获取该对象锁,wait所在的线程也才可以获取该对象锁==》线程都wait了,不是先进锁池吗,大家统一去竞争吗?
12月5号我是这么理解的:
其实人家说的也没毛病,哈哈,我错了,线程被唤醒后,应该是进入竞争资源的状态,竞争失败才会进锁池。被唤醒的线程应该跟锁池上的线程一起去竞争资源,竞争失败都进锁池里面。
反正我个人觉得,写文章的时候至少要认真负责,最好把复杂的概念用图表 用流程图等辅助手段说明白了,不用真的去罗列文字,因为这东西毕竟不是笔记,给自己看的,你说你辛苦写半天,完了把人误导了,你亏不亏。
我膨胀了,保留要删除的文字是提醒自己学会感恩。
正确的文章姿势:
一张图让你看懂JAVA线程间的状态转换
线程间的状态转换:
新建(new):新创建了一个线程对象。
可运行(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
运行(running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
- 死亡(dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
其实上面那几篇文章如果你看过的话,最重要的就是这几个图。
sync线程状态图
另一个线程状态图:
sync的线程转换图
sync锁深度解析图
待明确的问题:
1 等待队列、锁池的数据结构到底张什么样子?
- 锁池: contentionList+entryList。
- contentionList是一个lock-free的数据结构,基本上就是node节点,节点的指针指向下一个节点,有头尾两个节点,加入队列的线程通过cas修改头节点,出队的通过owner线程(拥有执行权限的线程)从尾节点取出,扔到entryList队列里面去。
- entryList:owner线程(拥有执行权限的线程)会操作这个对列,干两件事,一个是将一批或者一个等待的节点(一个还是一批存疑啊)从contentionList挪过来,另一个是将entryList的头节点指向OnDeck,OnDeck获得抢锁的权限,与人品好刚进来就能抢锁的线程一起争抢锁。entryList是FIFO的队列。
- 锁池设置成这种结构就是为了减少线程并发对队列的争用,所有未抢到锁准备进入锁池的线程通过cas方式去循环更新contentionList的头节点,只有owner线程偶尔去操作下contentionList,主要操作entryList,这样就减少了队列的争用。
- 等待队列:WaitSet。
数据结构应该就是个Set(存疑)吧,owner线程执行notify()方法,随机选择一个线程扔到entryList;执行notifyaAll()方法唤醒所有线程进入entryList;
其实是根据底层策略的。
2 线程被notify唤醒后,到底会不会先进锁池呢?
线程被notify唤醒后,会被owner从WaitSet移动到cxq或EntryList中去
其实是根据底层策略的,默认策略是2 如下:
如果EntryList是空队列,则iterator单个节点构成一个双向环形链表,然后_EntryList指向该节点;==》就是进入EntryList
如果EntryList不为空,通过CAS将iterator入队cxq,并将_cxq指针指向iterator; ==》就是进入cxq
贴一下别人总结的:
根据QMode的不同,有不同的处理方式:
- QMode = 2,并且_cxq非空:取_cxq队列排头位置的ObjectWaiter对象,调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,此处会立即返回,后面的代码不会执行了;
- QMode = 3,并且_cxq非空:把_cxq队列首元素放入_EntryList的尾部;
- QMode = 4,并且_cxq非空:把_cxq队列首元素放入_EntryList的头部;
- QMode = 0,不做什么,继续往下看;
只有QMode=2的时候会提前返回,等于0、3、4的时候都会继续往下执行:
如果_EntryList的首元素非空,就取出来调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回;
如果_EntryList的首元素为空,就取_cxq的首元素,放入_EntryList,然后再从_EntryList中取出来执行ExitEpilog方法,然后立即返回;
3 锁池上的线程到底是怎么被唤醒去竞争资源的?
唤醒的是处在锁池的entryList线程,owner线程执行完逻辑,准备释放锁前,将Entry List的head赋值给OnDeck,使其具备抢锁权限。
4wait(long)和wait()有啥区别?
Object类里面源码如下:
public final void wait() throws InterruptedException {
wait(0);
}
native的wait方法我找了别人的截图(存疑,没去看源码):
反正看了这么多,我最大的感受是,如果有时间有经历就看看源码吧,二道贩子的知识或多或少会有一些作者的认知在里面。