自旋锁: 是指一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断判断锁是否能够被成功获取,直到获取到锁才会退出循环。
乐观锁 :
假设没有冲突
,在修改数据时如果发现数据和之前获取的不一致,则读取最新数据,修改后重试修改。乐观锁的基础——CAS,乐观锁回滚重试,乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。
悲观锁 :假设会发生并发冲突
,同步所有对数据的相关操作,从读取数据时就开始加锁。如synchronized,悲观锁阻塞事务,如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
独享锁(写): 给资源加上写锁,线程可以修改资源,其他线程不能再加锁;(单写)。
共享锁(读): 给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁(多读)。
可重入锁、不可重入锁:可重入锁即允许
同一个线程多次获取同一把锁
,如synchronized 和 ReentrantLock
公平锁、非公平锁: 如果多个线程申请一把公平锁,那么当锁释放的时候,
先申请的先得到
,非常公平。显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。如ReentrantLock中,可以指定该锁是否为公平锁,默认是非公平锁。一般情况下,非公平锁的吞吐量比公平锁的大,如果没有特殊要求,优先使用非公平锁。下图为ReentrantLock的源码构造函数,用于指定是否为公平锁
可中断锁 : 可以响应中断的锁,Java并没有提供任何直接中断某线程的方法,只提供了
中断机制
。何谓“中断机制”?线程A向线程B发出
“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理。这好比是父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁
(参考文章: https://zhuanlan.zhihu.com/p/71156910)
。
锁消除 : 锁消除是发生在编译器级别的一种锁优化方式。在编写代码时,有些时候不需要进行加锁操作,但是却执行了加锁操作,典型的例子就是 StringBuffer类中的append操作,其中用了synchronized,append方法不停的调用,不停的加锁解锁,
达到一定次数
后,触发JIT编译
,但是synchronized并没有发生抢锁,所以进行了JIT优化操作,即是锁消除。
锁消除是发生在
单线程
中,将锁消除,前提是java必须运行在server模式(server模式会比client模式作更多的优化)
同时必须开启逃逸分析:-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
其中-XX:+DoEscapeAnalysis表示开启逃逸分析,-XX:+EliminateLocks表示锁消除
。
逃逸分析:比如上面的代码,它要看StringBuffer是否可能逃出它的作用域?如果将StringBuffer作为方法的返回值进行返回,那么它在方法外部可能被当作一个全局对象使用,就有可能发生线程安全问题,这时就可以说StringBuffer这个对象发生逃逸了,因而不应将append操作的锁消除,但我们上面的代码没有发生锁逃逸,锁消除就可以带来一定的性能提升。(转载自 java锁优化的5个方法 )
/**
* @author 潇兮
* @date 2019/9/18 23:33
* 类说明: 锁消除
**/
public class LockElimination {
public void test(Object arg){
// StringBuilder线程不安全,StringBuffer用了synchronized,是线程安全的
// 锁消除的本质是一种jit 优化, 消除了锁,
/**
* append方法不停的调用,不停的加锁解锁,达到一定次数后,触发JIT编译,
* 但是synchronized并没有发生抢锁,所以进行了JIT优化操作,即是锁消除
*/
StringBuffer stringBuffer=new StringBuffer();
stringBuffer.append("a");
stringBuffer.append("b");
stringBuffer.append("c");
stringBuffer.append("d");
stringBuffer.append("e");
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000000; i++) {
new LockElimination().test("123");
}
}
}
代码中test方法中的局部对象stringBuffer,就只在该方法内的作用域有效,不同线程同时调用test()方法时,都会创建不同的stringBuffer对象,因此此时的append操作若是使用同步操作,就是白白浪费的系统资源。
锁粗化 : JIT编译优化。锁粗化即是在某些情况下,希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
//需要锁粗化的代码
public void method1() {
synchronized (lock) {
//do some thing
}
//这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
//注意:这样做是有前提的,就是中间不需要同步的代码能够很快速地完成(非耗时操作),如果不需要同步的代码需要花很长时间(耗时操作),就会导致同步块的执行需要花费很长的时间,这样做也就不合理了。
synchronized (lock) {
//do other thing
}
}
//将上述method1优化为如下method2方法
public void method2() {
synchronized (lock) {
//do some thing
//做其它不需要同步但能很快执行完的工作
//do other thing
}
}
//需要锁粗化的代码
public void method3() {
for(int i=0;i<size;i++){
synchronized(lock){
}
}
}
//将上述method3优化位如下method4方法
public void method4() {
synchronized(lock){
for(int i=0;i<size;i++){
}
}
}
什么是java对象的指针压缩?
1.jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩
2.jvm配置参数:UseCompressedOops,compressed–压缩、oop–对象指针
3.启用指针压缩:-XX:+UseCompressedOops,禁止指针压缩:-XX:-UseCompressedOops
————————————————
原文链接:JVM-对象的指针压缩
对象头包含如下结构,包括
markword
和类型指针
,如果是数组,还包括数组长度
;其中对象的加锁解锁的状态
就是记录在MarkWord
中。
mark word
存储了同步状态、标识、hashcode、GC状态等等。
klass pointer
(class Meta address)存储对象的类型指针,该指针指向它的类元数据
值得注意的是,如果应用的对象过多,使用64位的指针将浪费大量内存。64位的JVM比32位的JVM多耗费50%的内存。
我们现在使用的64位 JVM会默认使用选项 +UseCompressedOops 开启指针压缩,将指针压缩至32位。
首先知道悉下图,以64位操作系统为例
|--------------------------------------------------------------------------------------------------------------|
| Object Header (128 bits) |
|--------------------------------------------------------------------------------------------------------------|
| Mark Word (64 bits) | Klass Word (64 bits) |
|--------------------------------------------------------------------------------------------------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 无锁
|----------------------------------------------------------------------|--------|------------------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 偏向锁
|----------------------------------------------------------------------|--------|------------------------------|
| ptr_to_lock_record:62 | lock:2 | OOP to metadata object | 轻量锁
|----------------------------------------------------------------------|--------|------------------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | 重量锁
|----------------------------------------------------------------------|--------|------------------------------|
| | lock:2 | OOP to metadata object | GC
|--------------------------------------------------------------------------------------------------------------|
简单介绍一下各部分的含义
lock
: 锁状态标记位,该标记的值不同,整个mark word表示的含义不同。
biased_lock
:偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
age
:Java GC标记位对象年龄。
identity_hashcode
:对象标识Hash码,采用延迟加载技术。当对象使用HashCode()计算后,并会将结果写到该对象头中。当对象被锁定时,该值会移动到线程Monitor中。
thread
:持有偏向锁的线程ID和其他信息。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
epoch
:偏向时间戳。
ptr_to_lock_record
:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor
:指向线程Monitor的指针。
1.从未锁定锁状态开始,首先Mark Word中存储了HashCode
、垃圾分代回收的年龄age
、偏向锁标记biased_lock
0、锁状态标记位lock
为01,此时锁的状态为 无锁(即是未加锁状态)
2.此时执行synchronized(禁用偏向锁时,先理解轻量锁
),此时有线程T1 和线程 T2,开始抢锁,当T1 的方法栈帧去抢锁时,首先在当前抢锁的方法中的栈帧开辟一段内存区域——Lock Record
,会将当前Mark Word
的内存区域(HashCode|Age|0)copy到Lock Record中,线程2同上步骤。copy完成后,开始抢锁,执行CAS
操作(手撕CAS),在CAS过程中,旧值为上一步中copy过来的值(HashCode|Age|0),新值为轻量级锁(Lock Record Address|00)。假设T1抢锁成功,在栈帧中会有一块owner
内存区域,用于指向MarkWord做标记。T2抢锁失败,自旋达到一定次数后进行锁升级
,进入重量级锁
。PS:如果此时有个T3过来抢锁,因为T1已经将MarkWork中锁的未锁定状态被修改为轻量级锁了,T3会copy失败,此时T3直接锁升级,升级为重量级锁
。过程如下几幅图:
当轻量级锁升级为重量级锁时,Mark Word中Lock Record Address变为Monitor Address
,锁状态标记位lock变为10,此时线程T2进入了entryList
(锁池,先进先出的队列)中,而对象监视器Monitor 中owner
指向T1线程,假设T1此时调用了wait方法,线程释放锁,owner变为null,T1此时进入waitSet
(等待池),T2进入waiting状态。假设此时(T2被唤醒的瞬间)外来一个线程T4,因为owner为无锁状态,会与T2进行抢锁(synchronized为不公平锁,先抢锁,抢不到进入锁池
)若T1此时调用notiy
方法唤醒线程,如果此时owner被T2占用了,会进入entryList排队,T1线程状态为blocked状态。此时T2线程占用了owner,此时代码块执行完毕,解锁也就是执行monitorExit字节码指令,锁就会退出,owner变为null,其他线程被唤醒。PS:看着文字配上脑图走流程理解最佳
在JDK6以后,默认开启了偏向锁这个优化,通过JVM参数 -XX: -UseBiasedLocking 来禁用偏向锁。若偏向锁开启,只有一个线程抢锁,可获取到偏向(可以查看此文内的 偏向锁 理解)。下图为Mark Work对偏向锁的描述: