Synchronized原理以及Java锁膨胀

Synchronized原理以及Java锁膨胀

 

从语法上讲,Synchronized可以把任何一个非null对象作为"锁",在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)。
1、Synchronized属性:原子性,可见性,有序性
(1)原子性:确保线程互斥的访问同步代码;
为什么volatile已经保证了变量的可见性,synchronized依然保证变量的可见性:
(2)可见性:保证共享变量的同步能够及时可见,通过JMM模型:“对一个共享变量unlock(解锁)之前一定要先同步到主内存中;如果要是对一个共享变量进行lock(锁定)的之前一定要清除工作内存中的值,然后通过主内存中的值加载到工作内存中”;
(3)有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;
2、同步的原理:
(1)方法同步:JVM从方法常量池的表(method_info Structure)中,获取ACC_SYNCHRONIZED访问标志来区分一个方法是否是同步方法,如果设置了该标识。当方法被调用时,执行线程将先持有monitor,然后执行方法,待方法执行完毕后释放monitor;
public synchronized void f(){ //这个是同步方法 System.out.println("Hello world"); }
字节码文件:

(2)代码块同步:代码块的同步利用两个字节码文件:monitorenter和monitorexit这两个字节码指令。它们分别位于代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程获取monitor的持有权,就把锁的计数器+1;当jvm执行到monitorexit指令时,锁计数器-1;当计数器为0的时候,锁释放;
public void g(){
//这个是同步代码块 synchronized (this){ System.out.println("Hello world"); } }

3、由Java对象的内部存储引发的synchronized概念:
在JVM中Java对象主要由3部分组成:对象头,实例数据,对齐填充;

  1. 实例数据:存放类的属性数据信息,包括父类的属性信息;
  2. 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
  3. 对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

 

4、锁消除:
为了保证数据的完整性,在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。
锁消除的依据是逃逸分析的数据支持
如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于程序员来说这还不清楚么?在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?虽然没有显示使用锁,但是在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:
public void vectorTest(){ Vector<String> vector = new Vector<String>(); for(int i = 0 ; i < 10 ; i++){ vector.add(i + ""); } System.out.println(vector); }
在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。

5、锁粗化:
在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是 为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁
如上面实例:
vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
6、偏向锁VS无锁VS轻量级锁VS重量级锁(Java锁的膨胀过程和优化):
(1)markword的存储结构:
在32虚拟机下:

在64位虚拟机下:

后两位的锁标志位:无锁:01;偏向锁:01;

Lock Record是线程私有的数据结构,每一个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表。每一个被锁住的对象Mark Word都会和一个Lock Record关联(对象头的MarkWord中的Lock Word指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放拥有该锁的线程的唯一标识(或者object mark word),表示该锁被这个线程占用。如下图所示为Lock Record的内部结构:

Lock Record

描述

Owner

初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;

EntryQ

关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程;

RcThis

表示blocked或waiting在该monitor record上的所有线程的个数;

Nest

用来实现 重入锁的计数;

HashCode

保存从对象头拷贝过来的HashCode值(可能还包含GC age)。

Candidate

用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

(2)偏向锁:HotSpot人员发现大多数情况下虽然加了锁,但是同一线程却总是能反复获取到锁:当一个线程访问同步块且获取到锁的时候,会在对象头和栈帧记录存储取得偏向锁的线程ID,下一次有线程尝试去获取锁的时候,首先检查这个对象头MarkWord是不是存储着这个线程的ID,如果是,那么直接进去不需要任何操作。

(3)偏向锁存在的意义(即偏向锁解决的问题):
SMP(对称多处理器)架构:
其意思就是CPU会通过一条消息总线(BUS),靠此主线来连接各个内核之间的通信关系:

结构如下:假如为6核CPU,每个core(内核)前都会有一个cache,例如,Core1 和 Core2都会通过总线(Bus)来加载数据到cache中,之后同步到core中,但是当Core1 修改L1 Cache这个位置的数据时,使Core 2中的L1 Cache中的数据失效,而Core2一旦发现自己L1 Cache中的值失效(称为Cache命中缺失)则会通过总线从内存中加载该地址最新的值,大家通过总线的来回通信称为“Cache一致性流量”;如果Cache一致性流量过大,就会造成总线成为瓶颈,而CAS操作通过这种不断比对的过程,恰好会导致Cache一致性流量过大,从而引起“总线风暴”,这就是所谓的本地延迟,本质上偏向锁就是为了消除CAS,降低Cache的一致性流量;

当一个线程访问同步块并获取锁(用synchronized锁定的任何对象,方法都被成为 “锁” )时,会将该线程的ID存储到对象头和栈帧的锁记录里,以后该线程进入和退出同步块时不需要进行CAS操作,执行的过程如下:
(1)检测Mark Word中是否有偏向锁1,锁标志位01;
(2)如果有则检测线程ID是否是当前线程ID,
1)如果是执行同步代码块;
2)如果不是,则通过CAS操作竞争锁,竞争成功,则将该线程ID替换原有锁记录里的线程ID;
3)如果竞争锁失败(说明原有线程依然持有锁),偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
偏向锁的释放也叫锁撤销,步骤如下:
1、暂停拥有偏向锁的线程;
2、判断锁定对象是否拥有锁,否,则恢复到无锁状态(01),是,则挂起持有锁的当前线程,并将指向当前线程锁记录的地址指针存放到对象头Mark Word中,升级为轻量锁(00),然后恢复持有锁的当前线程,进入到轻量级锁的竞争中;

线程安全(中)--彻底搞懂synchronized(从偏向锁到重量级锁)

(4)轻量级锁:当关闭偏向锁或者存在多个线程竞争就会导致偏向锁升级为轻量级锁;
获取轻量级锁的步骤如下:
1、当线程进入到同步块时,如果同步对象锁状态为无锁状态(偏向锁:0,锁的标志位:01),虚拟机会首先在当前线程的栈帧中,建立一个LockRecord锁的空间,用于存储Mark Word中的拷贝,此时线程堆栈与对象头的状态如下图所示:

2、将Mark Word中的数据拷贝到锁记录(Lock Record)中;
3、拷贝成功以后,虚拟机将会使用CAS操作将Mark Word中的Lock Word指针指向此时Lock Record中地址,并将Lock Record中的Owner指针指向Object Mark Word:
(1)如果更新成功:说明当前线程获取到了轻量级锁,同时JVM会将该对象的Mark Word中的锁标志位更新为“00”;
(2)如果更新失败:JVM会检查该对象的Mark Word中的Lock Record指针是否指向Lock Record,如果是,则说明持有了锁(可以继续执行同步代码块),如果不是,说明有多个线程竞争锁,轻量级锁上升为重量级锁,锁的标志位为“10”;

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:
1、通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word;
2、如果替换成功,整个同步过程就完成了,恢复到无锁状态(01);
3、如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程;

(5)重量级锁:
Synchronized是通过对象内部的一个叫做 监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值