Java并发
2024-11-17 08:49:19 4 举报
AI智能生成
Java并发是指在Java语言中实现多个任务同时执行的能力。在多线程编程中,一个线程的执行不会干扰另一个线程的执行,这有助于提高程序的运行速度和性能。Java提供了一系列并发编程的工具,如线程、线程池、锁、信号量等,方便开发者进行并发编程。然而,并发编程也可能带来一些挑战,如线程安全问题、死锁、资源争用等,开发者需要掌握并发编程的相关知识,以确保程序的正确性和稳定性。
作者其他创作
大纲/内容
IO密集型线程池具体计算公式
单个任务总时间=CPU运算时间+IO等待时间,当任务A在等待IO时,CPU可以切到任务B进行CPU运算。如果你想拉满CPU利用率,那理想线程数 = ((CPU运算时间+IO等待时间) / CPU运算时间) * CPU核心数 ;当你认为CPU和IO时间相等时,这时就是2CPU核心数;但是在常规的Web场景下,IO时间总是远大于CPU时间,比如一个简单的数据库查询,计算可能只有0.1ms,IO则可能达到2ms,这样一算,得21CPU核心数;Tomcat和Dubbo默认核心线程数都是200,而不是简单的2*核心数。真实场景下,每个接口/任务可能时间占比和执行量都不一样,应该通过压测来确定。
时钟访问周期
L1/L2/L3时钟访问所需周期
1.L1:4个是时钟周期
2.L2:10个时钟周期
3.L3:52个时钟周期
Linux OS的上下文切换需要花费5000-20000个时钟周期
1.L1:4个是时钟周期
2.L2:10个时钟周期
3.L3:52个时钟周期
Linux OS的上下文切换需要花费5000-20000个时钟周期
什么是虚假唤醒
什么是虚假唤醒?
当一定的条件触发时会唤醒很多在阻塞态的线程,但只有部分的线程唤醒是有用的,其他线程的唤醒是多余的。
比如说卖货,如果本来没有货物,突然进了一间货物,这时所有的顾客都被通知了,但是只能一个人买,所以其他人都是无用的通知
if (某个条件不成立) {
wait()
}
// 业务操作。。。。。
为什么if会出现虚假唤醒?
1.因为if只会执行一次,执行完会接着向下执行if(){}后边的逻辑
2.而while不会,直到条件满足才会向下执行while(){}后边的逻辑
如何避免虚假唤醒
使用while循环去循环判断一个体哦啊金,而不是使用if只判断一次体哦阿健,即wait()要在循环中
当一定的条件触发时会唤醒很多在阻塞态的线程,但只有部分的线程唤醒是有用的,其他线程的唤醒是多余的。
比如说卖货,如果本来没有货物,突然进了一间货物,这时所有的顾客都被通知了,但是只能一个人买,所以其他人都是无用的通知
if (某个条件不成立) {
wait()
}
// 业务操作。。。。。
为什么if会出现虚假唤醒?
1.因为if只会执行一次,执行完会接着向下执行if(){}后边的逻辑
2.而while不会,直到条件满足才会向下执行while(){}后边的逻辑
如何避免虚假唤醒
使用while循环去循环判断一个体哦啊金,而不是使用if只判断一次体哦阿健,即wait()要在循环中
什么是线程饥饿
一个或多个线程因为种种原因无法获得锁需要的资源,导致线程一直无法执行的状态。一直有线程级别高的占用资源,线程级别低的一直处在饥饿状态。
比如ReentrantLock显示锁里提供的不公平锁机制,不公平锁能够提高吞吐量但不可避免的会造成某些线程的饥饿。
线程饥饿是指线程一直无法获得所需要的资源导致任务一直无法执行的一种活性故障。
我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级无法得到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如果那个占用资源的线程结束了并释放了资源。
线程饥饿的一个典型例子就是在高争用环境中使用非公平模式的读写锁,读写锁默认情况下采用非公平调度模式,如果这些线程对锁的争用程度比较高,有可能会出现读锁总是抢先执行,而写锁始终无法得到的情况,导致一直无法更新数据,非公平锁可以支持更高的吞吐率,也可能导致某些线程始终无法获得资源锁。在高争用环境中,由于线程优先级设置不当,可能会导致优先级低的线程一直无法获得CPU执行权,出现了线程饥饿的情况
饥饿与死锁的区别:
线程处于饥饿时因为不断有优先级高的线程占用资源,当不再有高优先级的线程争抢资源时,饥饿状态将会自动解除
产生饥饿的原因:
1.高优先级线程抢占资源
2.线程在等待一个本身也处于永久等待完成的对象
3.线程被永久阻塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问
比如ReentrantLock显示锁里提供的不公平锁机制,不公平锁能够提高吞吐量但不可避免的会造成某些线程的饥饿。
线程饥饿是指线程一直无法获得所需要的资源导致任务一直无法执行的一种活性故障。
我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级无法得到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如果那个占用资源的线程结束了并释放了资源。
线程饥饿的一个典型例子就是在高争用环境中使用非公平模式的读写锁,读写锁默认情况下采用非公平调度模式,如果这些线程对锁的争用程度比较高,有可能会出现读锁总是抢先执行,而写锁始终无法得到的情况,导致一直无法更新数据,非公平锁可以支持更高的吞吐率,也可能导致某些线程始终无法获得资源锁。在高争用环境中,由于线程优先级设置不当,可能会导致优先级低的线程一直无法获得CPU执行权,出现了线程饥饿的情况
饥饿与死锁的区别:
线程处于饥饿时因为不断有优先级高的线程占用资源,当不再有高优先级的线程争抢资源时,饥饿状态将会自动解除
产生饥饿的原因:
1.高优先级线程抢占资源
2.线程在等待一个本身也处于永久等待完成的对象
3.线程被永久阻塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问
什么是活锁
任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试、失败、尝试、失败。在这期间线程状态会不停的改变
活锁与死锁的区别:
1.死锁会阻塞,一直等待对方释放资源,一直处于阻塞状态;活锁会不停的改变线程状态尝试获得资源。活锁有可能自行解开,死锁则不行
活锁具有两个特点:
1.第一个是线程没有阻塞,始终在运行中,所以叫做活锁,线程是获得,运行中的
2.第二个是程序却得不到进展,因为线程始终重复同样的无效事情
活锁与死锁的区别:
1.死锁会阻塞,一直等待对方释放资源,一直处于阻塞状态;活锁会不停的改变线程状态尝试获得资源。活锁有可能自行解开,死锁则不行
活锁具有两个特点:
1.第一个是线程没有阻塞,始终在运行中,所以叫做活锁,线程是获得,运行中的
2.第二个是程序却得不到进展,因为线程始终重复同样的无效事情
什么是死锁
什么是死锁?
死锁可以这样理解,就是互相不让步不放弃,同时需要对方的资源。造成互相不满足资源需求,也不放弃自身已有资源。
死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果外力作用,它们都将无法推进下去,如果系统资源重组,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺优先的资源而陷入死锁。
产生死锁的主要原因是:
1.因为系统资源不足
2.进程运行推进的顺序不合理
3.资源分配不当等
死锁产生的4个必要条件:
1.互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待
2.不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)
3.请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不释放
4.循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁
死锁的四个条件:1.互斥 2.保持锁并请求锁 3.不可抢夺 4.循环等待
死锁可以这样理解,就是互相不让步不放弃,同时需要对方的资源。造成互相不满足资源需求,也不放弃自身已有资源。
死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果外力作用,它们都将无法推进下去,如果系统资源重组,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺优先的资源而陷入死锁。
产生死锁的主要原因是:
1.因为系统资源不足
2.进程运行推进的顺序不合理
3.资源分配不当等
死锁产生的4个必要条件:
1.互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待
2.不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)
3.请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不释放
4.循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁
死锁的四个条件:1.互斥 2.保持锁并请求锁 3.不可抢夺 4.循环等待
内存编织
对象内存布局
开启指针压缩的情况下的内存布局。
Markword 占8B
开启指针压缩:类型指针占4B
变量a占4B
不需要对齐
总共对象大小占16B
Markword 占8B
开启指针压缩:类型指针占4B
变量a占4B
不需要对齐
总共对象大小占16B
关闭指针压缩的情况下的内存布局
MarkWord:8B
关闭指针压缩:类型指针占8B
变量a占4B
需要对齐,补充4B
总共对象大小占24B
MarkWord:8B
关闭指针压缩:类型指针占8B
变量a占4B
需要对齐,补充4B
总共对象大小占24B
虚表
C++ vtable
klass content
JVM vtable
我们随便定义的一个类,它有没有JVM虚表呢?其实是有的。那是哪些方法的内存地址呢?回答这个问题前先得搞明白:什么样的方法回存入虚表,只有public、protected类型的,且不被static、final修饰的方法才能被多态调用,才会进入虚表。因为Java中所有的类都是Object的子类,所以Object中满足这个条件的方法都会在每个类的虚表中.
Object中的finalize、equals、toString、hashCode、clone方法也会存入虚表
klass content
JVM vtable
我们随便定义的一个类,它有没有JVM虚表呢?其实是有的。那是哪些方法的内存地址呢?回答这个问题前先得搞明白:什么样的方法回存入虚表,只有public、protected类型的,且不被static、final修饰的方法才能被多态调用,才会进入虚表。因为Java中所有的类都是Object的子类,所以Object中满足这个条件的方法都会在每个类的虚表中.
Object中的finalize、equals、toString、hashCode、clone方法也会存入虚表
我们可以通过Test的内存首地址加上0x1b8计算得出一个内存地址,
可以看到,Test类当中也存在着5个方法位于虚表当中
接着我们再来查看Object的方法内存地址,finalize、equals、toString、hashCode、clone也存在于虚表中
对应的HotSpot中的klass.hpp
从HotSpot源码层main剖析Java的多态实现原理
Oop三大机制为什么就是封装、继承、多态。没增加也没减少。
在HotSpot中,多态需要动态绑定才能得以实现,而绑定通俗一点讲就是让不同的对象对同一个函数进行调用,或者反过来讲,就是让同一个函数与不同的对象绑定起来,所以多态得以实现的一个大前提就是,编程语言必须是面向对象的。同时,函数与对象相互绑定,意味着函数也是属于对象的一部分,这便具备了封装的特性。因为有了封装,才有了对象。同时,一个函数能够绑定多个对象,意味着对各不同的对象具有相同的行为,这是继承的含义。
因此,面向对象的三大特性缺一不可。封装与继承其实是为了多态准备的,或者说,封装与继承成全了多态,多态让封装与继承的意义最大化
在HotSpot中,多态需要动态绑定才能得以实现,而绑定通俗一点讲就是让不同的对象对同一个函数进行调用,或者反过来讲,就是让同一个函数与不同的对象绑定起来,所以多态得以实现的一个大前提就是,编程语言必须是面向对象的。同时,函数与对象相互绑定,意味着函数也是属于对象的一部分,这便具备了封装的特性。因为有了封装,才有了对象。同时,一个函数能够绑定多个对象,意味着对各不同的对象具有相同的行为,这是继承的含义。
因此,面向对象的三大特性缺一不可。封装与继承其实是为了多态准备的,或者说,封装与继承成全了多态,多态让封装与继承的意义最大化
C++是如何实现多态的?
多态的实现,现在几乎所有的编程语言都是基于虚表实现的,英文vtable。C++的虚表位于new创建的对象的头部。虚表里面存储的是什么呢?是虚函数。
多态的实现,现在几乎所有的编程语言都是基于虚表实现的,英文vtable。C++的虚表位于new创建的对象的头部。虚表里面存储的是什么呢?是虚函数。
因为hotspot主要是用C++.,Java的类对应的C++对象,会有C++级别的虚表。知道了虚表,再了解虚表分发就容易多了。虚表分发,其实就是通过虚表内存地址拿到虚表记录,然后通过函数名+内涵参数信息及返回值信息的签名去虚表中找。因为是从前往后找,所以如果子类重写了父类的方法,会调用子类的方法。虚表是用数组实现的
JVM中的虚表
JVM的虚表和C++中的虚表有点不太一样,不一样体现在哪儿呢?研究虚表可以从三个方面入手:
1.虚表在哪儿
2.虚表是用什么结构实现的
3.虚表分发机制是怎样的
JVM的虚表也是用数组实现的。Java的类,JVM中对应的C++对象是Klass模型。Java的对象,JVM中对应的C++对象是oop模型。C++中的虚表在对象头中,而JVM的虚表在Klass模型的头部,即Java类对象的头部。这点区别一定要记住,这样才能理解Java对象的内存布局,,如图所示,翻译下来就是:
//在实现中使用oop/ class二分法的一个原因是
//我们不希望在每个对象中都有c++的vtbl指针。因此,
// normal oops没有任何虚函数。相反,他们
//将所有的“虚”函数转发到它们的类中
//调用vtbl,并根据对象的值进行c++分派
//实际类型。(请参阅oop.inline.hpp了解一些转发代码。)
//所有实现此分派的函数都以“oop_”作为前缀!
HotSpot不想在每个对象中都存储一份虚表地址,它将虚表存放在了Klass的头部,而非对象的头部。可以节省一个对象的占用空间大小(个人想法)。再加上Object是所有类的父类,也就是说,每个对象都会有一个虚表地址。一般人可能不在乎这个,但是作为HotSpot的人员来说,对象的内存空间占用大小做到了极致。
有人好奇,我们随便定义的一个类,它有没有虚表呢?其实是有的。那是哪些方法的内存地址呢? 回答这个问题前先得搞明白:什么样的方法会存入虚表。只有public、protec类型的,且不被static、final修饰的方法才能被多态调用,才会进入虚表。因为Java中所有的类都是Object的子类,所以Object中这个条件的方法都会在每个类的虚表当中
Object中的finalize、equals、toString、hashCode、clone方法也会存入虚表
JVM的虚表和C++中的虚表有点不太一样,不一样体现在哪儿呢?研究虚表可以从三个方面入手:
1.虚表在哪儿
2.虚表是用什么结构实现的
3.虚表分发机制是怎样的
JVM的虚表也是用数组实现的。Java的类,JVM中对应的C++对象是Klass模型。Java的对象,JVM中对应的C++对象是oop模型。C++中的虚表在对象头中,而JVM的虚表在Klass模型的头部,即Java类对象的头部。这点区别一定要记住,这样才能理解Java对象的内存布局,,如图所示,翻译下来就是:
//在实现中使用oop/ class二分法的一个原因是
//我们不希望在每个对象中都有c++的vtbl指针。因此,
// normal oops没有任何虚函数。相反,他们
//将所有的“虚”函数转发到它们的类中
//调用vtbl,并根据对象的值进行c++分派
//实际类型。(请参阅oop.inline.hpp了解一些转发代码。)
//所有实现此分派的函数都以“oop_”作为前缀!
HotSpot不想在每个对象中都存储一份虚表地址,它将虚表存放在了Klass的头部,而非对象的头部。可以节省一个对象的占用空间大小(个人想法)。再加上Object是所有类的父类,也就是说,每个对象都会有一个虚表地址。一般人可能不在乎这个,但是作为HotSpot的人员来说,对象的内存空间占用大小做到了极致。
有人好奇,我们随便定义的一个类,它有没有虚表呢?其实是有的。那是哪些方法的内存地址呢? 回答这个问题前先得搞明白:什么样的方法会存入虚表。只有public、protec类型的,且不被static、final修饰的方法才能被多态调用,才会进入虚表。因为Java中所有的类都是Object的子类,所以Object中这个条件的方法都会在每个类的虚表当中
Object中的finalize、equals、toString、hashCode、clone方法也会存入虚表
Java是如何实现虚表分发的
JVM实现虚表分发,对应的字节码指令有两个:invokevirtual、invokeinterface。
虽然invokeinterface后面的操作数是接口方法信息。但是真正的对象会作为this传过来。所以在调用的时候,从操作数栈拿到真正的对象,然后通过对象头中的类型指针拿到TestDuotai(自己手写的一个类)的C++类对象,即Klass模型。虚表就在这个对象的头部。然后通过函数名+内涵参数信息及返回值信息的签名去虚表中找。因为是从前往后找,所以如果子类重写了父类的方法,会调用子类的方法。这就是JVM虚表分发的底层原理
JVM实现虚表分发,对应的字节码指令有两个:invokevirtual、invokeinterface。
虽然invokeinterface后面的操作数是接口方法信息。但是真正的对象会作为this传过来。所以在调用的时候,从操作数栈拿到真正的对象,然后通过对象头中的类型指针拿到TestDuotai(自己手写的一个类)的C++类对象,即Klass模型。虚表就在这个对象的头部。然后通过函数名+内涵参数信息及返回值信息的签名去虚表中找。因为是从前往后找,所以如果子类重写了父类的方法,会调用子类的方法。这就是JVM虚表分发的底层原理
管程模型
互斥与同步
互斥:同一时刻,只有一个线程干活
同步: 在互斥的基础上,保证线程协同工作
同步(Synchronization),指在一个系统中所发生的事件(event)之间进行协调,在时间上出现一致性与统一化的现象。在系统中进行同步,也被称为即时(in time)或同步化的(synchronous ,in sync).
同步,可以理解为在通信时、函数调用时、协议栈的相邻层协议交互时等场景下,发信方与收信方、主调与被调等双方的状态是否能即时保持状态一致,如果一方完成一个动作后,另一方立即就修改了自己的状态,就是同步,而异步,是指调用方发出请求就立即返回,请求甚至可能还没达到接收方,比如说放到了某个缓冲区中,等待对方取走或者第三方转交;而调用结果是通过接收方主动推送,或调用方轮询来得到。
同步还可以理解为:发出一个调用时,在没有得到结果之前,该调用就不反悔;一旦调用返回,就得到返回值。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,,所以没有返回结果。当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知或通过回调函数,让调用者响应结果
对于非阻塞情形,同步非阻塞是观察者定期主动的去查看目标对象状态。异步非阻塞是目标对象状态改变后去通知观察者做出响应处理
互斥:同一时刻,只有一个线程干活
同步: 在互斥的基础上,保证线程协同工作
同步(Synchronization),指在一个系统中所发生的事件(event)之间进行协调,在时间上出现一致性与统一化的现象。在系统中进行同步,也被称为即时(in time)或同步化的(synchronous ,in sync).
同步,可以理解为在通信时、函数调用时、协议栈的相邻层协议交互时等场景下,发信方与收信方、主调与被调等双方的状态是否能即时保持状态一致,如果一方完成一个动作后,另一方立即就修改了自己的状态,就是同步,而异步,是指调用方发出请求就立即返回,请求甚至可能还没达到接收方,比如说放到了某个缓冲区中,等待对方取走或者第三方转交;而调用结果是通过接收方主动推送,或调用方轮询来得到。
同步还可以理解为:发出一个调用时,在没有得到结果之前,该调用就不反悔;一旦调用返回,就得到返回值。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,,所以没有返回结果。当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知或通过回调函数,让调用者响应结果
对于非阻塞情形,同步非阻塞是观察者定期主动的去查看目标对象状态。异步非阻塞是目标对象状态改变后去通知观察者做出响应处理
基本概念
在Java1.5之前,Java语言提供的唯一并发语言就是管程,Java1.5之后提供的SDK并法宝也是以管程为基础的。除了Java之外,C/C++、C#等高级语言也都是支持管程的。
那么什么时管程呢?
见名知意,是指管理共享变量以及对共享变量操作的过程,让他们支持并发。翻译成Java领域的语言,就是管理类的状态变量,让这个类时线程安全的。
synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。操作系统中,在线程一块还有信号量机制,管程在功能上和信号量及PV操作类似,属于一种进程同步互斥工具,Java选择管程来实现并发主要还是因为实现管程比较容易。
管程对应的英文是Monitor,直译为"监视器",而操作系统领域一般翻译为"管程"。在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型,现在正在广泛使用的是MESA模型
在Java1.5之前,Java语言提供的唯一并发语言就是管程,Java1.5之后提供的SDK并法宝也是以管程为基础的。除了Java之外,C/C++、C#等高级语言也都是支持管程的。
那么什么时管程呢?
见名知意,是指管理共享变量以及对共享变量操作的过程,让他们支持并发。翻译成Java领域的语言,就是管理类的状态变量,让这个类时线程安全的。
synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。操作系统中,在线程一块还有信号量机制,管程在功能上和信号量及PV操作类似,属于一种进程同步互斥工具,Java选择管程来实现并发主要还是因为实现管程比较容易。
管程对应的英文是Monitor,直译为"监视器",而操作系统领域一般翻译为"管程"。在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型,现在正在广泛使用的是MESA模型
什么是PV(Passeren 通过, Vrijgeven 释放)操作?
在操作系统中,进程是一个很要花时间理解的东西,进程通常分为就绪、运行和阻塞三个工作状态。三种状态在某些条件下可以转换,三者之间的转换关系如下:进程三个状态之间的转换就是靠PV操作来控制的。PV操作主要就是P(Passeren 通过)操作、V(Vrijgeven 释放)操作和信号量。其中信号量起到了至关重要的作用
在操作系统中,进程是一个很要花时间理解的东西,进程通常分为就绪、运行和阻塞三个工作状态。三种状态在某些条件下可以转换,三者之间的转换关系如下:进程三个状态之间的转换就是靠PV操作来控制的。PV操作主要就是P(Passeren 通过)操作、V(Vrijgeven 释放)操作和信号量。其中信号量起到了至关重要的作用
1.什么是信号量?
信号量(Semaphore),我们有时称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。信号量的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。
一般来说,信号量S>0时,S表示可用资源的数量。执行一次P操作意味着请求分配一个单位资源,因此S的值减1;
当S<0时,表示已经没有可用资源,请求者必须等待别的进程释放该类资源,它才能运行下去。而执行一个V操作意味着释放一个单位资源,因此S的值加1.
若S=0,表示有某些进程正在等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。
注意:信号量的值只能由PV操作来改变
信号量(Semaphore),我们有时称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。信号量的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。
一般来说,信号量S>0时,S表示可用资源的数量。执行一次P操作意味着请求分配一个单位资源,因此S的值减1;
当S<0时,表示已经没有可用资源,请求者必须等待别的进程释放该类资源,它才能运行下去。而执行一个V操作意味着释放一个单位资源,因此S的值加1.
若S=0,表示有某些进程正在等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。
注意:信号量的值只能由PV操作来改变
PV操作的意义
我们用信号量及PV操作来实现进程的同步和互斥。PV操作是属于进程的低级通信。
进程的同步、互斥:
同步:与其说同步,称为"协作"更好点,目标只有一个,奔着同一个目标去的,都是在大家的努力下共同完成这么一件事情。
互斥:只能有一个线程在操作共享资源
我们用信号量及PV操作来实现进程的同步和互斥。PV操作是属于进程的低级通信。
进程的同步、互斥:
同步:与其说同步,称为"协作"更好点,目标只有一个,奔着同一个目标去的,都是在大家的努力下共同完成这么一件事情。
互斥:只能有一个线程在操作共享资源
MESA模型(AQS近似于)。
管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。
举个例子,多个线程对一个共享队列进行操作。
假设线程T1要执行出队操作,但是这个操作要执行成功的前提是队列不能为空。这个队列不能为空就是管程里的条件变量。若是线程T1进入管程后发现队列是空的,那它就需要在"队列不空"这个条件变量的等待队列中等待。通过调用wait()实现。若是用对象A代表"队列不空"这个条件,那么线程T1需要调用A.wait(),来将自己阻塞。在线程T1进入条件变量的等待队列后,是允许其他线程进入管程的。再假设之后另外一个线程T2执行入队操作,入队操作成功之后,"队列不空"这个条件对线程T1来说已经满足了,此时线程T2调用A.notify()来通知A等待队列中的一个线程,此时这个线程里面只有T1,所以notify唤醒的就是线程T1,如果当这个条件变量的等待队列不至T1一个线程,我们就需要使用notifyAll().当线程T1得到通知后,会从等待队列中出来,重新进入到入口等待队列中。
管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。
举个例子,多个线程对一个共享队列进行操作。
假设线程T1要执行出队操作,但是这个操作要执行成功的前提是队列不能为空。这个队列不能为空就是管程里的条件变量。若是线程T1进入管程后发现队列是空的,那它就需要在"队列不空"这个条件变量的等待队列中等待。通过调用wait()实现。若是用对象A代表"队列不空"这个条件,那么线程T1需要调用A.wait(),来将自己阻塞。在线程T1进入条件变量的等待队列后,是允许其他线程进入管程的。再假设之后另外一个线程T2执行入队操作,入队操作成功之后,"队列不空"这个条件对线程T1来说已经满足了,此时线程T2调用A.notify()来通知A等待队列中的一个线程,此时这个线程里面只有T1,所以notify唤醒的就是线程T1,如果当这个条件变量的等待队列不至T1一个线程,我们就需要使用notifyAll().当线程T1得到通知后,会从等待队列中出来,重新进入到入口等待队列中。
wait()的正确使用
对于MESA管程来说,有一个编程范式:
```java
while(条件不满足) {
wait();
}
```
这个范式可以解决"条件曾经满足过"这个问题。唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不满足了,所以循环检验条件。MESA模型的wait()方法还有一个超时参数,为了避免线程进入等待队列永久阻塞
对于MESA管程来说,有一个编程范式:
```java
while(条件不满足) {
wait();
}
```
这个范式可以解决"条件曾经满足过"这个问题。唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不满足了,所以循环检验条件。MESA模型的wait()方法还有一个超时参数,为了避免线程进入等待队列永久阻塞
notify()和notifyAll()何时使用
满足以下三个条件时,可以使用notify(),其余情况尽量使用notifyAll():
1.所有等待线程拥有相同的等待条件
2.所有等待线程被唤醒后,执行相同的操作
3.只需要唤醒一个线程
满足以下三个条件时,可以使用notify(),其余情况尽量使用notifyAll():
1.所有等待线程拥有相同的等待条件
2.所有等待线程被唤醒后,执行相同的操作
3.只需要唤醒一个线程
三种管程模型再通知线程上的区别
Hasen模型、Hoare模型和MESA模型的一个核心区别是当条件满足后,如何通知相关线程。
管程要求同一时刻只允许一个线程执行,那当线程T2的操作使得线程T1等待的条件满足时,T1和T2究竟谁可以执行呢?
1.在Hasen模型里,要求notify()放在代码的最后,这样T2通知完T1后,T2就结束了,然后T1再执行这样就可以保证同一时刻只有一个线程执行
2.在Hoare模型里面,T2通知完T1后,T2阻塞,T1马上执行;等T1执行完,再唤醒T2。比起Hasen模型,T2多了一次阻塞唤醒操作
3.在MESA管程里,T2通知完T1后,T2还是会接着执行,T1并不会立即执行,仅仅是从条件变量的等待队列进入到入口等待队列中(但是T1再次执行时,可能条件又不满足了,所以需要循环检验条件变量)。这样的好处时:notify()代码不用放到代码的最后,T2也没有多余的阻塞唤醒操作
Hasen模型、Hoare模型和MESA模型的一个核心区别是当条件满足后,如何通知相关线程。
管程要求同一时刻只允许一个线程执行,那当线程T2的操作使得线程T1等待的条件满足时,T1和T2究竟谁可以执行呢?
1.在Hasen模型里,要求notify()放在代码的最后,这样T2通知完T1后,T2就结束了,然后T1再执行这样就可以保证同一时刻只有一个线程执行
2.在Hoare模型里面,T2通知完T1后,T2阻塞,T1马上执行;等T1执行完,再唤醒T2。比起Hasen模型,T2多了一次阻塞唤醒操作
3.在MESA管程里,T2通知完T1后,T2还是会接着执行,T1并不会立即执行,仅仅是从条件变量的等待队列进入到入口等待队列中(但是T1再次执行时,可能条件又不满足了,所以需要循环检验条件变量)。这样的好处时:notify()代码不用放到代码的最后,T2也没有多余的阻塞唤醒操作
Java语言的内置管程synchronized
Java参考了MESA模型,语言内置的管程(synchronized)对MESA模型进行了精简。MESA模型中,条件变量可以有多个,Java语言内置的管程里只有一个条件变量。Java内置的管程方案(synchronized)使用简单,synchronized关键字修饰的代码块,在编译器会自动生成相关加锁和解锁的代码,但是仅支持一个条件变量;而Java SDK并发包AQS实现的管程支持u东哥条件变量,不过JUC包里的锁,需要我们自己进行加锁和解锁操作
Java参考了MESA模型,语言内置的管程(synchronized)对MESA模型进行了精简。MESA模型中,条件变量可以有多个,Java语言内置的管程里只有一个条件变量。Java内置的管程方案(synchronized)使用简单,synchronized关键字修饰的代码块,在编译器会自动生成相关加锁和解锁的代码,但是仅支持一个条件变量;而Java SDK并发包AQS实现的管程支持u东哥条件变量,不过JUC包里的锁,需要我们自己进行加锁和解锁操作
自旋锁模型
什么是自旋锁和互斥锁?
由于CLH锁是一种自旋锁,那么自选锁是什么?
自旋锁说白了也是一种互斥锁,只不过没有强盗锁的线程会一致自旋等待锁的释放,处于busy-waiting的状态,此时等待锁的线程不会进入休眠状态,而是一直忙碌等待浪费CPU周期。因此自旋锁适用于锁占用时间短的场合。
什么是互斥锁?
互斥锁说的就是传统意义的互斥锁,就是多个线程并发竞争的时候,没有强盗锁的线程会进入休眠状态即sleep-waiting,当锁被释放的时候,处于休眠状态的一个线程会再次获取到锁。缺点就是这一系列过程需要线程切换,需要执行很多CPU指令,同样需要时间。如果CPU执行线程切换的时间比锁占用的时间还长,那么可能还不如使用自旋锁。因此互斥锁适用于锁占用时间长的场合
由于CLH锁是一种自旋锁,那么自选锁是什么?
自旋锁说白了也是一种互斥锁,只不过没有强盗锁的线程会一致自旋等待锁的释放,处于busy-waiting的状态,此时等待锁的线程不会进入休眠状态,而是一直忙碌等待浪费CPU周期。因此自旋锁适用于锁占用时间短的场合。
什么是互斥锁?
互斥锁说的就是传统意义的互斥锁,就是多个线程并发竞争的时候,没有强盗锁的线程会进入休眠状态即sleep-waiting,当锁被释放的时候,处于休眠状态的一个线程会再次获取到锁。缺点就是这一系列过程需要线程切换,需要执行很多CPU指令,同样需要时间。如果CPU执行线程切换的时间比锁占用的时间还长,那么可能还不如使用自旋锁。因此互斥锁适用于锁占用时间长的场合
SMP(Symmetric Multi-Processor, 对称多处理器结构)
它是相对非对称多处理技术而言的、应用十分广泛的并行技术。在这种架构中,一台计算机由多个CPU组成,并共享内存和其他资源,所有的CPU都可以平等地访问内存、I/O和外部中断,虽然同时使用多个CPU,但是从管理的角度来看,它们的表现就像一台单机一样。操作系统将任务队列对称地分布于多个CPU之上,从而极大地提高了整个系统的数据处理能力。但是随着CPU数量的增加,每个CPU都要访问相同的内存资源,共享资源可能会称为瓶颈,导致CPU资源浪费
它是相对非对称多处理技术而言的、应用十分广泛的并行技术。在这种架构中,一台计算机由多个CPU组成,并共享内存和其他资源,所有的CPU都可以平等地访问内存、I/O和外部中断,虽然同时使用多个CPU,但是从管理的角度来看,它们的表现就像一台单机一样。操作系统将任务队列对称地分布于多个CPU之上,从而极大地提高了整个系统的数据处理能力。但是随着CPU数量的增加,每个CPU都要访问相同的内存资源,共享资源可能会称为瓶颈,导致CPU资源浪费
NUMA(Non-Uniform Memory Access, 非一致存储访问)
将CPU分成CPU模块,每个CPU模块由多个CPU组成,并且具有独立的本地内存、IO槽口等,模块之间可以通过互联模块相互访问,访问本地内存(本CPU模块的内存)的速度将远远高于访问远地内存(其他CPU模块的内存)的速度,这也是非一致存储访问的由来。NUMA较好地解决SMP地扩展问题,当CPU数量增加时,因为访问远地内存地延时远远超过本地内存,系统性能无法线性增加
将CPU分成CPU模块,每个CPU模块由多个CPU组成,并且具有独立的本地内存、IO槽口等,模块之间可以通过互联模块相互访问,访问本地内存(本CPU模块的内存)的速度将远远高于访问远地内存(其他CPU模块的内存)的速度,这也是非一致存储访问的由来。NUMA较好地解决SMP地扩展问题,当CPU数量增加时,因为访问远地内存地延时远远超过本地内存,系统性能无法线性增加
CLH模型
CLH是一种基于单向链表的高性能、公平的自旋锁。申请加锁的线程通过前去节点的变量进行自旋。在前置节点解锁后,当前节点会结束自旋,并进行加锁。在SMP架构下,CLH更具有优势。在NUMA架构下,如果当前节点与前驱节点不在同一CPU模块下,跨CPU模块会带来额外的系统开销,而MCS锁模型更适用于NUMA架构.
什么是CLH锁?
CLH锁其实就是一种基于逻辑队列非线程饥饿的一种自旋公平锁,由于是Craig、Landin和Hagersten三位大佬的发明,因此命名为CLH锁。
为什么要学习CLH锁?
因为AQS是JUC的核心,而CLH锁又是AQS的基础,说核心也不为过,因为AQS就是用了变种的CLH锁。如果要学好Java并发编程,那么必定要学好JUC,学号JUC,必定要先学好AQS,学好AQS,那么必定先学好CLH
加锁逻辑
1.获取当前线程的锁节点,如果为空,则进行初始化
2.同步方法获取链表的尾节点,并将当前节点置为尾节点,此时原来的尾节点为当前节点的前置节点
3.如果尾节点为空,表示当前节点是第一个节点,直接加锁成功
4.如果尾节点不为空,则基于前置节点的锁值(locked == true)进行自旋,直到前置节点的锁值变为false
解锁逻辑
1.获取当前线程对应的所节点,如果节点为空或者锁值为false,则无需解锁,直接返回
2.同步方法为尾节点赋空值,赋值不成功表示当前节点不是尾节点,则需要将当前节点的locked=false解锁节点。如果当前节点是尾节点,则无需为该节点设置
CLH是一种基于单向链表的高性能、公平的自旋锁。申请加锁的线程通过前去节点的变量进行自旋。在前置节点解锁后,当前节点会结束自旋,并进行加锁。在SMP架构下,CLH更具有优势。在NUMA架构下,如果当前节点与前驱节点不在同一CPU模块下,跨CPU模块会带来额外的系统开销,而MCS锁模型更适用于NUMA架构.
什么是CLH锁?
CLH锁其实就是一种基于逻辑队列非线程饥饿的一种自旋公平锁,由于是Craig、Landin和Hagersten三位大佬的发明,因此命名为CLH锁。
为什么要学习CLH锁?
因为AQS是JUC的核心,而CLH锁又是AQS的基础,说核心也不为过,因为AQS就是用了变种的CLH锁。如果要学好Java并发编程,那么必定要学好JUC,学号JUC,必定要先学好AQS,学好AQS,那么必定先学好CLH
加锁逻辑
1.获取当前线程的锁节点,如果为空,则进行初始化
2.同步方法获取链表的尾节点,并将当前节点置为尾节点,此时原来的尾节点为当前节点的前置节点
3.如果尾节点为空,表示当前节点是第一个节点,直接加锁成功
4.如果尾节点不为空,则基于前置节点的锁值(locked == true)进行自旋,直到前置节点的锁值变为false
解锁逻辑
1.获取当前线程对应的所节点,如果节点为空或者锁值为false,则无需解锁,直接返回
2.同步方法为尾节点赋空值,赋值不成功表示当前节点不是尾节点,则需要将当前节点的locked=false解锁节点。如果当前节点是尾节点,则无需为该节点设置
https://yangsanity.me/2021/08/10/CLHLock/
加锁过程:
1.首先获得当前线程的当前节点curNode,每次获取的CLHNode节点的locked状态都为false
2.然后将当前CLHNode节点的locked状态赋值为true,表示当前线程的一种有效状态,即获取到了锁或正在等待锁的状态
3.因为尾指针tailNode总是指向了前一个线程的CLHNode节点,因此这里利用尾指针取出前一个线程的CLHNode节点,然后赋值给当前线程的前继节点predNode,并且将尾指针重新指向最后一个节点即当前线程的当前CLHNode节点,以便下一个线程到来时使用
4.根据前继节点(前一个线程)的locked状态判断,若locked为false,则说明前一个线程释放了锁,当前线程即可获得锁,不用自旋等待;若前继节点的locked状态为true,则表示前一线程获取到了锁或正在等待,自旋等待
释放锁过程:
1.首先从当前线程的线程本地变量中获取出当前CLHNode节点,同时这个CLHNode节点被后面一个线程的preNode变量指向着
2.然后将locked状态设置为false即释放了锁;
注意:locked因为被volatile关键字修饰,此时后面自旋锁等待的线程的局部变量preNode.locked也为false,因此后面自旋等待的线程结束while循环即结束自旋等待,此时也获取到了锁,这一步骤也在异步进行着
3.然后给当前线程的表示当前节点的线程本地变量重新赋值一个新的CLHNode
加锁过程:
1.首先获得当前线程的当前节点curNode,每次获取的CLHNode节点的locked状态都为false
2.然后将当前CLHNode节点的locked状态赋值为true,表示当前线程的一种有效状态,即获取到了锁或正在等待锁的状态
3.因为尾指针tailNode总是指向了前一个线程的CLHNode节点,因此这里利用尾指针取出前一个线程的CLHNode节点,然后赋值给当前线程的前继节点predNode,并且将尾指针重新指向最后一个节点即当前线程的当前CLHNode节点,以便下一个线程到来时使用
4.根据前继节点(前一个线程)的locked状态判断,若locked为false,则说明前一个线程释放了锁,当前线程即可获得锁,不用自旋等待;若前继节点的locked状态为true,则表示前一线程获取到了锁或正在等待,自旋等待
释放锁过程:
1.首先从当前线程的线程本地变量中获取出当前CLHNode节点,同时这个CLHNode节点被后面一个线程的preNode变量指向着
2.然后将locked状态设置为false即释放了锁;
注意:locked因为被volatile关键字修饰,此时后面自旋锁等待的线程的局部变量preNode.locked也为false,因此后面自旋等待的线程结束while循环即结束自旋等待,此时也获取到了锁,这一步骤也在异步进行着
3.然后给当前线程的表示当前节点的线程本地变量重新赋值一个新的CLHNode
MCS模型
MCS与CLH最大的不同并不是链表是显式还是隐式,而是线程自旋的规则不同:,CLH是在前驱节点的locked域上自旋等待,而MCS是在自己的节点的locked域上自旋等待,它解决了CLH在NUMA系统架构中获取locked域状态内存过远的问题
对比说明
1.从代码实现来看,CLH比MCS要简单得多
2.从自旋的条件来看,CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋
3.从链表队列来看,CLH的队列是隐式的,CLHNode并不时机持有下一个节点;MCS的队列是物理存在的
4.CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性
MCS如何解决了NUMA架构中的问题?
MCS锁通过在自己节点自旋,减少了NUMA系统架构中跨CPU模块获取locked域状态的内存访问,从而解决了CLH锁在NUMA架构中的问题。自旋的域不同,CLH自旋在前一个节点,如果跨内存访问的话,需要保持这个连接,而MCS则不需要
https://note.xcloudapi.com/2022/08/26/Java%E8%87%AA%E6%97%8B%E9%94%81%E3%80%81CLH%E9%94%81%E5%8F%8AMCS%E9%94%81%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/
MCS与CLH最大的不同并不是链表是显式还是隐式,而是线程自旋的规则不同:,CLH是在前驱节点的locked域上自旋等待,而MCS是在自己的节点的locked域上自旋等待,它解决了CLH在NUMA系统架构中获取locked域状态内存过远的问题
对比说明
1.从代码实现来看,CLH比MCS要简单得多
2.从自旋的条件来看,CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋
3.从链表队列来看,CLH的队列是隐式的,CLHNode并不时机持有下一个节点;MCS的队列是物理存在的
4.CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性
MCS如何解决了NUMA架构中的问题?
MCS锁通过在自己节点自旋,减少了NUMA系统架构中跨CPU模块获取locked域状态的内存访问,从而解决了CLH锁在NUMA架构中的问题。自旋的域不同,CLH自旋在前一个节点,如果跨内存访问的话,需要保持这个连接,而MCS则不需要
https://note.xcloudapi.com/2022/08/26/Java%E8%87%AA%E6%97%8B%E9%94%81%E3%80%81CLH%E9%94%81%E5%8F%8AMCS%E9%94%81%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/
1.线程机制
线程状态转换图
Linux中的原生线程操作API
创建线程,可以使用man pthread_create查看该API的使用介绍。返回值,若成功,返回0,否则返回错误编号
参数介绍:
1.thread,必传,获取线程ID,所有操作线程的API都需要这个参数
2.attr, 非必传,可以通过该参数设置线程栈大小、分离线程属性
3.start_routine,必传,线程执行函数,有类似于注册回调函数
4.arg,非必传,通过该参数给线程执行函数传参
参数介绍:
1.thread,必传,获取线程ID,所有操作线程的API都需要这个参数
2.attr, 非必传,可以通过该参数设置线程栈大小、分离线程属性
3.start_routine,必传,线程执行函数,有类似于注册回调函数
4.arg,非必传,通过该参数给线程执行函数传参
设置线程属性.若成功,返回0,否则返回错误编号。可以设置的属性如下:
1.detachstate: 线程的分离状态属性
2.guardsize:线程栈末尾的警戒缓冲区大小(字节数)
3.stackaddr:线程栈的最低地址
4.stacksize:线程栈的最小长度(字节数)
如果在创建线城市就知道不需要了解线程的中止状态,就可以修改pthread_attr_t结构中的detachstate线程属性,让线程一开始就处于分离状态。可以使用pthread_attr_setdetachstate函数把线程属性detachstate设置成以下两个合法值之一:PTHREAD_CREATE_DETACHED,以分离状态启动线程;或者PTHREAD_CREATE_JOINABLE,正常启动线程,应用程序可以获取线程的中止状态。
1.detachstate: 线程的分离状态属性
2.guardsize:线程栈末尾的警戒缓冲区大小(字节数)
3.stackaddr:线程栈的最低地址
4.stacksize:线程栈的最小长度(字节数)
如果在创建线城市就知道不需要了解线程的中止状态,就可以修改pthread_attr_t结构中的detachstate线程属性,让线程一开始就处于分离状态。可以使用pthread_attr_setdetachstate函数把线程属性detachstate设置成以下两个合法值之一:PTHREAD_CREATE_DETACHED,以分离状态启动线程;或者PTHREAD_CREATE_JOINABLE,正常启动线程,应用程序可以获取线程的中止状态。
两个函数的返回值:若成功,返回0,否则,返回错误编号
可以调用pthread_attr_getdetachstate函数获取当前的detachstate线程属性。第二个参数所指向的整数要么设置成PTHREAD_CREATE_DETACHED,要么设置成PTHREAD_CREATE_JOINABLE,具体取决于给定pthread_attr_t结构中的属性值。
可以调用pthread_attr_getdetachstate函数获取当前的detachstate线程属性。第二个参数所指向的整数要么设置成PTHREAD_CREATE_DETACHED,要么设置成PTHREAD_CREATE_JOINABLE,具体取决于给定pthread_attr_t结构中的属性值。
JVM的虚拟机栈大小初始是1M是如何实现的,就是通过这两个API实现的。若成功,返回0,否则,返回错误编号。
可以使用函数pthread_attr_getstack和pthread_attr_setstack对线程栈属性进行管理。
可以使用函数pthread_attr_getstack和pthread_attr_setstack对线程栈属性进行管理。
线程中断机制
Linux多线程是没有中断机制的.JVM中的中断机制是自己上线的,实现逻辑很简单,在线程运行函数中或线程运行这条路上有一处或多处这样的判断。意思就是通过改变中断标志告诉线程,你的任务已经执行完了,你可以销毁了。当然,这个标志可以被清理掉,所以看到说设置了有可能会被忽略掉。其实就是其他逻辑发现这个线程不能销毁,把这个状态位改掉了
Linux多线程是没有中断机制的.JVM中的中断机制是自己上线的,实现逻辑很简单,在线程运行函数中或线程运行这条路上有一处或多处这样的判断。意思就是通过改变中断标志告诉线程,你的任务已经执行完了,你可以销毁了。当然,这个标志可以被清理掉,所以看到说设置了有可能会被忽略掉。其实就是其他逻辑发现这个线程不能销毁,把这个状态位改掉了
等待线程运行结束。若成功,返回0,否则,返回错误编号。
参数可以用来获取线程执行结果
参数可以用来获取线程执行结果
互斥锁的初始化和销毁逻辑。若成功返回0,否则,返回错误编号
加锁/解锁。若成功返回0,否则返回错误编号
读写锁的初始化和销毁逻辑。若成功返回0,否则返回错误编号
读写锁的上锁/解锁。若成功返回0,否则返回错误编号
条件变量的初始化和销毁逻辑。若成功返回0,否则返回错误编号
条件变量的等待,若成功返回0,否则返回错误编号
Linux应用态的线程相关点
多线程
pthread_create
pthread_join
pthread_exit
互斥
同步
线程与信号
锁 mutex
pthread_mutex_init
pthread_mutex_destroy
pthread_mutex_lock
pthread_mutex_unlock
条件变量
pthread_cond_init
pthread_cond_destroy
pthread_cond_wait
pthread_cond_signal 公平锁,逐个唤醒队列中的线程
pthread_cond_broadcast 非公平锁,唤醒队列里的所有线程
Linux的线程机制是面向过程式的API,而JVM则是在Linux的基础之上开发出了一种面向对象的线程机制
多线程
pthread_create
pthread_join
pthread_exit
互斥
同步
线程与信号
锁 mutex
pthread_mutex_init
pthread_mutex_destroy
pthread_mutex_lock
pthread_mutex_unlock
条件变量
pthread_cond_init
pthread_cond_destroy
pthread_cond_wait
pthread_cond_signal 公平锁,逐个唤醒队列中的线程
pthread_cond_broadcast 非公平锁,唤醒队列里的所有线程
Linux的线程机制是面向过程式的API,而JVM则是在Linux的基础之上开发出了一种面向对象的线程机制
OS内核层的线程相关点
用户态和内核态的切换
线程切换
线程在内核中到底是什么
用户态和内核态的切换
线程切换
线程在内核中到底是什么
joinable与detached两种类型的线程
1.默认创建的线程就是joinable
2.两者的区别:
a.DETACHED分离线程是不受pthread_join相关API控制的
b.比如socket资源的占用,joinable不会自动释放,除非手动调用。而detached则会自动释放
主控线程:进程的第一个线程,进程只是提供一个空间,一个资源环境,它本身是不做什么事情的。在操作系统的源码当中,创建进程时会创建出一个线程,负责启动程序
main线程:原生线程的层面是没有的,只有Java层面才有。执行java main方法的线程。joinable
java线程: 通过new Thread创建出来的线程. detached类型。原因在于,希望Java线程运行完系统直接回收,第二个是从技术角度出发
1.默认创建的线程就是joinable
2.两者的区别:
a.DETACHED分离线程是不受pthread_join相关API控制的
b.比如socket资源的占用,joinable不会自动释放,除非手动调用。而detached则会自动释放
主控线程:进程的第一个线程,进程只是提供一个空间,一个资源环境,它本身是不做什么事情的。在操作系统的源码当中,创建进程时会创建出一个线程,负责启动程序
main线程:原生线程的层面是没有的,只有Java层面才有。执行java main方法的线程。joinable
java线程: 通过new Thread创建出来的线程. detached类型。原因在于,希望Java线程运行完系统直接回收,第二个是从技术角度出发
Java中的main线程属性是JOINABLE
代码测试JOINABLE
代码测试DETACHED
获取线程返回值。
joinable线程:
detached线程:
joinable线程:
detached线程:
joinable
detached
解决方案:
1.借助全局访问变量。加上sleep()等待全局变量去被赋值。但HotSpot肯定不会这么去做这件事。JVM是通过vm_result vm_result_2,一个是基本数据类型,另一个是引用类型。
那么线程的先后顺序如何控制的呢?
使用mutex+条件变量控制的
解决方案:
1.借助全局访问变量。加上sleep()等待全局变量去被赋值。但HotSpot肯定不会这么去做这件事。JVM是通过vm_result vm_result_2,一个是基本数据类型,另一个是引用类型。
那么线程的先后顺序如何控制的呢?
使用mutex+条件变量控制的
线程中断机制
中断机制是Java自己实现的。理解有个前提,线程是自己杀死的,设置它的中断标志位,然后等线程唤醒它之后,发现自己的中断标志位已经被设置了,于是它就中断退出。这就是中断的本质。
中断的意义
精准杀死你想要杀死的线程
中断机制是JVM自己实现的,Linux没有线程中断这一说
中断机制是Java自己实现的。理解有个前提,线程是自己杀死的,设置它的中断标志位,然后等线程唤醒它之后,发现自己的中断标志位已经被设置了,于是它就中断退出。这就是中断的本质。
中断的意义
精准杀死你想要杀死的线程
中断机制是JVM自己实现的,Linux没有线程中断这一说
把线程封装成面向对象机制会出现的问题
1.通过分离线程解决串行执行
2.知晓线程合适运行结束。得让主控线程阻塞,所有子线程运行结束,主控线程退出
HotSpot是借助锁来实现的,mutex互斥锁,以及条件变量
达到的效果
1.面向过程的线程API封装成面向对象的线程机制
2.主控线程要阻塞住,知道子线程运行完
1.通过分离线程解决串行执行
2.知晓线程合适运行结束。得让主控线程阻塞,所有子线程运行结束,主控线程退出
HotSpot是借助锁来实现的,mutex互斥锁,以及条件变量
达到的效果
1.面向过程的线程API封装成面向对象的线程机制
2.主控线程要阻塞住,知道子线程运行完
ParkEvent#park()
ParkEvent#unpark()
使用mutex+条件变量之后的代码
加锁和解锁应该是由创建的线程去做,而不是主控线程去做加锁,然后让创建的线程去做解锁。加锁/解锁的操作应该都由创建的线程去做,这种封装形式叫做entry_point.这样的话,相当于主控线程在创建一个线程的时候加锁阻塞,等到该线程执行完毕之后解锁,主控线程再去创建第二个线程,还是没有解决串行的问题
entry_poin封装了这个线程的业务逻辑,线程函数中去执行这个entry_point,然后由这个线程在线程函数中去执行加锁、解锁。
加锁和解锁应该是由创建的线程去做,而不是主控线程去做加锁,然后让创建的线程去做解锁。加锁/解锁的操作应该都由创建的线程去做,这种封装形式叫做entry_point.这样的话,相当于主控线程在创建一个线程的时候加锁阻塞,等到该线程执行完毕之后解锁,主控线程再去创建第二个线程,还是没有解决串行的问题
entry_poin封装了这个线程的业务逻辑,线程函数中去执行这个entry_point,然后由这个线程在线程函数中去执行加锁、解锁。
执行结果
线程并行执行
遍历线程去加锁,如果子线程执行结束,则唤醒主控线程
遍历线程去加锁,如果子线程执行结束,则唤醒主控线程
2.Java线程底层实现
Java中的Thread对象和操作系统的线程的关系|
Java的Thread对象是一个oop对象
JVM的JavaThread是一个C++对象
JVM的OSThread是一个C++对象
操作系统线程
主要牵涉到四个层面
Java层面
JVM层面
OS(应用层面)
-----------------
OS(内核层)
Java的Thread对象是一个oop对象
JVM的JavaThread是一个C++对象
JVM的OSThread是一个C++对象
操作系统线程
主要牵涉到四个层面
Java层面
JVM层面
OS(应用层面)
-----------------
OS(内核层)
如何找到native方法的JVM源码。以Thread#start()方法为例。在HotSpot源码中会有一个Thread.c文件
JVM中的start0方法对应的方法名叫做JVM_StartThread方法,该方法是以JVM开头的方法,我们需要到jvm.cpp中去找这个方法
再比如说想要找Object的native方法,我们就要到Object.c中去找方法对应的JVM方法,找到JVM方法之后,再去jvm.cpp里面去找
Object.c中的HashCode方法在jvm.cpp中所对应的
Java线程实现整体结构
main方法/程序代码的执行是由main线程来执行的
new Thread(() -> System.out.println("ss")).start();
核心逻辑是在start()方法里面,new Thread只是创建了一个Java对象,Thread对象oop.new出来的Thread对象oop和JavaThread(C++对象)、OSThread(C++对象)、Linux线程这四层的对象创建、绑定逻辑也是在start()方法里面,
main方法/程序代码的执行是由main线程来执行的
new Thread(() -> System.out.println("ss")).start();
核心逻辑是在start()方法里面,new Thread只是创建了一个Java对象,Thread对象oop.new出来的Thread对象oop和JavaThread(C++对象)、OSThread(C++对象)、Linux线程这四层的对象创建、绑定逻辑也是在start()方法里面,
new JavaThread(C++对象)在哪里创建
获取run函数的执行逻辑
JavaThread的构造方法当中的逻辑主要做了以下三件事
1.执行了Thread()
2.set_entry_point(entry_point)
3.os::create_thread();
1.执行了Thread()
2.set_entry_point(entry_point)
3.os::create_thread();
Thread()方法当中做了一些线程初始化的工作,关键的代码在于这个地方创建了ParkEvent对象,用来控制线程的阻塞与唤醒
set_entry_point(entry_point)是把我们定义的run函数里面的逻辑设置到线程的entry_point当中
os::create_thread()创建OSThread的核心逻辑,这里会涉及到其他系统平台,每个操作系统的线程机制又不一样,所以,我们以Linux为主,点进去看os_linux.cpp文件即可。其中thread->set_osthread(osthread);这行代码会将JavaThread和OSThread进行一个绑定.并且还将OSThread的属性设置为了DETACHED
这行代码执行完,程序中就会有一个子线程、一个main线程
线程的操作都是靠pthread_create的传出参数tid进行识别的,在这里osthread->set_pthread_id(tid)。又将OSThread和Linux线程做了绑定关系。但是new Thread创建的oop对象还没有进行关联
java_start分析。
pthread_create(&tid, &attr,(void*(*)(void*))java_start, thread);这里有一个java_start函数,也就是pthread要执行的具体逻辑,对应的是Java中的Thread的run方法。而java_start方法中,会阻塞住主线程.在Java中线程创建出来之后,需要调用start()方法线程才会运行起来,而在OS层面,则是在pthread_create代码执行之后,线程就会运行起来,对应的java_start逻辑也会跑起来
这里阻塞的原因:
1.如果主线程不阻塞,主线程就运行完了,子线程可能还没创建好环境,或者还没有执行完
2.主线程会阻塞,直到子线程被初始化完成或者被丢弃
pthread_create(&tid, &attr,(void*(*)(void*))java_start, thread);这里有一个java_start函数,也就是pthread要执行的具体逻辑,对应的是Java中的Thread的run方法。而java_start方法中,会阻塞住主线程.在Java中线程创建出来之后,需要调用start()方法线程才会运行起来,而在OS层面,则是在pthread_create代码执行之后,线程就会运行起来,对应的java_start逻辑也会跑起来
这里阻塞的原因:
1.如果主线程不阻塞,主线程就运行完了,子线程可能还没创建好环境,或者还没有执行完
2.主线程会阻塞,直到子线程被初始化完成或者被丢弃
java_start分析。接着我们再来看子线程的java_start方法逻辑。主要做了以下几件事情
1.初始化子线程环境
2.唤醒父线程
3.把自己的状态改为INITIALIZED
4.自己阻塞,等待调用os::start_thread()
5.执行run方法
子线程为什么要唤醒父线程再把自己设置成阻塞状态?
猜想:其实也可以在这里直接让子线程去运行run方法,但是hotspot的开发人员却是将子线程的环境初始化工作和执行逻辑分成两个阶段执行的,具体出于什么原因不得而知,但其实子线程和主线程等来等去都是为了在一个合适的时机让运行环境准备好再去执行,而不是直接一下全部执行完
1.初始化子线程环境
2.唤醒父线程
3.把自己的状态改为INITIALIZED
4.自己阻塞,等待调用os::start_thread()
5.执行run方法
子线程为什么要唤醒父线程再把自己设置成阻塞状态?
猜想:其实也可以在这里直接让子线程去运行run方法,但是hotspot的开发人员却是将子线程的环境初始化工作和执行逻辑分成两个阶段执行的,具体出于什么原因不得而知,但其实子线程和主线程等来等去都是为了在一个合适的时机让运行环境准备好再去执行,而不是直接一下全部执行完
当JavaThread构造方法执行完之后,就会调用native_thread->prepare(jthread),当JavaThread创建之后,会执行prepare方法,
而在prepare方法中,有一个非常关键的代码就是java_lang_Thread::set_thread(thread_oop(), this);这行代码就是将new Thread oop对象和JavaThread对象进行绑定。它是将JavaThread对象赋值给oop对象中的某一块内存地址。在前面JavaThread、OSThread、Linux线程建立了起来,这里又将new Thread oop和JavaThread进行了绑定
因为它是通过操作偏移量进行设置oop对象中的JavaThread
在创建线程的时候,还将创建的子线程加入到了一个线程单链表当中,因为OS并没有提供这样的一个API来获取这个进程的所有线程。也是为了后面的STW做准备
如果线程创建失败了,那么就抛出异常。否则调用Thread::start(native_thread);启动线程,现在子线程还是处于阻塞状态,这里是主线程在执行。
有个现象,如果唤醒完子线程,主线程在这里已经执行完了,但是没有发现主线程阻塞等待子线程执行完的逻辑。DETACHED还是有一个机制去等待子线程运行结束。核心代码还没找到
有个现象,如果唤醒完子线程,主线程在这里已经执行完了,但是没有发现主线程阻塞等待子线程执行完的逻辑。DETACHED还是有一个机制去等待子线程运行结束。核心代码还没找到
接着调用os::start_thread(),
接着设置线程状态为RUNNABLE,调用pd_start_thread方法,
sync_with_child->notify()去唤醒子线程,子线程被唤醒后,就会执行run方法,这是一个多态方法,需要到JavaThread::run方法中去找,
在run方法中会初始化Java线程运行所需要的环境,初始化tlab,栈指针。核心的代码在thread_main_inner()中的this->entry_point()(this,this)运行main函数
这里设置了entry_point.run方法跑了之后,会执行exit动作
Java线程为什么能操作系统线程?
原生线程是通过传出参数tid调用的,那么Java为什么能够操作操作系统原生线程呢?
原因就在于上面的线程四层关系的建立
为什么要做四层线程封装?JavaThread和OSThread为什么做两层?
答案是为了跨平台,做了一个中间层。OSThread上接Java层,下接操作系统
OS并没有zheyang 一个API,获取这个进程的所有线程
原生线程是通过传出参数tid调用的,那么Java为什么能够操作操作系统原生线程呢?
原因就在于上面的线程四层关系的建立
为什么要做四层线程封装?JavaThread和OSThread为什么做两层?
答案是为了跨平台,做了一个中间层。OSThread上接Java层,下接操作系统
OS并没有zheyang 一个API,获取这个进程的所有线程
run函数是如何跑起来的
执行run函数的地方:thread_entry,如何执行起来的?
执行run函数的地方:thread_entry,如何执行起来的?
为什么join方法能阻塞直到线程执行结束?
因为它底层是一个wait方法,当线程的run方法执行结束之后,会对其进行唤醒
因为它底层是一个wait方法,当线程的run方法执行结束之后,会对其进行唤醒
Java世界 | (CallStub) | JVM世界
generate_call_stub方法内部也是一种执行流
核心节点:
1.父线程与子线程的交替执行顺序
2.四层关系的建立
3.run方法是怎么跑起来的(线程模块与执行流模块是如何建立联系的的)
generate_call_stub方法内部也是一种执行流
核心节点:
1.父线程与子线程的交替执行顺序
2.四层关系的建立
3.run方法是怎么跑起来的(线程模块与执行流模块是如何建立联系的的)
调试HotSpot源码的代码,添加如图所示的代码,就可以在我们自己定义的线程处下断点
线程无限执行的问题.
原生的线程栈大小默认为8M,如果你一直调用,超出8M的空间,就会超出线程栈的大小,当访问线程栈之外的错误的时候,就会触发段异常。
信号分为可捕捉和不可捕捉。这也是为什么Ctrl + C为什么能够中止程序,原因是操作系统代码中对SIGCHILD信号进行了捕获。
JVM为什么能捕获段异常呢?操作系统都捕获不到,
JVM在线程栈上做了封装,当线程栈快要爆掉的时候对其进行捕获
线程的运行需要空间来运行,如果没有空间了,那也就没法执行了
原生的线程栈大小默认为8M,如果你一直调用,超出8M的空间,就会超出线程栈的大小,当访问线程栈之外的错误的时候,就会触发段异常。
信号分为可捕捉和不可捕捉。这也是为什么Ctrl + C为什么能够中止程序,原因是操作系统代码中对SIGCHILD信号进行了捕获。
JVM为什么能捕获段异常呢?操作系统都捕获不到,
JVM在线程栈上做了封装,当线程栈快要爆掉的时候对其进行捕获
线程的运行需要空间来运行,如果没有空间了,那也就没法执行了
代码部分
HotSpot源码中的位置
线程无限执行的现象演示。
如果catch Error会永远执行,catch Exception的话,会抛出StackOverflowError错误。栈的深度大小默认为1024.
既然这个Java程序能够无限执行,这个能力是操作系统自带的还是JVM开发出来的?
如果catch Error会永远执行,catch Exception的话,会抛出StackOverflowError错误。栈的深度大小默认为1024.
既然这个Java程序能够无限执行,这个能力是操作系统自带的还是JVM开发出来的?
这是在Linux系统下执行的结果,可以看到,Linux系统默认是不支持程序无限执行的。为什么最后会报段错误呢?因为Linux系统创建线程,默认的栈大小为8M,程序无限递归把8M用光了,但是程序还不会中止,不自觉会用到8M之外的内存
既然Linux系统没有提供这样的能力,既然JVM能如此,大胆猜想可能的原因:
1.JVM改变了系统默认栈大小8M,可能改成了很大很大,如果是这样,那我们看这段程序的无限执行其实是假象,如果让它一直跑,跑很久很久,可能它就over了
2.JVM内部做了优化,比如栈帧回溯、递归内联、尾递归优化。验证这个的时候还得考虑方法执行的两个阶段:解释执行、执行JIT即时编译后的代码。那么我们可以看下JVM的主线程有没有改变线程栈的大小,改成了多少,需要单步调试OpenJDK.
可以看到JVM把stacksize设置成了1M(1024*1024=1048576).
JVM的虚拟机栈是在操作系统的线程栈上进行拓展的,Linux系统的默认线程栈是8M.如果是递归调用,一下就跑完了,但是上面的程序,很明显跑了很久都没结束,所以这种情况排除,只剩最后一种情况。程序无限执行的能力,Linux没有提供,但是这段程序却能够无限执行,说明这个能力是JVM赋予的。
那么JVM如何做到的呢?首先,栈的内存大小决定了,一个程序的调用深度是优先的,超过了栈内存大小,Linux会触发这段错误信号:SIGSEGV.JVM应该是捕获了这个信号,并进行了处理。那什么的处理能支持程序一直运行下去呢? 一定是做了栈帧回溯。
1.JVM改变了系统默认栈大小8M,可能改成了很大很大,如果是这样,那我们看这段程序的无限执行其实是假象,如果让它一直跑,跑很久很久,可能它就over了
2.JVM内部做了优化,比如栈帧回溯、递归内联、尾递归优化。验证这个的时候还得考虑方法执行的两个阶段:解释执行、执行JIT即时编译后的代码。那么我们可以看下JVM的主线程有没有改变线程栈的大小,改成了多少,需要单步调试OpenJDK.
可以看到JVM把stacksize设置成了1M(1024*1024=1048576).
JVM的虚拟机栈是在操作系统的线程栈上进行拓展的,Linux系统的默认线程栈是8M.如果是递归调用,一下就跑完了,但是上面的程序,很明显跑了很久都没结束,所以这种情况排除,只剩最后一种情况。程序无限执行的能力,Linux没有提供,但是这段程序却能够无限执行,说明这个能力是JVM赋予的。
那么JVM如何做到的呢?首先,栈的内存大小决定了,一个程序的调用深度是优先的,超过了栈内存大小,Linux会触发这段错误信号:SIGSEGV.JVM应该是捕获了这个信号,并进行了处理。那什么的处理能支持程序一直运行下去呢? 一定是做了栈帧回溯。
最终结论是: JVM捕获了段异常信号并做了处理,处理方式是栈帧回溯。
JVM设置的1024栈大小,就是MaxJavaStackTraceDepth的值。JVM默认支持的最大深度就是1024,为什么是1024?因为触发异常的时候需要遍历栈,导出栈信息,如果栈的深度很深,很肥时间费性能,就取了一个有象征意义的值。
1024也是一个临界点,当栈深度达到1024,栈帧开始回溯。你可以理解成程序跑起来把栈深度冲到1024,开始回溯,回溯到初始调用时的栈,然后栈深度又开始冲1024.循环往复
1024也是一个临界点,当栈深度达到1024,栈帧开始回溯。你可以理解成程序跑起来把栈深度冲到1024,开始回溯,回溯到初始调用时的栈,然后栈深度又开始冲1024.循环往复
最后,针对回调做优化,目前主流的优化方式有:尾递归优化、内联优化。JVM没有用尾递归优化,而是用的内联优化,专业名词叫递归内联
3.手写线程池
线程API
创建线程:pthread_create
阻塞线程:pthread_join
互斥锁:pthread_mutex_init、pthread_mutex_destroy、pthread_mutex_tryLock、pthread_mutex_lock、pthread_mutex_unlock
结束线程:pthread_exit
创建线程:pthread_create
阻塞线程:pthread_join
互斥锁:pthread_mutex_init、pthread_mutex_destroy、pthread_mutex_tryLock、pthread_mutex_lock、pthread_mutex_unlock
结束线程:pthread_exit
线程池理论
两大模块:线程池、任务池
1.线程池:
参数:核心线程数、最大线程数(需要区分IO、CPU)
功能: 执行任务、需要根据任务的多少自动调节、扩加、缩减
2.管理者线程:
需要根据任务的多少自动调节、扩加、缩减
3.任务池
参数:需要做什么 类似HotSpot中往队列当中丢任务,让VMThread去异步执行,有时候也会直接交给VMThread执行。doit方法 vm_operation类似这样一个操作,异步任务
task = pop(dequeue);
task->doit();
功能:取出任务、添加任务。没有任务时,线程阻塞,有任务时谁来唤醒?什么时候唤醒
唤醒的方案:
a.单独创建一个线程管理任务池,如果搞一个线程一直while(true)去监听,但是仍然会消耗CPU
b.任务池唤醒(最好的方式),最好的是加任务的时候就唤醒
任务池里面没有任务,所有线程全部进入等待.如果线程不等待的话,一直循环去检查任务池中是否有任务,其实是会消耗CPU的,
while (0 != tasakpool.size()) {
wait
task = pop(queue);
task->doit();
}
任务池里面有任务,唤醒工作线程
就单独创建一个线程管理任务池,当任务池有任务时就唤醒
任务池以什么数据结构来实现?参考synchronized的回环队列有两种方式:数组形式(JDK实现的方式)、双链表形式
两大模块:线程池、任务池
1.线程池:
参数:核心线程数、最大线程数(需要区分IO、CPU)
功能: 执行任务、需要根据任务的多少自动调节、扩加、缩减
2.管理者线程:
需要根据任务的多少自动调节、扩加、缩减
3.任务池
参数:需要做什么 类似HotSpot中往队列当中丢任务,让VMThread去异步执行,有时候也会直接交给VMThread执行。doit方法 vm_operation类似这样一个操作,异步任务
task = pop(dequeue);
task->doit();
功能:取出任务、添加任务。没有任务时,线程阻塞,有任务时谁来唤醒?什么时候唤醒
唤醒的方案:
a.单独创建一个线程管理任务池,如果搞一个线程一直while(true)去监听,但是仍然会消耗CPU
b.任务池唤醒(最好的方式),最好的是加任务的时候就唤醒
任务池里面没有任务,所有线程全部进入等待.如果线程不等待的话,一直循环去检查任务池中是否有任务,其实是会消耗CPU的,
while (0 != tasakpool.size()) {
wait
task = pop(queue);
task->doit();
}
任务池里面有任务,唤醒工作线程
就单独创建一个线程管理任务池,当任务池有任务时就唤醒
任务池以什么数据结构来实现?参考synchronized的回环队列有两种方式:数组形式(JDK实现的方式)、双链表形式
线程池实现
管理者线程:负责检测任务池及线程池来动态调节线程数量,如扩容、缩容,还有在一定条件下的阻塞与唤醒
工作线程:抢任务、执行任务、生成统计数据
管理者线程:负责检测任务池及线程池来动态调节线程数量,如扩容、缩容,还有在一定条件下的阻塞与唤醒
工作线程:抢任务、执行任务、生成统计数据
任务池实现
一般采用环形队列实现。结合锁实现阻塞唤醒。环形队列有两种实现思路:数组、链表式
一般采用环形队列实现。结合锁实现阻塞唤醒。环形队列有两种实现思路:数组、链表式
线程池的意义
1.节省线程创建的开销
2.动态调整工作线程的数量
扩容的依据:任务还有很多,所有的线程现在都在执行任务
缩减的依据:没任务了,所有的线程也都是空闲的了
管理者线程:有任务,唤醒,没有任务,进入睡眠
管理线程的阻塞与唤醒
ParkEvent去精准控制一个线程的阻塞与唤醒
杀死线程的本质:调用API,改变线程本身的某个参数,让它知道它要去死亡,Linux机制,让它运行起来进入死亡
多线程的不确定性,要熟练使用线程API控制线程
1.节省线程创建的开销
2.动态调整工作线程的数量
扩容的依据:任务还有很多,所有的线程现在都在执行任务
缩减的依据:没任务了,所有的线程也都是空闲的了
管理者线程:有任务,唤醒,没有任务,进入睡眠
管理线程的阻塞与唤醒
ParkEvent去精准控制一个线程的阻塞与唤醒
杀死线程的本质:调用API,改变线程本身的某个参数,让它知道它要去死亡,Linux机制,让它运行起来进入死亡
多线程的不确定性,要熟练使用线程API控制线程
pthread_cond_wait()这个API做了什么?
1.执行了unlock
2.线程进入阻塞,阻塞在条件变量上
3.唤醒之后又会lock
1.执行了unlock
2.线程进入阻塞,阻塞在条件变量上
3.唤醒之后又会lock
杀死线程的本质:调用API,改变线程本身的某个参数,让它直到它要去死,Linux机制,让它运行起来
中断的意义:精准杀死你想要杀死的线程。因为Linux中的线程机制是随机唤醒,随机杀死
中断是JVM自己实现的,Linux没有线程中断这一说
中断的意义:精准杀死你想要杀死的线程。因为Linux中的线程机制是随机唤醒,随机杀死
中断是JVM自己实现的,Linux没有线程中断这一说
Java中线程池的参数.这个线程池被创建出来的时候,在没有提交任何任务时,线程池中实际上不会有任务线程,只有提交任务的时候,才会创建线程。
1.corePoolSize:线程池核心大小
线程池维护的最小线程数量,核心线程创建后不会被回收(注意:设置allowanCoreThreadTimeout=true后,空闲的核心线程超过存活时间也会被回收)
线程池刚创建时,里面没有一个线程,当调用execute()方法添加一个任务时,如果正在运行的线程数量小于corePoolSize,则马上创建新线程并运行这个任务
2.maximumPoolSize:最大线程数
线程池i允许创建的最大线程数量。当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满的情况下,创建一个新线程,然后从工作队列的头部取出一个任务交给新线程来处理,而将刚提交的任务放入工作队列尾部
3.keepAliveTIme:空闲线程存活时间
当一个可被回收的线程的空闲时间大于keepAliveTIme,就会被回收。可被回收的线程:设置allowCoreThreadTimeout=true的核心线程。大于核心线程数的线程(非核心线程)
4.unit:时间单位
5.workQueue:工作队列
新任务被提交后,会先添加到工作队列,任务调度时再从队列中取出任务。工作队列实现了BlockingQueue接口。JDK默认的工作队列有五种:
a.ArrayBlockingQueue 数组型阻塞队列: 数组结构,初始化传入大小,有界,FIFO,使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥
b.LinkedBlockingQueue链表型阻塞队列:链表结构,默认初始化大小为Integer.MAX_VALUE,有界(近似无界),FIFO,使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待
c.SynchronousQueue同步队列:容量为0,添加任务必须等待取出任务,这个队列相当于通道,不存储元素
d.PriorityBlockingQueue 优先阻塞队列: 无界,默认采用元素自然顺序升序排列
e.DelayQueue 延时队列: 无界,元素有过期时间,过期的元素才能被取出
6.threadFactory:线程工厂
创建线程的工厂,可以设定线程名、线程编号等等
7.handler 拒绝策略
当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需要实现RejectedExecutionHandler接口
JDK默认的拒绝策略有四种:
a.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常
b.DiscardPolicy: 丢弃任务,但是不跑出异常。可能导致无法发现系统的异常状态
c.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新提交被拒绝的任务
d.CallerRunPolicy:由调用线程处理该任务
1.corePoolSize:线程池核心大小
线程池维护的最小线程数量,核心线程创建后不会被回收(注意:设置allowanCoreThreadTimeout=true后,空闲的核心线程超过存活时间也会被回收)
线程池刚创建时,里面没有一个线程,当调用execute()方法添加一个任务时,如果正在运行的线程数量小于corePoolSize,则马上创建新线程并运行这个任务
2.maximumPoolSize:最大线程数
线程池i允许创建的最大线程数量。当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满的情况下,创建一个新线程,然后从工作队列的头部取出一个任务交给新线程来处理,而将刚提交的任务放入工作队列尾部
3.keepAliveTIme:空闲线程存活时间
当一个可被回收的线程的空闲时间大于keepAliveTIme,就会被回收。可被回收的线程:设置allowCoreThreadTimeout=true的核心线程。大于核心线程数的线程(非核心线程)
4.unit:时间单位
5.workQueue:工作队列
新任务被提交后,会先添加到工作队列,任务调度时再从队列中取出任务。工作队列实现了BlockingQueue接口。JDK默认的工作队列有五种:
a.ArrayBlockingQueue 数组型阻塞队列: 数组结构,初始化传入大小,有界,FIFO,使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥
b.LinkedBlockingQueue链表型阻塞队列:链表结构,默认初始化大小为Integer.MAX_VALUE,有界(近似无界),FIFO,使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待
c.SynchronousQueue同步队列:容量为0,添加任务必须等待取出任务,这个队列相当于通道,不存储元素
d.PriorityBlockingQueue 优先阻塞队列: 无界,默认采用元素自然顺序升序排列
e.DelayQueue 延时队列: 无界,元素有过期时间,过期的元素才能被取出
6.threadFactory:线程工厂
创建线程的工厂,可以设定线程名、线程编号等等
7.handler 拒绝策略
当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需要实现RejectedExecutionHandler接口
JDK默认的拒绝策略有四种:
a.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常
b.DiscardPolicy: 丢弃任务,但是不跑出异常。可能导致无法发现系统的异常状态
c.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新提交被拒绝的任务
d.CallerRunPolicy:由调用线程处理该任务
4.AQS
实现管程模型
1.抢锁
1.所还没有被人持有,直接抢锁
CAS实现,将state改为1
记录线程信息(为什么?为了后面判断重入)
2.又过来抢锁,判断是否重入
如果是,记录重入次数(为什么?加锁几次就要解锁几次)
重入次数如何存储state += 1
3.既没有抢到锁,又不是重入
抢锁失败
1.线程结束(不符合逻辑)
2.自旋抢锁、进入队列、阻塞、等待唤醒、唤醒后、抢锁
2.入队
AQS式:
创建一个空姐点表示当前持有锁的线程
再创建一个表示第一个等待的线程
自旋抢锁:
1.自旋一次,然后waitStatus=-1, 代表可唤醒
2.循环清理掉所有的cancel节点
3.阻塞,等待唤醒
4.持有锁的线程释放锁
1.解决重入
2.恢复锁状态位
3.唤醒队列中的第二个节点
5.队列中的第二个节点开始运行
1.抢锁
2.t1节点出队
3.将自己设置成头节点,thread =null
4.执行自己的业务逻辑
6.运行
1.抢锁
1.所还没有被人持有,直接抢锁
CAS实现,将state改为1
记录线程信息(为什么?为了后面判断重入)
2.又过来抢锁,判断是否重入
如果是,记录重入次数(为什么?加锁几次就要解锁几次)
重入次数如何存储state += 1
3.既没有抢到锁,又不是重入
抢锁失败
1.线程结束(不符合逻辑)
2.自旋抢锁、进入队列、阻塞、等待唤醒、唤醒后、抢锁
2.入队
AQS式:
创建一个空姐点表示当前持有锁的线程
再创建一个表示第一个等待的线程
自旋抢锁:
1.自旋一次,然后waitStatus=-1, 代表可唤醒
2.循环清理掉所有的cancel节点
3.阻塞,等待唤醒
4.持有锁的线程释放锁
1.解决重入
2.恢复锁状态位
3.唤醒队列中的第二个节点
5.队列中的第二个节点开始运行
1.抢锁
2.t1节点出队
3.将自己设置成头节点,thread =null
4.执行自己的业务逻辑
6.运行
机制
1.状态位 state 0 1
2.队列机制
1.如果我们实现:普通队列
2.AQS式的队列
3.waitStatus 闹钟
-3 同步相关
-2 同步相关
-1 可唤醒的线程
0 默认
1 cancel
1.状态位 state 0 1
2.队列机制
1.如果我们实现:普通队列
2.AQS式的队列
3.waitStatus 闹钟
-3 同步相关
-2 同步相关
-1 可唤醒的线程
0 默认
1 cancel
公平锁
lock方法的逻辑
1.获取当前锁状态位
2.如果为0,则表示当前锁没有被其他人持有
当锁状态为0,判断是否有形成队列,如果此时队列还没有,则说明自己是第一个线程来加锁,接着会进行CAS设置锁状态位,设置独占锁线程属性。如果说队列已经形成了,说明自己不是第一个抢锁的线程,那就要入队
如果不为0,则判断是否重入,如果不是,则加锁失败
1.获取当前锁状态位
2.如果为0,则表示当前锁没有被其他人持有
当锁状态为0,判断是否有形成队列,如果此时队列还没有,则说明自己是第一个线程来加锁,接着会进行CAS设置锁状态位,设置独占锁线程属性。如果说队列已经形成了,说明自己不是第一个抢锁的线程,那就要入队
如果不为0,则判断是否重入,如果不是,则加锁失败
接着分析当前线程不是第一个线程过来抢锁,从源码中我们可以看到如果抢锁失败,则先调用addWaiter()将当前线程封装成node节点,然后入队,此时还没有设置waitStatus,如果当前线程是第一个等待获取锁的线程,则该线程还要承担队列初始化的任务
接着调用acquireQueued方法,判断前面将当前线程封装成的Node的前驱节点是否为头节点,如果为头节点则再次尝试加锁
shouldParkAfterFailedAcquire方法逻辑,如果还是加锁失败,当前线程在阻塞之前,会将它的前驱节点的waitStatus设置成-1,表示让前驱节点解锁时,换唤醒下一个节点。然后就调用park()方法进行阻塞
这里还有一个逻辑,如果前驱节点被设置成了CANCEL状态,那么它会将队列中其他被设置了的CANCEL状态踢出队列
这里还有一个逻辑,如果前驱节点被设置成了CANCEL状态,那么它会将队列中其他被设置了的CANCEL状态踢出队列
unlock方法的逻辑
1.如果解锁方法不是当前持有锁的线程,则直接抛出异常
2.如果重入解锁的次数小于加锁的次数,则正常将重入锁的次数减1
3.如果说当前锁状态位已经置为0,恢复到无锁的状态了,则释放锁成功,将当前独占线程置为空
1.如果解锁方法不是当前持有锁的线程,则直接抛出异常
2.如果重入解锁的次数小于加锁的次数,则正常将重入锁的次数减1
3.如果说当前锁状态位已经置为0,恢复到无锁的状态了,则释放锁成功,将当前独占线程置为空
如果释放锁成功,则调用unparkSuccessor唤醒后面队列中的线程
非公平锁
lock方法的逻辑
可以看到,ReentrantLock上来就直接抢锁,CAS的方式,抢不到锁再走acquire()方法
可以看到,ReentrantLock上来就直接抢锁,CAS的方式,抢不到锁再走acquire()方法
tryAcquire()的非公平锁加锁逻辑nonfairTryAcquire()进来之后,
1.如果当前锁状态位为无锁状态,则直接进行CAS抢锁,如果抢到锁了,则会将锁的独占线程设置为自己本身。这里也是体现了非公平抢锁的特点,在公平锁当中,如果锁状态位为无锁,则会判断是否有队列形成以及排队逻辑。这里呢就暴力,抢到锁为目的,
2.重入锁
1.如果当前锁状态位为无锁状态,则直接进行CAS抢锁,如果抢到锁了,则会将锁的独占线程设置为自己本身。这里也是体现了非公平抢锁的特点,在公平锁当中,如果锁状态位为无锁,则会判断是否有队列形成以及排队逻辑。这里呢就暴力,抢到锁为目的,
2.重入锁
如果tryAcquire尝试加锁失败,那么会被封装成Node节点入队,接着在阻塞之前,如果前驱节点是头节点,则会再次调用tryAcquire方法,尝试CAS加锁。
这个时候如果还是加锁失败则才回去阻塞等待唤醒,抢锁失败,也是设置前驱节点的waitStatus为SIGNAL,当此时的前驱节点释放锁的时候,唤醒要阻塞的当前线程,接着就park()阻塞
这个时候如果还是加锁失败则才回去阻塞等待唤醒,抢锁失败,也是设置前驱节点的waitStatus为SIGNAL,当此时的前驱节点释放锁的时候,唤醒要阻塞的当前线程,接着就park()阻塞
unlock方法的逻辑,解锁逻辑,两种模式都是一致的,像重入解锁,非重入解锁
公平锁和非公平锁的差异
在于锁释放之后,下一个节点还没开始抢锁,此时信赖了一个线程找到了这个线程缝隙于是抢锁成功
在于锁释放之后,下一个节点还没开始抢锁,此时信赖了一个线程找到了这个线程缝隙于是抢锁成功
5.volatile
并发编程三大特性:
1.可见性
2.有序性
3.原子性
1.可见性
2.有序性
3.原子性
volatile为何而生?Java中其实也不一定非得叫这个关键字,也可以叫别的名称,只要它能实现volatile应该干的事情。
语言的特性实现是编译系统和运行系统搭配起来的结果。
volatile贯穿了计算机的每一层。Java层 内核层 硬件层
volatile的出现是解决JMM(Java Memory Model)模型和CPU乱序执行存在的问题
volatile触发了MESI协议(谬论),MESI协议不需要触发,这是CPU本身内部就会触发这个协议,因为CPU数据刷新是有两种策略,
一种是同步,一种是异步写(目前的主流机制)。
所以MESI协议确实可以通过内存屏障触发,目的是在异步机制下保证同步刷新,不是说你不加就不触发了,而是硬件本身就会触发MESI协议的,只不过会有延时.
语言的特性实现是编译系统和运行系统搭配起来的结果。
volatile贯穿了计算机的每一层。Java层 内核层 硬件层
volatile的出现是解决JMM(Java Memory Model)模型和CPU乱序执行存在的问题
volatile触发了MESI协议(谬论),MESI协议不需要触发,这是CPU本身内部就会触发这个协议,因为CPU数据刷新是有两种策略,
一种是同步,一种是异步写(目前的主流机制)。
所以MESI协议确实可以通过内存屏障触发,目的是在异步机制下保证同步刷新,不是说你不加就不触发了,而是硬件本身就会触发MESI协议的,只不过会有延时.
JMM
很多时候需要保证本地内存和主内存的数据一致性,同时这个模型也带来了一些局限性。由于这个模型是JVM自己定义的,所以一致性需要JVM去实现。
为什么Java中数据本身不具备可见性?原生线程为什么允许?
1.JMM为什么要这样实现呢?它遵循的是什么理论呢?遵循的是JSR-133规范(Java内存模型与线程规范),就跟Double类型存储时用两个Slot槽位来存储,使用的时候再合并起来
2.JMM核心的一句话是什么
线程不可以操作共享内存,只能操作私有内存。如果有volatile关键字修饰,需要回写到共享内存中。没有修饰,其他线程读取的将会产生数据不一致。那么JVM为什么不按照操作系统那套机制,直接操作共享内存呢? 目前不得而知,
3.JMM带来的问题
数据不可见性
4.JMM VS 原生线程
在Intel处理器下,原生线程是可见的,因为原生线程不会持有数据的副本,而是直接操作共享变量。在操作系统中,共享变量是存储在该进程的数据区,是所有线程共享的
5.编译优化
gcc编译 O1 、O2、O3,编译级别越高,优化程度越大,
JMM本身是不具备数据可见性的,所以引入volatile来保证线程可见性。牵涉到了这个关键字HotSpot背后做了哪些事情。
工作内存(私有内存,相当于栈)、主内存(共享内存,相当于堆)
很多时候需要保证本地内存和主内存的数据一致性,同时这个模型也带来了一些局限性。由于这个模型是JVM自己定义的,所以一致性需要JVM去实现。
为什么Java中数据本身不具备可见性?原生线程为什么允许?
1.JMM为什么要这样实现呢?它遵循的是什么理论呢?遵循的是JSR-133规范(Java内存模型与线程规范),就跟Double类型存储时用两个Slot槽位来存储,使用的时候再合并起来
2.JMM核心的一句话是什么
线程不可以操作共享内存,只能操作私有内存。如果有volatile关键字修饰,需要回写到共享内存中。没有修饰,其他线程读取的将会产生数据不一致。那么JVM为什么不按照操作系统那套机制,直接操作共享内存呢? 目前不得而知,
3.JMM带来的问题
数据不可见性
4.JMM VS 原生线程
在Intel处理器下,原生线程是可见的,因为原生线程不会持有数据的副本,而是直接操作共享变量。在操作系统中,共享变量是存储在该进程的数据区,是所有线程共享的
5.编译优化
gcc编译 O1 、O2、O3,编译级别越高,优化程度越大,
JMM本身是不具备数据可见性的,所以引入volatile来保证线程可见性。牵涉到了这个关键字HotSpot背后做了哪些事情。
工作内存(私有内存,相当于栈)、主内存(共享内存,相当于堆)
原生线程操作共享变量
Java线程去操作共享变量,不加static修饰
Java线程去操作共享变量,加static修饰
Java线程去操作共享变量,加上volatile修饰
编译优化。
其实在这里是不需要开辟堆栈的,因为没有局部变量需要去存储到栈帧操作,只有一个全局变量。
模板解释器、JIT也有编译优化,内部使用的是O2编译级别
编译优化对线程可见性是没有影响的。 volatile其实就是编译屏障。在开启优化的情况下,程序本来编译出来的就是精简的版本。加上volatile修饰,就是告诉编译器,跟这个变量相关的程序不要做优化
其实在这里是不需要开辟堆栈的,因为没有局部变量需要去存储到栈帧操作,只有一个全局变量。
模板解释器、JIT也有编译优化,内部使用的是O2编译级别
编译优化对线程可见性是没有影响的。 volatile其实就是编译屏障。在开启优化的情况下,程序本来编译出来的就是精简的版本。加上volatile修饰,就是告诉编译器,跟这个变量相关的程序不要做优化
开启了编译优化,则直接对全局变量进行赋值
CPU乱序执行/ CPU顺序执行
目的:为了保证执行的高效
乱序有两层
1.指令的乱序
2.读写的乱序
这些都是CPU内部执行的,需要用CPU机制去解决,进而引入了内存屏障
volatile也可以保证了CPU的顺序执行。
目的:为了保证执行的高效
乱序有两层
1.指令的乱序
2.读写的乱序
这些都是CPU内部执行的,需要用CPU机制去解决,进而引入了内存屏障
volatile也可以保证了CPU的顺序执行。
JVM是怎么知道这个变量被volatile修饰的呢?
字段的访问标志中会有volatile修饰?
字段的访问标志中会有volatile修饰?
对属性或静态属性的操作,对应的bytecodeInterpreter中会有是否有volatile修饰的判断
核心代码.
在不同架构下,屏障指令是不一样的,CPU内核有对称架构和非对称架构。
__asm__ volatile ("lock; addl $0,0 (%%rsp)" : : : "cc", "memory");
强制从内存中去读,不要从寄存器中读
在不同架构下,屏障指令是不一样的,CPU内核有对称架构和非对称架构。
__asm__ volatile ("lock; addl $0,0 (%%rsp)" : : : "cc", "memory");
强制从内存中去读,不要从寄存器中读
可见性
volatile是如何实现可见性的?
1.回写
2.实时触发MESI协议,实时回写内存
volatile跟原子性关系不大
__asm__ volatile ("lock; addl $0, 0 (%rsp%" : : : "cc" , "memory");
既是编译屏障。也是内存屏障
Java中由于JMM的存在,会存在线程可见性的问题,volatile就是告诉JVM要实现线程可见性
原生线程没有这个线程可见性问题
volatile是如何实现可见性的?
1.回写
2.实时触发MESI协议,实时回写内存
volatile跟原子性关系不大
__asm__ volatile ("lock; addl $0, 0 (%rsp%" : : : "cc" , "memory");
既是编译屏障。也是内存屏障
Java中由于JMM的存在,会存在线程可见性的问题,volatile就是告诉JVM要实现线程可见性
原生线程没有这个线程可见性问题
有序性
volatile能保证程序的有序性,其实是在告诉我们CPU会乱序执行,当然还有可能是编译优化造成的。认为乱序执行效率更高
这是编译优化造成的。
研究编译优化有三个层面
1.Java层面(加/不加volatile,生成的字节码是不是一样的,结果是一样的)
2.编译层面
OpenJDK在本身在编译阶段是开启了-O2优化的。可能存在指令重排,为了执行效率.对于部分代码,在编译的时候不希望被优化,就插入编译屏障
3.运行层面
CPU乱序执行
有可能带来的问题,单核情况跟多核情况执行结果不一样。所以有了as-if-serial语义(CPU的约束,不是Java层面)。需要保证单核和多核执行结果都一样
int a = 10;
int sum = a + 20;
a = 20
volatile能保证程序的有序性,其实是在告诉我们CPU会乱序执行,当然还有可能是编译优化造成的。认为乱序执行效率更高
这是编译优化造成的。
研究编译优化有三个层面
1.Java层面(加/不加volatile,生成的字节码是不是一样的,结果是一样的)
2.编译层面
OpenJDK在本身在编译阶段是开启了-O2优化的。可能存在指令重排,为了执行效率.对于部分代码,在编译的时候不希望被优化,就插入编译屏障
3.运行层面
CPU乱序执行
有可能带来的问题,单核情况跟多核情况执行结果不一样。所以有了as-if-serial语义(CPU的约束,不是Java层面)。需要保证单核和多核执行结果都一样
int a = 10;
int sum = a + 20;
a = 20
内存屏障
1.保证了数据已回写内存
2.隔离了写与读
触发实时回写。CPU写内存,内存数据刷硬盘
1.同步写 write through
2.异步写(主流实现),正常情况下,效率比同步要高。会存在延时,但是这个延时是纳秒级别的。write back
正常写程序,一般不需要加volatile,在并发超高的情况下要求数据的实时性,才会加,接受不了纳秒级别的并发
可以通过屏障来让CPU实现同步写,有两种方案:
1.lock (SMP 对称架构下)
2.fence: mfence(读写)、lfence(读)、sfence(写) (NUMA架构下)
HotSpot中有个注解
// always use locked addl since mfence is sometimes expensive
lock是锁总线
fence族本身就是序列化读写,读写操作都放入到一个队列中,是硬件层面的一个CPU竞争,通过硬件信号解决的。这个指令操作比较昂贵
1.保证了数据已回写内存
2.隔离了写与读
触发实时回写。CPU写内存,内存数据刷硬盘
1.同步写 write through
2.异步写(主流实现),正常情况下,效率比同步要高。会存在延时,但是这个延时是纳秒级别的。write back
正常写程序,一般不需要加volatile,在并发超高的情况下要求数据的实时性,才会加,接受不了纳秒级别的并发
可以通过屏障来让CPU实现同步写,有两种方案:
1.lock (SMP 对称架构下)
2.fence: mfence(读写)、lfence(读)、sfence(写) (NUMA架构下)
HotSpot中有个注解
// always use locked addl since mfence is sometimes expensive
lock是锁总线
fence族本身就是序列化读写,读写操作都放入到一个队列中,是硬件层面的一个CPU竞争,通过硬件信号解决的。这个指令操作比较昂贵
happens_before原则(先行发生).一句话概括:前后逻辑关系保障
1.程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构
2.管程锁定规则(Monitor Lock Rule):对于同一个锁,如果一个unlock操作先行发生于一个lock操作,那么该unlock操作所产生地影响对于该lock操作是可见的
3.volatile变量规则(Volatile Variable Rule):对于同一个volatile变量,如果对于这个变量的写操作先行发生于这个变量的读操作,那么对于这个变量的写操作所产生的影响对于这个变量的读操作是可见的。
4.线程启动规则(Thread Start Rule):对于同一个对象,该Thread对象的start()方法先行发生于此线程的每一个动作,也就是说对线程start()方法调用所产生的影响对于该线程的每一个动作都是可见的
5.线程中断规则(Thread Interruption Rule):对同一个线程,线程中发生的所有操作先行发生于对此线程的终止检测,也就是说线程中的所有操作所产生的影响对于调用线程Thread.join()方法或者Thread.isAlive()方法都是可见的
6.对象终结规则(FInalizer Rule):对于同一个对象,它的构造方法执行结束先行发生于它的finalize()方法的开始,也就是说,一个对象的构造方法结束所产生的影响,对于它的finalize()方法开始执行是可见的
7.传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C,也就是说操作A所产生的所有影响对于操作C是可见的
例如new 创建对象,finalizer释放对象的清理工作。finalize方法要在new对象之后执行,不能让它出现先执行finalize方法,再执行new
如果不能保证这个逻辑关系,那么,在finalize方法中,
if (对象是否创建完了, 没有) {
return
}
JVM不能保证这个顺序,需要人为地在代码中去判断,有点不符合现实。于是JVM设计的时候,将提前可知地有先后逻辑关系地程序,内置入JVM中。
如果不能保证,则需要让开发人员自己控制,比如内存屏障。如volatile
1.程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构
2.管程锁定规则(Monitor Lock Rule):对于同一个锁,如果一个unlock操作先行发生于一个lock操作,那么该unlock操作所产生地影响对于该lock操作是可见的
3.volatile变量规则(Volatile Variable Rule):对于同一个volatile变量,如果对于这个变量的写操作先行发生于这个变量的读操作,那么对于这个变量的写操作所产生的影响对于这个变量的读操作是可见的。
4.线程启动规则(Thread Start Rule):对于同一个对象,该Thread对象的start()方法先行发生于此线程的每一个动作,也就是说对线程start()方法调用所产生的影响对于该线程的每一个动作都是可见的
5.线程中断规则(Thread Interruption Rule):对同一个线程,线程中发生的所有操作先行发生于对此线程的终止检测,也就是说线程中的所有操作所产生的影响对于调用线程Thread.join()方法或者Thread.isAlive()方法都是可见的
6.对象终结规则(FInalizer Rule):对于同一个对象,它的构造方法执行结束先行发生于它的finalize()方法的开始,也就是说,一个对象的构造方法结束所产生的影响,对于它的finalize()方法开始执行是可见的
7.传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C,也就是说操作A所产生的所有影响对于操作C是可见的
例如new 创建对象,finalizer释放对象的清理工作。finalize方法要在new对象之后执行,不能让它出现先执行finalize方法,再执行new
如果不能保证这个逻辑关系,那么,在finalize方法中,
if (对象是否创建完了, 没有) {
return
}
JVM不能保证这个顺序,需要人为地在代码中去判断,有点不符合现实。于是JVM设计的时候,将提前可知地有先后逻辑关系地程序,内置入JVM中。
如果不能保证,则需要让开发人员自己控制,比如内存屏障。如volatile
DCL为什么要加volatile?
关键指令
new 创建内存
dup 把创建的对象在栈中复制一份,因为要充当this指针
invokespecial 执行构造函数
putstatic 把指针赋值给静态属性
在超高并发情况下,有乱序执行的情况,创建完内存,还没执行构造函数,就把这个不完全对象指针返回了,本质是解决CPU的乱序执行带来不完全对象的问题。
gcc优化越高,跨平台性越差
DCL为什么要二次判空?
因为第一次判断instance是否为null的时候参与竞争进入同步代码块的线程可能不止一个,但是一次只有一个能够进入同步代码块,其他的线程会进入阻塞状态等待锁的释放。当第一个进入同步代码块的线程创建好实例后退出同步代码块释放锁,这个时候处于阻塞状态线程立马被通知激活重新参与竞争,当获得锁的线程进入同步代码块时其实已经有一个实例了,所以为了保证单例要进行二次检测。
也就是说,第一次判空是判断堆中有没有,第二次的判空是为了防止被阻塞的线程唤醒后再次创建新的对象,当一个线程创建完之后,会立马刷新到主内存当中,这样,后面的线程再进来时,第一个判空条件则不再满足,唤醒后的线程们也会由于第二个判空条件不成立退出,
关键指令
new 创建内存
dup 把创建的对象在栈中复制一份,因为要充当this指针
invokespecial 执行构造函数
putstatic 把指针赋值给静态属性
在超高并发情况下,有乱序执行的情况,创建完内存,还没执行构造函数,就把这个不完全对象指针返回了,本质是解决CPU的乱序执行带来不完全对象的问题。
gcc优化越高,跨平台性越差
DCL为什么要二次判空?
因为第一次判断instance是否为null的时候参与竞争进入同步代码块的线程可能不止一个,但是一次只有一个能够进入同步代码块,其他的线程会进入阻塞状态等待锁的释放。当第一个进入同步代码块的线程创建好实例后退出同步代码块释放锁,这个时候处于阻塞状态线程立马被通知激活重新参与竞争,当获得锁的线程进入同步代码块时其实已经有一个实例了,所以为了保证单例要进行二次检测。
也就是说,第一次判空是判断堆中有没有,第二次的判空是为了防止被阻塞的线程唤醒后再次创建新的对象,当一个线程创建完之后,会立马刷新到主内存当中,这样,后面的线程再进来时,第一个判空条件则不再满足,唤醒后的线程们也会由于第二个判空条件不成立退出,
6.synchrnoized
markword的内存布局
基础知识
对象的MarkWord组成
锁类型
无锁 001
延迟偏向锁: 延迟一段时间才能用偏向锁
偏向锁 101
可偏向
已偏向
轻量级锁 00
重量级锁 10
锁对象
Class对象
普通的Java对象
sync代码块,一眼就能看出来
sync修饰方法,静态方法->Class对象, 非静态->当前对象
用法
synchronized代码块
synchronize修饰方法
--------------------------- 以上有锁膨胀机制
unsafe 只有重量级锁
无锁 001
延迟偏向锁: 延迟一段时间才能用偏向锁
偏向锁 101
可偏向
已偏向
轻量级锁 00
重量级锁 10
锁对象
Class对象
普通的Java对象
sync代码块,一眼就能看出来
sync修饰方法,静态方法->Class对象, 非静态->当前对象
用法
synchronized代码块
synchronize修饰方法
--------------------------- 以上有锁膨胀机制
unsafe 只有重量级锁
为什么能够用MarkWord内存地址的尾三位存储锁类型?8字节对齐
锁对应的HotSpot中的实体类是什么?
偏向锁的实体: 对象头 markOop 在线程栈中是没有记录的 工具类是BiasedLockking
轻量级锁的实体是: BasicLock / BasicObjectLock的_lock属性是BasicLock记录在栈区, 是怎么放到栈上去的,重写操作符
重量级锁的实体是:ObjectMonitor 记录在堆区
整个synchronized的工具类: ObjectSynchronizer:fast_enter() 整个锁机制都是从在这个类里面
偏向锁的实体: 对象头 markOop 在线程栈中是没有记录的 工具类是BiasedLockking
轻量级锁的实体是: BasicLock / BasicObjectLock的_lock属性是BasicLock记录在栈区, 是怎么放到栈上去的,重写操作符
重量级锁的实体是:ObjectMonitor 记录在堆区
整个synchronized的工具类: ObjectSynchronizer:fast_enter() 整个锁机制都是从在这个类里面
无锁
偏向锁
轻量级锁
重量级锁
锁膨胀机制。
延迟偏向之前: 无锁->轻量级锁->重量级锁
延迟偏向之后:可偏向的偏向锁->偏向锁->轻量级锁->重量级锁。。这种个情况下对象默认的锁是可偏向的偏向锁。但是JVM底层还是经历了无锁->可偏向的偏向锁
--------------------------------------------------------------
锁的膨胀
(未过偏向延时时间创建对象)无锁->轻量级锁->重量级锁
(偏向延时之后,安全点开启创建对象)还未偏向的偏向锁->偏向锁->恢复到无锁->轻量级锁->重量级锁
(偏向延时之后,安全点未开启创建对象)还未偏向的偏向锁->偏向锁->无锁->轻量级锁->重量级锁
延迟偏向之前: 无锁->轻量级锁->重量级锁
延迟偏向之后:可偏向的偏向锁->偏向锁->轻量级锁->重量级锁。。这种个情况下对象默认的锁是可偏向的偏向锁。但是JVM底层还是经历了无锁->可偏向的偏向锁
--------------------------------------------------------------
锁的膨胀
(未过偏向延时时间创建对象)无锁->轻量级锁->重量级锁
(偏向延时之后,安全点开启创建对象)还未偏向的偏向锁->偏向锁->恢复到无锁->轻量级锁->重量级锁
(偏向延时之后,安全点未开启创建对象)还未偏向的偏向锁->偏向锁->无锁->轻量级锁->重量级锁
t1 拿到o1的偏向锁
t2 过来抢锁,膨胀成轻量级锁,此时拿到锁仍然是t1。t2再进入轻量级锁的抢锁逻辑,如果发现t1还是存活着的,
t2 接着膨胀成重量级锁,如果此时t1释放了锁,恢复成了无锁,t2抢锁
t2 过来抢锁,膨胀成轻量级锁,此时拿到锁仍然是t1。t2再进入轻量级锁的抢锁逻辑,如果发现t1还是存活着的,
t2 接着膨胀成重量级锁,如果此时t1释放了锁,恢复成了无锁,t2抢锁
synchronized和ReentrantLock对比
都是一种线程锁,保证互斥,用来保证同步
synchronized C++写的
重量级锁 mutex 队列比较多,为什么没有直接用mutex去实现?因为是操作系统的代码,
1.不方便调试
2.实现起来没那么复杂
3. Qmode 希望定制化
AQS ReentrantLock Java写的
CLH队列其实就是EntryList
都是一种线程锁,保证互斥,用来保证同步
synchronized C++写的
重量级锁 mutex 队列比较多,为什么没有直接用mutex去实现?因为是操作系统的代码,
1.不方便调试
2.实现起来没那么复杂
3. Qmode 希望定制化
AQS ReentrantLock Java写的
CLH队列其实就是EntryList
锁的用法
1.代码块
2.修饰方法
---------------------------都是遵循锁膨胀逻辑
3.Unsafe类 # monitorenter 只有mutex的重量级锁,没有膨胀逻辑
1.代码块
2.修饰方法
---------------------------都是遵循锁膨胀逻辑
3.Unsafe类 # monitorenter 只有mutex的重量级锁,没有膨胀逻辑
研究synchronized市面上的资料都是从bytecodeInterpreter字节码解释器的_monitorenter指令已经不再用了改用模板解释器的ObjectSyncrhonizer::fast_enter(),
怎么证明:
Java层面:JOL-core
怎么证明:
Java层面:JOL-core
怎么找native方法
1.常识 Unsafe在JVM中有一个.c的文件
2.找到JVM_xxxx开头的方法,
3.再去Jvm.cpp中去找
1.常识 Unsafe在JVM中有一个.c的文件
2.找到JVM_xxxx开头的方法,
3.再去Jvm.cpp中去找
偏向锁。Synchronize锁类型中最复杂的
JDK15已经打上了去掉的标签了
偏向锁的整体逻辑:
1.如果是匿名偏向状态,单线程环境下,CAS肯定成功,拿到偏向锁,安安稳稳的执行完,释放完偏向锁。注意这里是偏向锁的释放
2.如果是匿名偏向锁状态,多个线程竞争,CAS成功的线程拿到偏向锁,CAS失败的线程就会触发偏向锁撤销及膨胀成轻量级锁
3.如果是偏向锁状态,这种情况和上面说的CAS失败的线程走的代码逻辑基本上是一样的,都是去抢占别的线程已经占有的锁。所以这时候就需要用到安全点,在STW环境下撤销、膨胀,这就是经常说的竞争环境下偏向锁性能差的原因。注意这里,膨胀成轻量级锁,拿到这个轻量级锁的还是原来那个持有偏向锁的线程,而不是来抢锁的线程。抢锁的线程会继续触发膨胀成重量级锁。还有一种情况,就是持有偏向锁的线程已经over,这时候撤销成什么状态得看是否想重新偏向,如果想,撤销为匿名偏向,不想,撤销为无锁
JDK15已经打上了去掉的标签了
偏向锁的整体逻辑:
1.如果是匿名偏向状态,单线程环境下,CAS肯定成功,拿到偏向锁,安安稳稳的执行完,释放完偏向锁。注意这里是偏向锁的释放
2.如果是匿名偏向锁状态,多个线程竞争,CAS成功的线程拿到偏向锁,CAS失败的线程就会触发偏向锁撤销及膨胀成轻量级锁
3.如果是偏向锁状态,这种情况和上面说的CAS失败的线程走的代码逻辑基本上是一样的,都是去抢占别的线程已经占有的锁。所以这时候就需要用到安全点,在STW环境下撤销、膨胀,这就是经常说的竞争环境下偏向锁性能差的原因。注意这里,膨胀成轻量级锁,拿到这个轻量级锁的还是原来那个持有偏向锁的线程,而不是来抢锁的线程。抢锁的线程会继续触发膨胀成重量级锁。还有一种情况,就是持有偏向锁的线程已经over,这时候撤销成什么状态得看是否想重新偏向,如果想,撤销为匿名偏向,不想,撤销为无锁
偏向锁官方解释
synchronized刚开始引入偏向锁的时候,轻量级锁已经是应用态的锁了,为什么还要搞一个偏向锁?CAS是基于lock指令实现的,这个指令不会引起态的切换,即不会引起应用态/用户态切内核态。
官方说:
偏向锁定是HotSpot虚拟机中使用的一种优化技术,用于减少无竞争锁定的开销。它旨在避免在获取监视器时执行比较和交换原子操作,方法是假设监视器一直归给定线程所有,直到不同的线程尝试获取它。监视器的初始锁定使监视器偏向于改线程,从而避免在对同一对象的后续同步操作中需要原子指令。当许多线程对以单线程方式使用的对象执行许多同步操作时,与常规锁定技术相比,偏向锁历来会导致显著的性能改进。
JDK15开始,开始考虑默认关闭偏向锁,由业务方决定是否开启,具体看官方怎么说:
过去看到的性能提升今天远没有那么明显。许多收益与偏向锁定的应用程序时使用早期Java集合API的较旧的遗留应用程序,这些API在每次访问时进行同步(例如Hashtable和Vector).较新的应用程序通常使用非同步集合(例如,HashMap和ArrayList),在Java1.2中引入单线程场景,或者在Java5中引入用于多线程场景的更高性能的并发数据结构。这意味着如果更新代码以使用这些较新的类,由于不必要的同步而受益于偏向锁定的应用程序可能会看到性能改进。
此外,围绕线程池队列和工作线程构建的应用程序通常在金庸偏向锁定的情况下性能更好。(SPECjbb2015就是这样涉及的,例如,而SPECjvm98和SPECjbb2005则不是)
偏向锁定带来了在争用情况下需要昂贵的撤销操作的成本。因此,受益于它的应用程序只有哪些表现出大量无竞争同步操作的应用程序,如上面提到的哪些,因此,执行廉价的锁所有者检查加上偶尔的昂贵撤销的成本仍然低于执行躲避的比较和交换原子指令的成本。自从将偏向锁定引入HotSpot依赖,原子指令成本的变化也改变了该关系保持真实所需的无竞争操作的数量。另一个值得注意的方面是,即使在之前的成本关系是正确的情况下,当花费在同步操作上的时间仍然只占应用程序工作负载的一小部分时,应用程序也不会从偏向锁定中获得明显的性能改进。
偏向锁定在同步子系统中引入了大量复杂的代码,并且对其他HotSpot组件也具有侵入性。这种复杂性时理解代码各个部分的障碍,也是在同步子系统内进行重大涉及更改的障碍。为此,我们希望禁用、弃用并最终删除对偏向锁定的支持
总结:
1.目前大多数Java程序都不会使用那些使用了syncrhonized的类库如Hashtable、Vector,而是用无锁的类库,需要锁的时候自己加锁
2.偏向锁撤销的成本很高,需要在安全点下才能干净地完成。言外之意就是撤销偏向锁时,需要STW
3.偏向锁的代码很复杂,又侵入了其他业务分支,导致代码难以理解难以维护难以拓展
synchronized刚开始引入偏向锁的时候,轻量级锁已经是应用态的锁了,为什么还要搞一个偏向锁?CAS是基于lock指令实现的,这个指令不会引起态的切换,即不会引起应用态/用户态切内核态。
官方说:
偏向锁定是HotSpot虚拟机中使用的一种优化技术,用于减少无竞争锁定的开销。它旨在避免在获取监视器时执行比较和交换原子操作,方法是假设监视器一直归给定线程所有,直到不同的线程尝试获取它。监视器的初始锁定使监视器偏向于改线程,从而避免在对同一对象的后续同步操作中需要原子指令。当许多线程对以单线程方式使用的对象执行许多同步操作时,与常规锁定技术相比,偏向锁历来会导致显著的性能改进。
JDK15开始,开始考虑默认关闭偏向锁,由业务方决定是否开启,具体看官方怎么说:
过去看到的性能提升今天远没有那么明显。许多收益与偏向锁定的应用程序时使用早期Java集合API的较旧的遗留应用程序,这些API在每次访问时进行同步(例如Hashtable和Vector).较新的应用程序通常使用非同步集合(例如,HashMap和ArrayList),在Java1.2中引入单线程场景,或者在Java5中引入用于多线程场景的更高性能的并发数据结构。这意味着如果更新代码以使用这些较新的类,由于不必要的同步而受益于偏向锁定的应用程序可能会看到性能改进。
此外,围绕线程池队列和工作线程构建的应用程序通常在金庸偏向锁定的情况下性能更好。(SPECjbb2015就是这样涉及的,例如,而SPECjvm98和SPECjbb2005则不是)
偏向锁定带来了在争用情况下需要昂贵的撤销操作的成本。因此,受益于它的应用程序只有哪些表现出大量无竞争同步操作的应用程序,如上面提到的哪些,因此,执行廉价的锁所有者检查加上偶尔的昂贵撤销的成本仍然低于执行躲避的比较和交换原子指令的成本。自从将偏向锁定引入HotSpot依赖,原子指令成本的变化也改变了该关系保持真实所需的无竞争操作的数量。另一个值得注意的方面是,即使在之前的成本关系是正确的情况下,当花费在同步操作上的时间仍然只占应用程序工作负载的一小部分时,应用程序也不会从偏向锁定中获得明显的性能改进。
偏向锁定在同步子系统中引入了大量复杂的代码,并且对其他HotSpot组件也具有侵入性。这种复杂性时理解代码各个部分的障碍,也是在同步子系统内进行重大涉及更改的障碍。为此,我们希望禁用、弃用并最终删除对偏向锁定的支持
总结:
1.目前大多数Java程序都不会使用那些使用了syncrhonized的类库如Hashtable、Vector,而是用无锁的类库,需要锁的时候自己加锁
2.偏向锁撤销的成本很高,需要在安全点下才能干净地完成。言外之意就是撤销偏向锁时,需要STW
3.偏向锁的代码很复杂,又侵入了其他业务分支,导致代码难以理解难以维护难以拓展
为什么要有偏向锁?
synchronize锁机制是以synchronizer::fast_enter作为入口的。
HotSpot的开发人员认为CAS也比较费性能,考虑到很多情况下,都是那一个线程加锁。加锁很简单,但是解锁需要在安全点。
安全点意味着STW.偏向锁膨胀成轻量级锁时需要撤销到无锁状态,意味着线程需要进入到安全点才可以,
1.JVM中很多机制都用到了fast_enter
2.偏向锁的撤销需要STW,影响JVM启动时间
3.JVM默认让自己启动完成以后才开启偏向锁
JVM为什么要延迟4s才开启偏向锁?
比如说JVM在启动的时候,需要加载很多类,每次用完之后需要撤销偏向锁,在这种情况下,会经常进入安全点,HotSpot给出了一个4s的时间
JVM虚拟机(JVM)中的偏向锁(Biased Locking)是一种优化锁性能的技术,它可以减少无竞争锁的开销。偏向锁的基本原理是,当一个线程访问一个同步块时,如果该同步块没有其他线程竞争,那么这个锁会"偏向"于第一个获得它的线程。在后面的锁操作中,如果该线程再次请求这个锁,那么不需要进行重量级的CAS同步操作,从而提高性能。直接比较线程id即可。
1.启动性能: 在JVM启动的初期,通常会执行大量的类加载和初始化操作,这些操作往往涉及大量的锁竞争。在这个阶段,偏向锁可能不仅不会带来性能上的提升,反而可能因为偏向锁的撤销和再偏向而降低性能
2.代码预热:JVM在启动后的一段时间内会进行代码预热,即执行一些编译优化,在这段时间内,许多锁的操作可能还不足以表现出明显的偏好模式,因此开启偏向锁得收益不大
3.竞争模式判断:JVM需要一定的时间来观察程序的运行特性,判断是否存在锁竞争。如果在启动初期就开启偏向锁,可能会由于竞争模式的不明确而导致偏向锁的不当使用
4.默认设置:JVM的默认设置是针对广泛的场景涉及的,它需要在各种不同的应用场景中尽可能提供最优的性能。因此,延迟开启偏向锁是一种比较保守且安全的策略。
4s的延迟是一个经验值,它基于大量实际应用的性能测试结果。这个延迟时间可以让JVM有足够的时间来观察程序的运行特性,并在适当的时机开启偏向锁,从而在不影响启动性能的情况下,提高后续运行时的锁操作效率。
当然,如果你对你的应用程序有足够的了解,并且确定偏向锁能够带来性能上的提升,可以通过JVM参数来调整偏向锁的开启时间,或者在启动时开启偏向锁。例如使用-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 可以使得JVM在启动时立即开启偏向锁
synchronize锁机制是以synchronizer::fast_enter作为入口的。
HotSpot的开发人员认为CAS也比较费性能,考虑到很多情况下,都是那一个线程加锁。加锁很简单,但是解锁需要在安全点。
安全点意味着STW.偏向锁膨胀成轻量级锁时需要撤销到无锁状态,意味着线程需要进入到安全点才可以,
1.JVM中很多机制都用到了fast_enter
2.偏向锁的撤销需要STW,影响JVM启动时间
3.JVM默认让自己启动完成以后才开启偏向锁
JVM为什么要延迟4s才开启偏向锁?
比如说JVM在启动的时候,需要加载很多类,每次用完之后需要撤销偏向锁,在这种情况下,会经常进入安全点,HotSpot给出了一个4s的时间
JVM虚拟机(JVM)中的偏向锁(Biased Locking)是一种优化锁性能的技术,它可以减少无竞争锁的开销。偏向锁的基本原理是,当一个线程访问一个同步块时,如果该同步块没有其他线程竞争,那么这个锁会"偏向"于第一个获得它的线程。在后面的锁操作中,如果该线程再次请求这个锁,那么不需要进行重量级的CAS同步操作,从而提高性能。直接比较线程id即可。
1.启动性能: 在JVM启动的初期,通常会执行大量的类加载和初始化操作,这些操作往往涉及大量的锁竞争。在这个阶段,偏向锁可能不仅不会带来性能上的提升,反而可能因为偏向锁的撤销和再偏向而降低性能
2.代码预热:JVM在启动后的一段时间内会进行代码预热,即执行一些编译优化,在这段时间内,许多锁的操作可能还不足以表现出明显的偏好模式,因此开启偏向锁得收益不大
3.竞争模式判断:JVM需要一定的时间来观察程序的运行特性,判断是否存在锁竞争。如果在启动初期就开启偏向锁,可能会由于竞争模式的不明确而导致偏向锁的不当使用
4.默认设置:JVM的默认设置是针对广泛的场景涉及的,它需要在各种不同的应用场景中尽可能提供最优的性能。因此,延迟开启偏向锁是一种比较保守且安全的策略。
4s的延迟是一个经验值,它基于大量实际应用的性能测试结果。这个延迟时间可以让JVM有足够的时间来观察程序的运行特性,并在适当的时机开启偏向锁,从而在不影响启动性能的情况下,提高后续运行时的锁操作效率。
当然,如果你对你的应用程序有足够的了解,并且确定偏向锁能够带来性能上的提升,可以通过JVM参数来调整偏向锁的开启时间,或者在启动时开启偏向锁。例如使用-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 可以使得JVM在启动时立即开启偏向锁
偏向锁什么情况下会直接膨胀成重量级锁?
1.竞争激烈:当多个线程频繁尝试获取同一个对象的锁是,偏向锁的撤销成本可能会变得很高。如果系统检测到偏向锁的撤销过于频繁,JVM可能会决定直接将锁膨胀成重量级锁,以减少撤销偏向锁的开销
2.偏向锁撤销:当持有偏向锁的线程已经结束,或者另一个线程尝试获取锁时,如果偏向锁不能简单地通过撤销来让另一个线程获取锁(例如,因为持有偏向锁的线程正在同步代码块中),则可能需要膨胀成重量级锁
3.系统设置:JVM启动参数可能会影响偏向锁的行为。例如,如果通过-XX:-UseBiasedLocking参数禁用了偏向锁,那么在锁竞争的情况下,锁可能会直接从轻量级锁膨胀成重量级锁
4.安全点操作:偏向锁的撤销需要在安全点下进行,这是一个所有线程都暂停的点。如果在撤销偏向锁的过程中发现竞争非常激烈,系统可能会选择直接膨胀为重量级锁,以避免在安全点频繁进行偏向锁的撤销操作
5.偏向锁模式不适合: 在某些情况下,如果JVM检测到偏向锁的模式不再适合当前的应用程序行为(例如,频繁的锁竞争),则可能会直接将锁膨胀为重量级锁
6.线程挂起: 如果持有偏向锁的线程被挂起,例如(因为等待IO操作),那么其他尝试获取锁的线程可能会使锁膨胀为重量级锁,以便进行线程调度。
1.竞争激烈:当多个线程频繁尝试获取同一个对象的锁是,偏向锁的撤销成本可能会变得很高。如果系统检测到偏向锁的撤销过于频繁,JVM可能会决定直接将锁膨胀成重量级锁,以减少撤销偏向锁的开销
2.偏向锁撤销:当持有偏向锁的线程已经结束,或者另一个线程尝试获取锁时,如果偏向锁不能简单地通过撤销来让另一个线程获取锁(例如,因为持有偏向锁的线程正在同步代码块中),则可能需要膨胀成重量级锁
3.系统设置:JVM启动参数可能会影响偏向锁的行为。例如,如果通过-XX:-UseBiasedLocking参数禁用了偏向锁,那么在锁竞争的情况下,锁可能会直接从轻量级锁膨胀成重量级锁
4.安全点操作:偏向锁的撤销需要在安全点下进行,这是一个所有线程都暂停的点。如果在撤销偏向锁的过程中发现竞争非常激烈,系统可能会选择直接膨胀为重量级锁,以避免在安全点频繁进行偏向锁的撤销操作
5.偏向锁模式不适合: 在某些情况下,如果JVM检测到偏向锁的模式不再适合当前的应用程序行为(例如,频繁的锁竞争),则可能会直接将锁膨胀为重量级锁
6.线程挂起: 如果持有偏向锁的线程被挂起,例如(因为等待IO操作),那么其他尝试获取锁的线程可能会使锁膨胀为重量级锁,以便进行线程调度。
延迟偏向是什么?
有个调优参数,JVM启动的时候偏向锁是不开启的,是有延迟的,
java -XX:+PrintFlagsFinal -version | grep Dealy
BiasedLockingStartupDelay 4s
有个调优参数,JVM启动的时候偏向锁是不开启的,是有延迟的,
java -XX:+PrintFlagsFinal -version | grep Dealy
BiasedLockingStartupDelay 4s
为什么要延迟4s呢?答案在HotSpot的源码注释当中
翻译如下:
如果启用了偏向锁,则调度一个任务在运行时启动几秒钟
这将为当前加载的所有类以及未来加载的类开启偏向锁。这是一种解决启动时间倒退的方法
因为在VM启动期间,为了取消偏差而采取大量安全点。
理想情况下,我们将有一个更低的成本,以消除个人偏见,而不需要这样的机制
翻译如下:
如果启用了偏向锁,则调度一个任务在运行时启动几秒钟
这将为当前加载的所有类以及未来加载的类开启偏向锁。这是一种解决启动时间倒退的方法
因为在VM启动期间,为了取消偏差而采取大量安全点。
理想情况下,我们将有一个更低的成本,以消除个人偏见,而不需要这样的机制
新创建的对象的锁是如何被延迟偏向影响的?
延迟偏向之前:无锁.
为什么新创建的对象是无锁?
1.Java对象,在JVM中就是一个oop
2.对象头就是markOop.C++里面的一个对象
private:
volatile markOop _mark;
1.Java对象,在JVM中就是一个oop
2.对象头就是markOop.C++里面的一个对象
private:
volatile markOop _mark;
创建一个对象的时候通过allocate_instance(),
Java的对象是内存编织的。申请内存和属性赋值是分开的,这个方法是申请内存,new Object(); 对象占16字节。
Java的对象是内存编织的。申请内存和属性赋值是分开的,这个方法是申请内存,new Object(); 对象占16字节。
调用obj_allocate()申请内存返回一个HeapWord指针,但是还没有进行属性赋值
接着我们再来看post_allocate_setup_obj()方法,这个里面主要看post_allocate_setup_common()方法,
如果使用了偏向锁就将对象的markword设置为klass->prototype_header();
反之则设置为markOopDEsc::prototype.
反之则设置为markOopDEsc::prototype.
对象头会存在两个地方
klass.hpp中有个属性_prototype_header对象头
oop_mark 是从klass_prototype_header中copy过来的。
当开启了偏向锁的时候取决于klass的prototype_header是无锁还是偏向锁
当偏向锁还没开启的时候,比如说JVM刚启动的时候,此时就是无锁,markOopDesc::prototype(),所以新创建的对象是无锁
klass.hpp中有个属性_prototype_header对象头
oop_mark 是从klass_prototype_header中copy过来的。
当开启了偏向锁的时候取决于klass的prototype_header是无锁还是偏向锁
当偏向锁还没开启的时候,比如说JVM刚启动的时候,此时就是无锁,markOopDesc::prototype(),所以新创建的对象是无锁
延迟偏向之后: 可偏向的偏向锁
延迟偏向开启之后,为什么是偏向锁?(klass._prototype_header是如何变成偏向锁的)
拆分成两个问题
1.klass在创建的时候,是什么?
看klass的构造函数,里面有句代码set_prototype_header(markOopDesc::prototype());
拆分成两个问题
1.klass在创建的时候,是什么?
看klass的构造函数,里面有句代码set_prototype_header(markOopDesc::prototype());
2.何时变成偏向锁的?
JVM肯定是有一个时机把klass._prototype_header改成了偏向锁。init()方法
JVM肯定是有一个时机把klass._prototype_header改成了偏向锁。init()方法
BiasedLocking.cpp#init()该方法是在thread#create_vm()方法中被调用的BiasedLocking::#init()
在开启偏向锁的情况下,如果开启了延迟偏向,则向VMThread提交一个VMOperation的异步任务,否则立即执行
VMThread除了执行GC操作,还会执行其他一些异步任务,封装成VMOperation,使之入队,交给VMThread执行,
在开启偏向锁的情况下,如果开启了延迟偏向,则向VMThread提交一个VMOperation的异步任务,否则立即执行
VMThread除了执行GC操作,还会执行其他一些异步任务,封装成VMOperation,使之入队,交给VMThread执行,
biasedLocking.cpp#doit()
SystemDictionary::clases_do(enable_biased_locking()).把延迟偏向之前加载的类,遍历,把它们设置为偏向锁
SystemDictionary::clases_do(enable_biased_locking()).把延迟偏向之前加载的类,遍历,把它们设置为偏向锁
enable_biased_locking()方法内开启偏向锁
k->set_prototype_header(markOOpDesc::biased_locking_prototype())
k->set_prototype_header(markOOpDesc::biased_locking_prototype())
dictionary.cpp#classes_do()开始遍历。f(k)
注意:延迟偏向之前创建的对象是无锁,延迟偏向之后,仍然是无锁
延迟偏向开启之后,把所有加载的类已经改成偏向锁了,klass._prototype_header(),但是新加载的类创建的对象为什么是偏向锁呢?
延迟开启之后,创建的对象为什么是偏向锁? klass._prototype_header()是如何变成偏向锁的?
遍历的是klass并非oop,之前创建的对象的锁状态不会发生变化
切入点:_prototype_header是如何赋值的?
klass.inline.hpp#set_prototype_header()
update_dictionary()中去更改klass._prototype_header()
1.创建klass对象
构造函数里面已经初始化
2.根据条件去改变
systemDictionary#update_dictionary()
延迟偏向开启之后,把所有加载的类已经改成偏向锁了,klass._prototype_header(),但是新加载的类创建的对象为什么是偏向锁呢?
延迟开启之后,创建的对象为什么是偏向锁? klass._prototype_header()是如何变成偏向锁的?
遍历的是klass并非oop,之前创建的对象的锁状态不会发生变化
切入点:_prototype_header是如何赋值的?
klass.inline.hpp#set_prototype_header()
update_dictionary()中去更改klass._prototype_header()
1.创建klass对象
构造函数里面已经初始化
2.根据条件去改变
systemDictionary#update_dictionary()
加载的Test2创建的对象是偏向锁
systemDictionary#update_dictionary
klass和oop都有一个markWord
如何只调试自己想要的类
ResourceMark resourceMark;
const char* name = external_name();
if (0 == strcmp(name, "com.xxxxxx.Test_1") {
int i = 10;
}
if (0 == strcmp(name, "com/xxxx/Test_1") {
}
HotSpot有时候用点有时候用斜杠
ResourceMark resourceMark;
const char* name = external_name();
if (0 == strcmp(name, "com.xxxxxx.Test_1") {
int i = 10;
}
if (0 == strcmp(name, "com/xxxx/Test_1") {
}
HotSpot有时候用点有时候用斜杠
延迟偏向之前加载的类的初始锁是什么锁?
延迟偏向之后加载的类是无锁还是偏向锁
轻量级锁实体: BasicObjectLock的一个_lock属性是BasicLock
BiasedLocking不能理解成是偏向锁实体,它就是一个工具类,类似SytemDirectory
BiasedLocking::revoke_and_rebias撤销和重偏向
fast_enter()如果还未置为偏向锁,就走slow_enter判断是否为无锁,如果是,则加轻量级锁,否则就上重量级锁
BiasedLocking::revoke_and_rebias撤销和重偏向
fast_enter()如果还未置为偏向锁,就走slow_enter判断是否为无锁,如果是,则加轻量级锁,否则就上重量级锁
ObjectLocker:类的初始化要上锁,上的就是这把锁,轻量级锁
ObjectLocker加锁和解锁
轻量级锁重入
生成LockRecord,displaced_header为null。其实这种说法有点问题,C++的析构函数会解锁的。
在JVM层面轻量级锁重入是不管的,为什么?因为轻量级锁其实是不需要处理的,它是一种CAS操作,不像是mutex_lock有明显的加锁重入,需要解锁
生成LockRecord,displaced_header为null。其实这种说法有点问题,C++的析构函数会解锁的。
在JVM层面轻量级锁重入是不管的,为什么?因为轻量级锁其实是不需要处理的,它是一种CAS操作,不像是mutex_lock有明显的加锁重入,需要解锁
epoch的作用
判断当前持有的偏向锁是否已经过期?
incr_bias_epoch
A obj 偏向锁,异步执行一个批量任务,obj 又被线程B加锁,那么就会增加
klass->_prototype_header->epoch() != mark->bias-_epoch()
判断两者是否不相等,如果不相等,则认为是过期了
其本质是一个时间戳,代表了偏向锁的有效性,epoch存储再可偏向对象的MarkWord中。
1.除了对象中的epoch,对象所属的类class信息中,也会保存一个epoch值
2.每当遇到一个全局安全点时(这里的意思是说批量重偏向没有完全替代了全局安全点,全局安全点是一直存在的)。比如要对class C进行批量重偏向,则首先对class C中保存的epoch进行增加操作,得到一个新的epoch_new
3.然后扫描所有持有class C实例的线程栈,根据线程栈的信息判断出该线程是否锁定了该对象,仅将epoch_new的值赋给被锁定的对象,也就是现在偏向锁还在被使用的对象才会被赋值epoch_new
4.退出安全点后,当由线程需要尝试获取偏向锁是,直接检查classC中存储的peoch值是否与目标对象中存储的epoch相等,如果不相等,则说明该对象的偏向锁已经无效了,因为步骤3里面已经说了只有偏向锁还在被使用的对象才会有epoch_new,这里不相等的原因是class C里面的值是epoch_new,而当前对象的epoch里面的值还是epoch,此时竞争线程可以尝试对此对象重新进行偏向操作
判断当前持有的偏向锁是否已经过期?
incr_bias_epoch
A obj 偏向锁,异步执行一个批量任务,obj 又被线程B加锁,那么就会增加
klass->_prototype_header->epoch() != mark->bias-_epoch()
判断两者是否不相等,如果不相等,则认为是过期了
其本质是一个时间戳,代表了偏向锁的有效性,epoch存储再可偏向对象的MarkWord中。
1.除了对象中的epoch,对象所属的类class信息中,也会保存一个epoch值
2.每当遇到一个全局安全点时(这里的意思是说批量重偏向没有完全替代了全局安全点,全局安全点是一直存在的)。比如要对class C进行批量重偏向,则首先对class C中保存的epoch进行增加操作,得到一个新的epoch_new
3.然后扫描所有持有class C实例的线程栈,根据线程栈的信息判断出该线程是否锁定了该对象,仅将epoch_new的值赋给被锁定的对象,也就是现在偏向锁还在被使用的对象才会被赋值epoch_new
4.退出安全点后,当由线程需要尝试获取偏向锁是,直接检查classC中存储的peoch值是否与目标对象中存储的epoch相等,如果不相等,则说明该对象的偏向锁已经无效了,因为步骤3里面已经说了只有偏向锁还在被使用的对象才会有epoch_new,这里不相等的原因是class C里面的值是epoch_new,而当前对象的epoch里面的值还是epoch,此时竞争线程可以尝试对此对象重新进行偏向操作
轻量级锁
什么是执行流?
__ movptr
__ 是一种宏定义,代码生成器的意思
movptr 汇编指令
1.HotSpot源码中的执行流是什么意思
生成执行流
JVM初始化启动时,申请一块内存用来存储执行流,JVM会编译生成的汇编代码所对应的机器码,然后声明一个指针指向这块代码,这就叫硬编码。下断点的话,需要在内存中去调试
执行流如何存储
执行流如何运行
void *(*lock_method_type)(void*);
lock_method_type lock_method = 0x7ff324234;
lock_method();
2.synchronized相关的执行流有哪些
monitorenter:
字节码解释器 bytecodeInterpreter.cpp
模板解释器 TemplateTable::monitorenter()_x86_64.cpp (主流)
方法的修饰符
call_stsub->entry_point->InterpreterGenerator::lock_method
__ movptr
__ 是一种宏定义,代码生成器的意思
movptr 汇编指令
1.HotSpot源码中的执行流是什么意思
生成执行流
JVM初始化启动时,申请一块内存用来存储执行流,JVM会编译生成的汇编代码所对应的机器码,然后声明一个指针指向这块代码,这就叫硬编码。下断点的话,需要在内存中去调试
执行流如何存储
执行流如何运行
void *(*lock_method_type)(void*);
lock_method_type lock_method = 0x7ff324234;
lock_method();
2.synchronized相关的执行流有哪些
monitorenter:
字节码解释器 bytecodeInterpreter.cpp
模板解释器 TemplateTable::monitorenter()_x86_64.cpp (主流)
方法的修饰符
call_stsub->entry_point->InterpreterGenerator::lock_method
锁实体
偏向锁 锁信息存在对象头 BiasedLocking 工具类,专门处理偏向锁逻辑
BasicLock
轻量级锁
BasicObjectLock (lock record 实体), BasicLock和BasicObjectLock是一种包含关系,BasicLock包含了BasicObjectLock
重量级锁
ObjectMonitor ObjectSynchronizer 工具类
偏向锁 锁信息存在对象头 BiasedLocking 工具类,专门处理偏向锁逻辑
BasicLock
轻量级锁
BasicObjectLock (lock record 实体), BasicLock和BasicObjectLock是一种包含关系,BasicLock包含了BasicObjectLock
重量级锁
ObjectMonitor ObjectSynchronizer 工具类
为什么要生成lock_record?
1.兼容轻量级锁
2.为了批量撤销与批量重偏向
display_header是一个随机数,这块是用不到的,偏向锁用不到
oop当前的锁对象内存地址写进去
1.兼容轻量级锁
2.为了批量撤销与批量重偏向
display_header是一个随机数,这块是用不到的,偏向锁用不到
oop当前的锁对象内存地址写进去
synchronized修饰方法,多次调用则创建多个栈帧
synchronized修饰代码块,多次调用就在一个栈帧里面生成多个lock record
synchronized修饰代码块,多次调用就在一个栈帧里面生成多个lock record
synchronized修饰方法的执行流在lock_method()方法里面
1.自动识别锁对象: this、Class对象(InstanceMirrorClass)
2.在堆中创建lock record(BasicObjectLock对象 16字节,两个属性,一个是BasicLock _lock; 一个是oop _obj都是8字节,共16字节)
3.调用lock_object
1.自动识别锁对象: this、Class对象(InstanceMirrorClass)
2.在堆中创建lock record(BasicObjectLock对象 16字节,两个属性,一个是BasicLock _lock; 一个是oop _obj都是8字节,共16字节)
3.调用lock_object
轻量级锁的释放过程/重入
当锁记录的displaced_header = NULL时,标识synchronized嵌套的情形,此时不需要恢复对象的对象头,只有在最外层解锁时才会恢复对象头。将displaced_header中的对象头写入锁对象动作是CAS的,如果成功,解锁成功,如果失败,说明某个线程已经将该轻量级锁膨胀成重量级锁了,需要获取对应的重量级锁,完成解锁动作。对比偏向锁的解锁实现可知,轻量级锁支持多个线程占有,但是必须交替的,不能是同时的。另外很多博客说轻量级锁膨胀成重量级锁前有一个自旋等待的动作,这其实是错误的,轻量级锁的实现只使用了BasicObjectLock的一个数据结构,无法支持自旋等待,因为没有地方记录自旋等待的次数,倒是重量级锁有单独的数据结构可以支持复杂的自旋逻辑。
在slow_exit中,如果CAS恢复对象头MarkWord失败,也会调用inflate获取Monitor对象,调用Monitor.exit最终走重量级锁释放
AQS不需要记录重入次数它的锁状态位是记录在mark word,不像mutex.lock() 需要对应次数的mutex.unlock()
如果displaced_header为NULL,不需要做处理,如果非空,CAS改mark word
当锁记录的displaced_header = NULL时,标识synchronized嵌套的情形,此时不需要恢复对象的对象头,只有在最外层解锁时才会恢复对象头。将displaced_header中的对象头写入锁对象动作是CAS的,如果成功,解锁成功,如果失败,说明某个线程已经将该轻量级锁膨胀成重量级锁了,需要获取对应的重量级锁,完成解锁动作。对比偏向锁的解锁实现可知,轻量级锁支持多个线程占有,但是必须交替的,不能是同时的。另外很多博客说轻量级锁膨胀成重量级锁前有一个自旋等待的动作,这其实是错误的,轻量级锁的实现只使用了BasicObjectLock的一个数据结构,无法支持自旋等待,因为没有地方记录自旋等待的次数,倒是重量级锁有单独的数据结构可以支持复杂的自旋逻辑。
在slow_exit中,如果CAS恢复对象头MarkWord失败,也会调用inflate获取Monitor对象,调用Monitor.exit最终走重量级锁释放
AQS不需要记录重入次数它的锁状态位是记录在mark word,不像mutex.lock() 需要对应次数的mutex.unlock()
如果displaced_header为NULL,不需要做处理,如果非空,CAS改mark word
如何单步调试synchronized相关执行流?
1.找到执行流的内存地址_monitorenter
2.下断
3.如何研究
添加JVM指令 -XX:+PrintInterpreter
monitorenter对应的字节码序号为194
fast_enter()JVM中很多地方都会进入这个方法
比如说研究ldc,需要在我们在自己的类中去下断点,由于执行流写不了if-else,所以需要写函数调用,
1.找到执行流的内存地址_monitorenter
2.下断
3.如何研究
添加JVM指令 -XX:+PrintInterpreter
monitorenter对应的字节码序号为194
fast_enter()JVM中很多地方都会进入这个方法
比如说研究ldc,需要在我们在自己的类中去下断点,由于执行流写不了if-else,所以需要写函数调用,
update_heuristics智能撤销控制器
计数器
k->last_biased_lock_b ulk_revocation_time();
每撤销一次,计数器就会+1,当这个次数达到20次是,触发批量重偏向。达到40次的时候触发批量撤销。小于20次,执行单次撤销
偏向锁的撤销需要在安全点下进行。
1.signal
2.bulk_revoke
VMThread在执行任务时会触发安全点。
批量撤销:wait方法也会撤销,比如说经常会调用wait,撤销达到40次,发现比较消耗性能,改成无锁,直接走轻量级锁逻辑
批量重偏向: epoch改掉,下次上锁时发现oop.epoch和klass.epoch不相等,则重偏向
计数器
k->last_biased_lock_b ulk_revocation_time();
每撤销一次,计数器就会+1,当这个次数达到20次是,触发批量重偏向。达到40次的时候触发批量撤销。小于20次,执行单次撤销
偏向锁的撤销需要在安全点下进行。
1.signal
2.bulk_revoke
VMThread在执行任务时会触发安全点。
批量撤销:wait方法也会撤销,比如说经常会调用wait,撤销达到40次,发现比较消耗性能,改成无锁,直接走轻量级锁逻辑
批量重偏向: epoch改掉,下次上锁时发现oop.epoch和klass.epoch不相等,则重偏向
批量重偏向逻辑
遍历线程
批量重偏向:
如果一个类的大量对象被一个线程T1执行了同步操作,也就是大量对象先偏向了T1,T1同步结束后,另一个线程也将这些对象作为锁对象进行操作,会导致偏向锁重偏向的操作
运行结果分析:
1.当一个线程t1运行结束后,所有的对象都偏向t1
2.线程t2只对前30个对象进行了同步,0-18的对象会从偏向锁101升级为轻量级锁00,19-29的对象由于撤销次数达到20,触发批量重偏向,偏向线程t2
3.t2结束后,0-18的对象由轻量级锁释放后变成了无锁,19-29的对象偏向t2,30-49的对象还是偏向t1
批量重偏向会以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向
https://blog.csdn.net/u022812849/article/details/108531031
如果一个类的大量对象被一个线程T1执行了同步操作,也就是大量对象先偏向了T1,T1同步结束后,另一个线程也将这些对象作为锁对象进行操作,会导致偏向锁重偏向的操作
运行结果分析:
1.当一个线程t1运行结束后,所有的对象都偏向t1
2.线程t2只对前30个对象进行了同步,0-18的对象会从偏向锁101升级为轻量级锁00,19-29的对象由于撤销次数达到20,触发批量重偏向,偏向线程t2
3.t2结束后,0-18的对象由轻量级锁释放后变成了无锁,19-29的对象偏向t2,30-49的对象还是偏向t1
批量重偏向会以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向
https://blog.csdn.net/u022812849/article/details/108531031
批量撤销:
当一个偏向锁如果撤销次数达到40的时候就认为这个对象设计的有问题,那么JVM会把这个对象锁对应的类所有对象撤销偏向锁,并且新实例化的对象也是不可偏向的
当一个偏向锁如果撤销次数达到40的时候就认为这个对象设计的有问题,那么JVM会把这个对象锁对应的类所有对象撤销偏向锁,并且新实例化的对象也是不可偏向的
运行结果分析:
1.t1执行完后,0-59的对象偏向t1
2.t2执行完后,0-18的对象为无锁,19-59偏向t2
3.t3执行时由于之前执行过批量重偏向了,所以这里会升级为轻量级锁
4.t4休眠前对象65为匿名偏向状态,t4休眠后,由于触发了批量撤销,所以锁状态变为轻量级锁,所以批量撤销会把正在执行同步的对象的锁状态
由偏向锁变为轻量级锁,而不再执行同步的对象的锁不会改变,如对象66
批量重偏向和批量撤销是针对类的优化,和对象无关,偏向锁重偏向一次之后不可再次重偏向。当某个类已经已经触发批量撤销机制后,JVM会默认
当前的类产生了严重的问题,剥夺该类的新实例对象使用偏向锁的权利
1.t1执行完后,0-59的对象偏向t1
2.t2执行完后,0-18的对象为无锁,19-59偏向t2
3.t3执行时由于之前执行过批量重偏向了,所以这里会升级为轻量级锁
4.t4休眠前对象65为匿名偏向状态,t4休眠后,由于触发了批量撤销,所以锁状态变为轻量级锁,所以批量撤销会把正在执行同步的对象的锁状态
由偏向锁变为轻量级锁,而不再执行同步的对象的锁不会改变,如对象66
批量重偏向和批量撤销是针对类的优化,和对象无关,偏向锁重偏向一次之后不可再次重偏向。当某个类已经已经触发批量撤销机制后,JVM会默认
当前的类产生了严重的问题,剥夺该类的新实例对象使用偏向锁的权利
见草稿图
重量级锁
三个队列
ObjectMonitor.hpp
_EntryList 调度队列 ,线程唤醒需要从这个队列中做
_cxq 竞争队列,互斥排队,一个线程抢到锁,其他线程进入到这个队列中去
_WaitSet 同步队列。调用wait方法阻塞的线程队列,用来形成Java的阻塞唤醒机制,
重量级为什么要有三个队列?为什么不直接用Linux中的Mutex+Cond实现互斥与同步?
本质原因就是给程序员足够的自由。因为它本质是一个管程模型,要实现同步与互斥,让你自己通过参数的调整,对同步场景或互斥场景更加友好。。
如果你的业务模型是同步场景居多,那么可以把QMode改成对同步更友好的模式,优先唤醒WaitSet队列
如果你的业务模型是互斥场景居多,可以把QMode,优先唤醒cxq队列的数据。
Java自己实现的,各个参数需要自己去控制
QMode 0
默认:优先唤醒同步线程,再是互斥线程。解决线程饥饿问题
ObjectMonitor.hpp
_EntryList 调度队列 ,线程唤醒需要从这个队列中做
_cxq 竞争队列,互斥排队,一个线程抢到锁,其他线程进入到这个队列中去
_WaitSet 同步队列。调用wait方法阻塞的线程队列,用来形成Java的阻塞唤醒机制,
重量级为什么要有三个队列?为什么不直接用Linux中的Mutex+Cond实现互斥与同步?
本质原因就是给程序员足够的自由。因为它本质是一个管程模型,要实现同步与互斥,让你自己通过参数的调整,对同步场景或互斥场景更加友好。。
如果你的业务模型是同步场景居多,那么可以把QMode改成对同步更友好的模式,优先唤醒WaitSet队列
如果你的业务模型是互斥场景居多,可以把QMode,优先唤醒cxq队列的数据。
Java自己实现的,各个参数需要自己去控制
QMode 0
默认:优先唤醒同步线程,再是互斥线程。解决线程饥饿问题
AQS的CLH队列其实就是EntryList
如果偏向锁恢复到无锁是锁降级那就存在降级这个概念
但是轻量级锁不i变成偏向锁,重量级锁不会变成轻量级锁
如果偏向锁恢复到无锁是锁降级那就存在降级这个概念
但是轻量级锁不i变成偏向锁,重量级锁不会变成轻量级锁
什么是线程饥饿问题?
线程进入了队列,迟迟得不到机会,
线程进入了队列,迟迟得不到机会,
wait方法是重量级锁才有的
synchronizer.cpp#wait()
直接膨胀成重量级锁,释放锁,加入队列
把当前线程包装成ObjectWaiter加入到_WaitSet队列中去
WaitSet 它是一个回环队列,尾插法,头出队
synchronizer.cpp#wait()
直接膨胀成重量级锁,释放锁,加入队列
把当前线程包装成ObjectWaiter加入到_WaitSet队列中去
WaitSet 它是一个回环队列,尾插法,头出队
封装线程节点
入队,然后调用exit
notify
ObjectMonitor:notify()
不是直接唤醒,而是加入到调度队列中,还会有三个队列中拷贝的操作
会有Policy机制去决定怎么唤醒
ObjectMonitor:notify()
不是直接唤醒,而是加入到调度队列中,还会有三个队列中拷贝的操作
会有Policy机制去决定怎么唤醒
从WaitSet的头节点开始,取出WaitSet中的节点,然后加入到_cxq的前面
1.同步线程调度机制
t2先加入WaitSet
t3后加入WaitSet
结果:t3先运行,t2后运行
原因:WaitSet不直接获得调度,默认机制,会插入到cxq队列获得调度
2.互斥线程调度机制
t3先加入cxq
t2后加入cxq
结果:t2先运行 t3后运行
原因: cxq队列默认是头插法
互斥+同步线程调度机制
入队: 顺序是反的: 先进入容器的在后面(没说一定后获得调度,因为可以改个模式参数.QMode,使之成为先入队的先获取调度)
出队:
调度
t2先加入WaitSet
t3后加入WaitSet
结果:t3先运行,t2后运行
原因:WaitSet不直接获得调度,默认机制,会插入到cxq队列获得调度
2.互斥线程调度机制
t3先加入cxq
t2后加入cxq
结果:t2先运行 t3后运行
原因: cxq队列默认是头插法
互斥+同步线程调度机制
入队: 顺序是反的: 先进入容器的在后面(没说一定后获得调度,因为可以改个模式参数.QMode,使之成为先入队的先获取调度)
出队:
调度
重量级锁入口fast_enter->slow_enter->
ObjectSynchronizer::inflate(THREAD, obj())
获得重量级锁对象ObjectMonitor对象
->enter():上锁逻辑->EnterI()
1.抢锁
自旋抢锁:弥补抢锁的缝隙,让抢锁的效率更高
加入队列
阻塞等待唤醒
ObjectSynchronizer::inflate(THREAD, obj())
获得重量级锁对象ObjectMonitor对象
->enter():上锁逻辑->EnterI()
1.抢锁
自旋抢锁:弥补抢锁的缝隙,让抢锁的效率更高
加入队列
阻塞等待唤醒
入队/出队操作:EnterI():
初始化队列、自旋。
如果一旦入队成功,那么就会直接跳出去,等待唤醒,反之,如果没有入队成功,那么还会tryLock再抢一次锁
初始化队列、自旋。
如果一旦入队成功,那么就会直接跳出去,等待唤醒,反之,如果没有入队成功,那么还会tryLock再抢一次锁
CAS抢锁,初始化队列
自适应自旋,排队之前进行自旋操作
自适应自旋
如果自旋失败,封装节点
CAS入队每失败一次,就尝试一次加锁
tryLock CAS操作
调度./ exit方法
QMode参数解释:
0(默认):什么顺序进去,就什么顺序取出来,先进入cxq的后获取调度
1: 入队顺序是反的,将cxq队列中的节点进行反转,先进入队列先唤醒
2:唤醒cxq中的第一个节点
3:把cxq容器中的数据移到EntryList的后面,顺序不变,没有做唤醒工作
4:cxq加入到EntryList的前面,不做唤醒工作
有待考究? 这个是没错的
如果3和4的情况,走到后面
如果为3,唤醒cxq的第一个线程
如果为4,EntryList的第一个线程被唤醒
QMode参数解释:
0(默认):什么顺序进去,就什么顺序取出来,先进入cxq的后获取调度
1: 入队顺序是反的,将cxq队列中的节点进行反转,先进入队列先唤醒
2:唤醒cxq中的第一个节点
3:把cxq容器中的数据移到EntryList的后面,顺序不变,没有做唤醒工作
4:cxq加入到EntryList的前面,不做唤醒工作
有待考究? 这个是没错的
如果3和4的情况,走到后面
如果为3,唤醒cxq的第一个线程
如果为4,EntryList的第一个线程被唤醒
QMode == 0
什么顺序进去,就什么顺序取出来,先进入cxq的后获取调度
如果当前调度的是最后一个线程,那么会做下面几件事儿:
1.release_store_ptr(): 释放锁
2.OrderAccess:storeload(); 立马更新到内存中去
3.判断_EntryList和_cxq队列是否为空,如果为空,那么也就不需要做唤醒动作了,直接退出
什么顺序进去,就什么顺序取出来,先进入cxq的后获取调度
如果当前调度的是最后一个线程,那么会做下面几件事儿:
1.release_store_ptr(): 释放锁
2.OrderAccess:storeload(); 立马更新到内存中去
3.判断_EntryList和_cxq队列是否为空,如果为空,那么也就不需要做唤醒动作了,直接退出
QMode == 1
将cxq队列中的节点进行翻转,先进入队列先调度唤醒
将cxq队列中的节点进行翻转,先进入队列先调度唤醒
QMode == 2
唤醒_cxq队列中第一个节点线程
唤醒_cxq队列中第一个节点线程
QMode == 3
_EntryList->_cxq
把_cxq容器中的数据移动到EntryList的后面,顺序不变,没有做唤醒动作
_EntryList->_cxq
把_cxq容器中的数据移动到EntryList的后面,顺序不变,没有做唤醒动作
QMode == 4
_cxq->_EntryList
不做唤醒动作
_cxq->_EntryList
不做唤醒动作
最后唤醒_EntryList
AQS就是六大操作
1.抢锁
自旋抢锁(可忽略)
加入队列
阻塞
2.释放锁
抢锁
出队
线程抢锁之间会存在一些缝隙,通过抢锁的方式去弥补这些缝隙
几大机制可能会略有区别
state AQS中用state标识锁状态位
ObjectMonitor使用_owner判断当前哪个线程拿到了锁
currentThread:
1.抢锁
自旋抢锁(可忽略)
加入队列
阻塞
2.释放锁
抢锁
出队
线程抢锁之间会存在一些缝隙,通过抢锁的方式去弥补这些缝隙
几大机制可能会略有区别
state AQS中用state标识锁状态位
ObjectMonitor使用_owner判断当前哪个线程拿到了锁
currentThread:
7.手写synchronized
markword的内存布局
重量级锁
重量级锁其实底层也是AQS.本质上synchronized也是一种AQS
syncrhonized可以做互斥也可以做同步
线程的阻塞与唤醒
mutex 互斥锁 Linux
cond 条件锁
ParkEvent其实是对这两个变量抽象化的一个实体
线程的阻塞与唤醒
mutex 互斥锁 Linux
cond 条件锁
ParkEvent其实是对这两个变量抽象化的一个实体
内联汇编语法
__asm__(汇编语句模板 : 输出部分 : 输入部分: 破坏描述部分)
eax寄存器存放的是函数返回值
cmpxchg(long exchange_value, volatile long * dest, long compare_value)
不管相不相等,都是返回dest
区别,相等,返回旧值
不相等,返回dest
在64位机器下long和void*都是占用8B
__asm__(汇编语句模板 : 输出部分 : 输入部分: 破坏描述部分)
eax寄存器存放的是函数返回值
cmpxchg(long exchange_value, volatile long * dest, long compare_value)
不管相不相等,都是返回dest
区别,相等,返回旧值
不相等,返回dest
在64位机器下long和void*都是占用8B
重量级锁实现理论
1.锁状态位 state / 持有锁的线程owner, 支持重入recursion, 队列机制 ObjectWaiter
2.抢锁
3.释放锁
4.出队
5.入队
6.阻塞
7.唤醒
synchronized重量级锁底层是mutex+cond机制实现的,其实mutex本身已经支持了互斥与同步,为什么还要自己实现?
1.它要定制化,唤醒机制,否则有的线程永远得不到唤醒,exit()决定线程的唤醒机制
2.调试方便(出现问题不容易调试操作系统)
3.只用了mutex的上锁机制,其他机制没用
1.锁状态位 state / 持有锁的线程owner, 支持重入recursion, 队列机制 ObjectWaiter
2.抢锁
3.释放锁
4.出队
5.入队
6.阻塞
7.唤醒
synchronized重量级锁底层是mutex+cond机制实现的,其实mutex本身已经支持了互斥与同步,为什么还要自己实现?
1.它要定制化,唤醒机制,否则有的线程永远得不到唤醒,exit()决定线程的唤醒机制
2.调试方便(出现问题不容易调试操作系统)
3.只用了mutex的上锁机制,其他机制没用
synchronized重量级锁是没有公平和非公平锁之分的,AQS中是有的
ObjectMonitor中是没有state锁状态位的,它是把state和owner合在一起用的
代码要有对称性,上锁/解锁、阻塞/唤醒、申请内存/释放内存
VMOperation可以理解为一个异步任务池,它希望是VMThread来执行,而不是JavaThread
ObjectMonitor中是没有state锁状态位的,它是把state和owner合在一起用的
代码要有对称性,上锁/解锁、阻塞/唤醒、申请内存/释放内存
VMOperation可以理解为一个异步任务池,它希望是VMThread来执行,而不是JavaThread
enter()
// 1.CAS抢锁
// 2.重入
// 3.加入队列
// 4.线程阻塞
exit()
// 1.处理重入
// 2.CAS释放锁
// 3. 唤醒队列中的节点
// 1.CAS抢锁
// 2.重入
// 3.加入队列
// 4.线程阻塞
exit()
// 1.处理重入
// 2.CAS释放锁
// 3. 唤醒队列中的节点
轻量级锁 偏向锁都是在先有了重量级锁之后才被开发出来的
轻量级锁
在synchronized中,轻量级锁是最简单,偏向锁是最复杂的。偏向锁解决的是单个线程的线程同步问题,轻量级锁的CAS成本过高
锁实体BasicLock,记录在虚拟机栈中。
Mark Word的实现细节
InstanceOop对象 oop模型 Java对象--C++对象
MarkOopDesc对象头 锁存储的位置。 特点:没有任何属性,
32bit: 4B
64bit: 8B
一个空对象就是8B,
MarkOopDesc* mark = new MarkOopDesc;
INFO_PRINT("%X\n", mark); 打印对象的地址
// 创建了对象,返回对象中的值
markOop o1 = markOop(1);
INFO_PRINT("%X\n", o1); 打印的是1
不要把MarkOop对象看成一个对象,而是一个值
Mark Word的实现细节
InstanceOop对象 oop模型 Java对象--C++对象
MarkOopDesc对象头 锁存储的位置。 特点:没有任何属性,
32bit: 4B
64bit: 8B
一个空对象就是8B,
MarkOopDesc* mark = new MarkOopDesc;
INFO_PRINT("%X\n", mark); 打印对象的地址
// 创建了对象,返回对象中的值
markOop o1 = markOop(1);
INFO_PRINT("%X\n", o1); 打印的是1
不要把MarkOop对象看成一个对象,而是一个值
轻量级锁的重入其实是不需要处理的,没有重入这一说,
它不像重量级锁mutex_lock进行了两次,mutex_unlock也需要两次
HotSpot中在判断重入的时候还额外判断了下锁地址是否在线程栈里,其实不如直接判断持有锁的线程是否
重入的逻辑要求不是同一把锁对象,否则会报错
fast_enter()
must not re-lock the same lock,HotSpot调试过程中很少逻辑会走到这里
它不像重量级锁mutex_lock进行了两次,mutex_unlock也需要两次
HotSpot中在判断重入的时候还额外判断了下锁地址是否在线程栈里,其实不如直接判断持有锁的线程是否
重入的逻辑要求不是同一把锁对象,否则会报错
fast_enter()
must not re-lock the same lock,HotSpot调试过程中很少逻辑会走到这里
手写实现轻量级锁膨胀成重量级锁
t1轻量级锁,t2重量级锁,如何处理t1?
解决两种方案:
1.t2膨胀成重量级锁,t2抢锁成功,t1应该放入到队列中,以实现线程互斥的效果
2.t1轻量级锁 和t2重量级锁同时运行
抢到重量级锁的线程把持有轻量级锁的线程踢出去走重量级锁?
1.找到t1的线程对象,让t1进入cxq队列,让t1阻塞等待唤醒
分为两步
1.inflate()获取重量级锁对象ObjectMonitor
a.轻量级锁膨胀
b.正在膨胀
c.已经膨胀
d.第一次膨胀
e.已经是重量级锁
2.exit()
a.释放锁
b,唤醒
c.出队
t1轻量级锁,t2重量级锁,如何处理t1?
解决两种方案:
1.t2膨胀成重量级锁,t2抢锁成功,t1应该放入到队列中,以实现线程互斥的效果
2.t1轻量级锁 和t2重量级锁同时运行
抢到重量级锁的线程把持有轻量级锁的线程踢出去走重量级锁?
1.找到t1的线程对象,让t1进入cxq队列,让t1阻塞等待唤醒
分为两步
1.inflate()获取重量级锁对象ObjectMonitor
a.轻量级锁膨胀
b.正在膨胀
c.已经膨胀
d.第一次膨胀
e.已经是重量级锁
2.exit()
a.释放锁
b,唤醒
c.出队
1.如果marword对象头中已经膨胀成重量级锁了,直接返回
2.如果说markword对象头正在膨胀,那么会进行park阻塞,等待唤醒,并非自旋一直获取重量级锁
3.如果当前markword中是轻量级锁,会构造ObjectMonitor
4.如果是无锁,膨胀成重量级锁,为了代码上的完整性,但是极少情况会触发到这里,更多的是从商业的角度触发
上轻量级锁必须要是在恢复成无锁的情况才能膨胀,偏向锁需要在STW进行安全点撤销,要把无锁的markword设置到displaced_header中
偏向锁
它的目的主要是为了节省CAS的开销,认为其开销很大。
如果没有偏向锁,只有轻量级锁,CAS抢锁失败,判断是否重入,不是就会发生锁膨胀。
如果说一个线程,只用一个对象这样的场景,在JVM中,这样的场景居多,相当于每用一次都要CAS.
HotSpot的开发人员认为开销很大,
在偏向锁中,如果这个对象是匿名可偏向,当前线程加锁,只需要判断当前持有锁是否等于当前线程即可,无需CAS
owner == current_threadId
如果没有偏向锁,只有轻量级锁,CAS抢锁失败,判断是否重入,不是就会发生锁膨胀。
如果说一个线程,只用一个对象这样的场景,在JVM中,这样的场景居多,相当于每用一次都要CAS.
HotSpot的开发人员认为开销很大,
在偏向锁中,如果这个对象是匿名可偏向,当前线程加锁,只需要判断当前持有锁是否等于当前线程即可,无需CAS
owner == current_threadId
偏向锁的使用场景
1.加载一个类
偏向锁的逻辑
1.模板解释器 lock_object biased_locking_enter
2.fast_enter
1.加载一个类
偏向锁的逻辑
1.模板解释器 lock_object biased_locking_enter
2.fast_enter
偏向锁哪些地方用到了?
它最开始是为了节省CAS的开销,但是随着版本的迭代,其他地方的升级,偏向锁也要兼容,牵扯的地方很多包括类加载的地方。它本身的开销已经超过了CAS的开销
类加载的时候,初始化会加锁,会使用ObjectLocker fast_enter上锁
它最开始是为了节省CAS的开销,但是随着版本的迭代,其他地方的升级,偏向锁也要兼容,牵扯的地方很多包括类加载的地方。它本身的开销已经超过了CAS的开销
类加载的时候,初始化会加锁,会使用ObjectLocker fast_enter上锁
实现偏向锁
锁对象oop
对象头 markOop
1.上锁
CAS
2.重入
不需要考虑重入问题,不像mutex_lock,mutex_unlock成对的出现,它是借助了锁对象
3.解锁
撤销逻辑
膨胀成轻量级锁,需要恢复到无锁,在JVM里面是通过VMThread实现的
4.批量
在HotSpot中,批量的操作是通过VMThread异步执行任务池实现的,手写版本暂时没有实现
锁对象oop
对象头 markOop
1.上锁
CAS
2.重入
不需要考虑重入问题,不像mutex_lock,mutex_unlock成对的出现,它是借助了锁对象
3.解锁
撤销逻辑
膨胀成轻量级锁,需要恢复到无锁,在JVM里面是通过VMThread实现的
4.批量
在HotSpot中,批量的操作是通过VMThread异步执行任务池实现的,手写版本暂时没有实现
调试HotSpot源码技巧
需要使用ResourceMark,否则会内存泄漏
找到自己调试的类,
需要使用ResourceMark,否则会内存泄漏
找到自己调试的类,
1.抢锁膨胀成轻量级锁,拿到偏向锁的线程不会阻塞,而是会继续执行,在HotSpot中,是要线程互斥的,同一时刻只能有一个线程执行
2.没有实现批量撤销 批量重偏向的功能
2.没有实现批量撤销 批量重偏向的功能
场景模拟
t1
拿到了偏向锁
t2
拿到了轻量级锁,应该对t1做处理,使其阻塞
t3
t2拿到了轻量级锁,t3只能去膨胀成重量级锁
t1
拿到了偏向锁
t2
拿到了轻量级锁,应该对t1做处理,使其阻塞
t3
t2拿到了轻量级锁,t3只能去膨胀成重量级锁
0 条评论
下一页