学习目标:
- 线程的状态有哪些
- 掌握synchronized的基本用法和底层原理
monitor,管程锁,偏向锁,锁变更,锁撤销
- 锁消除
- 区分join,yeild
- 区分wait和sleep
- await和notify的标准用法
- 守护线程,优雅停止线程和stop
学习内容:
线程的状态有哪些
synchronized的基本用法和底层原理
synchronized的底层实现是jvm的Object对象和linux系统的monitor共同实现的,每一个monitor分成Entry Set.The Owner,wait Set三个部分.其中entry Set记录的是这个对象有多少个线程在排队,The Owner是当前拥有者,Wait Set是等待的线程.
synchronized的Object指的就是对象锁中的对象,每一个object对象都有一个对象头(64位).其中最后两位记录的是锁状态.其中00(轻量级锁,也就是偏向锁) 01(正常无锁) 10是重量级锁(也就是管程锁).11是GC标记.
下面我们看一段代码
private Object o=new Object();
public void A(){
for(int i=0;i<30;i++>){
synchronized(o){
//TODO A
}
}
}
public void B(){
for(int i=0;i<30;i++>){
synchronized(o){
//TODO B
Thread.sleep(1s)
}
}
public void C(){
synchronized(o){
//TODO B
Thread.sleep(1s)
}
}
}
main(){
//用线程的方式启动A
t1= new Thread().A();
//等待t1结束
t1.join();
//用线程的方式启动B
B();
//用线程的方式启动C
for(int i=0;i<60;i++>){
C();
}
}
首先执行A(), 这个时候Object的状态是01,也就是正常,然后会判断前面的62位,发现这里只有一个obj自己的32位的hashcode.就会开始去linux系统申请关联一个monitor.然后把这个monitor的index信息以及obj的hashcode记录在线程A里面,把线程A的线程id记录在obj的header里面.同时状态变成了00,也就是偏向锁.
下一步就是修改monitor的内容了,把自己的线程id加入monitor的entryset,同时把ower设置成A自己.然后开始执行第一次循环的doA的内容,执行后进入第二个循环,此时先去判断obj的锁状态,是00的偏向锁,就开始比较Thread的id和obj里面记录的是否一致,发现是一致的,就不去访问monitor了,至今执行doA().也就是所谓的偏向锁.
当A执行完成.会把A从monitor里面释放掉,原理是synchronized的进行,class进行指令编译的时候,包括了在doA前面申请以及修改monitor部分和在doA以后修改monitor的ower和entrySet部分,以及还原Obj的对象头hashcode.当B开始执行的时候发现此时是01无锁,但是有关联monitor,就会复用monitor然后就会重复A的操作,然后执行doB.然后执行了sleep(1秒).此时obj偏向记录的是B,偏向计数0(每次进入的值不是偏向锁自身的时候就+1).monitor被一个sleep的B持有了.
在B启动的同时C也启动了,C开始循环用锁30次,首先进入后比较obj发现此时是00偏向锁然后比较发现持有锁的是B,就从B那里获取monitor的地址,找到monitor发现此时owner是B,C1就会把自己也放入到entrySet,.....C20放入到entrySet,当执行到第21次的时候偏向计数达到了临界值,JVM认为自己偏向错了,把偏向对象变成了线程C21,同样把B存的hashcode和monitor地址给了C21.其他将继续.这就是锁变更.
当执行到41次的时候JVM认为这个偏向锁压根就不应该有,所以就把偏向锁取消掉了,状态变成10,前面存的内容变成了monitor的地址.也就是变成了管程锁(重量级锁).也就是偏向锁的撤销.
再往后执行到C42~C60的时候就都是对比monitor的owner然后加入entrySet了,当B的第一次sleep执行完成以后,开始释放锁,会从entrySet里面随机选一个线程出来执行,同时B执行到了第二次的synchronized会在entrySet里面重新加入一个自己,开始竞争.
一直到entrySet里面所有的内容都结束了!!!
锁销除
void A(){
Object o=new Object();
for(//100次){
synchronized(o){doSomething()}
}
}
当一个代码块被运行多次的时候就会启动jvm的jmm.也就是及时编译.即时编译的时候会发现此时的锁是多余的,因为obj是个局部变量注定只能被自己的线程持有,就会把这个锁优化掉,就是锁销除.
join,Future,yeild
join说的是主线程等待,future类似,也是主线程等待,底层是wait,notifyAll的保护性暂停机制,yeild是线程把monitor的ower变成null,同时从entrySet随机取一个出来.
区分wait和sleep
- wait会释放锁,sleep不会
wait是管程锁实现,也就是说一定obj.wait();而且这个obj一定是synchronized(obj){}的,确保有synchronized申请到了monitor(管程), wait()是将monitor的ower重置为null,把线程id从entrySet移入waitSet().同时重enterySet里面启动一个线程变为owner执行.对obj的锁是释放的.当遇到obj,notify的时候就会把waitSet()里面的拿出来放到entrySet里面.等待下一次竞争.
notify()或者notifyAll()调用时并不会真正释放对象锁, 必须等到synchronized方法或者语法块执行完才真正释放锁.
public void test()
{
Object object = new Object();
synchronized (object){
object.notifyAll();
while (true){
}
}
}
如上, 虽然调用了notifyAll, 但是紧接着进入了一个死循环。这会导致一直不能出临界区, 一直不能释放对象锁。所以,即使它把所有在等待池中的线程都唤醒放到了对象的锁池中,但是锁池中的所有线程都不会运行,因为他们始终拿不到锁。如果调用的是wait(时间)的话时间结束以后自动把线程从waitSet放回到entrySet,中途如果有notify的话就会直接放回到entrySet.
而sleep是作用于线程的,直接调用的是内核线程wait方式.所以不会释放锁.同样他的操作对象是Object,也就是任意的都可以.常用的就是Thread.
- wait唤醒的时候不会马上执行,也不会报错,sleep被唤醒的时候会抛出异常,而且马上执行
wait被notify的时候会被放入entrySet等待下一次的锁争抢,sleep被interrupt的时候会被唤醒,然后抛出interruptException.此时可以catch异常继续执行,也可以抛出异常不执行剩余的部分,sleep的对象还可以用interrupted和isInterrupted()判断当前是否睡眠,二者区别在于isInterrupted()只获取状态而interrupted()获取状同时还会把线程sleep状态给变成false;
await和notify的标准用法
sychonizised(lock){
while(条件不成立){
lock.wait();
}
//dosomething
}
//另一个线程
synchronized(lock){
lock.notifyAll();
}
用while而不是if让判断可以反复运行,用notifyAll唤醒全部防止虚假唤醒.
如何优雅的stop
其实就是stop的时候会可能造成锁死.所以就不用粗暴的stop了,而采用代码内部if(flag)的方式,通过改变flag的值当代码执行到判断的 时候就会跳过逻辑了,这样做的好处就是能够让代码逻辑顺利的以阶段性结束而不是想停在哪里就停在哪里.
守护进程
不设置守护进程的时候主线程正常在自己执行完就会结束,不会等待子线程,但是java进程不会结束
设置子线程是守护线程, t1,setDaemon(true),当主线程(非守护线程)结束的时候 子线程会强制结束
学习产出:
2021.06.30