Java锁相关系列—synchronized与锁升级过程详解

Java中各种锁的概念

自旋锁: 是指一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断判断锁是否能够被成功获取,直到获取到锁才会退出循环。

乐观锁假设没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读取最新数据,修改后重试修改。乐观锁的基础——CAS,乐观锁回滚重试,乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。
悲观锁假设会发生并发冲突,同步所有对数据的相关操作,从读取数据时就开始加锁。如synchronized,悲观锁阻塞事务,如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

独享锁(写): 给资源加上写锁,线程可以修改资源,其他线程不能再加锁;(单写)。
共享锁(读): 给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁(多读)。

可重入锁、不可重入锁:可重入锁即允许同一个线程多次获取同一把锁,如synchronized 和 ReentrantLock

公平锁、非公平锁: 如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平。显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。如ReentrantLock中,可以指定该锁是否为公平锁,默认是非公平锁。一般情况下,非公平锁的吞吐量比公平锁的大,如果没有特殊要求,优先使用非公平锁。下图为ReentrantLock的源码构造函数,用于指定是否为公平锁
在这里插入图片描述

可中断锁 : 可以响应中断的锁,Java并没有提供任何直接中断某线程的方法,只提供了中断机制。何谓“中断机制”?线程A向线程B发出
“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理。这好比是父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁
(参考文章: https://zhuanlan.zhihu.com/p/71156910)

JAVA 锁消除和锁粗化

锁消除

锁消除 : 锁消除是发生在编译器级别的一种锁优化方式。在编写代码时,有些时候不需要进行加锁操作,但是却执行了加锁操作,典型的例子就是 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 对象在内存中的布局

如下图展示,不多作阐述
对象在内存中的布局

对象头部剖析

什么是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位。

对象头部结构

synchronized的锁升级过程

轻量级锁抢锁过程

首先知道悉下图,以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直接锁升级,升级为重量级锁。过程如下几幅图:

CAS执行前:
在这里插入图片描述
CAS执行后:
在这里插入图片描述

重量级锁

当轻量级锁升级为重量级锁时,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对偏向锁的描述:
在这里插入图片描述

锁的升级过程

锁的升级过程

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值