【Synchronized我可以讲半小时】

2022-09-29 10:36:53 浏览数 (1)

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。实例数据:存放类的属性数据信息,包括父类的属性信息;对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。所以虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Monitor监视器对象就是存在于每个Java对象的对象头Mark Word中,也就是存储的指针的指向,Synchronized锁便是通过这种方式获取锁的。Monitor可以把它理解为一个同步工具,它通常被描述为一个对象,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象就带了一把看不见的锁,可以叫做内部锁或者Monitor锁。Synchronized在JVM里的实现基于进入和退出Monitor对象来实现方法同步和代码块同步的。

对象头的最后两位存储了锁的标志位,01是初始状态,没加锁状态,对象头里存储的是对象本身的哈希码。01是偏向锁状态,存储的是当前占用对象的线程ID。00是轻量级锁状态,存储指向线程栈中锁记录的指针。10是重量级锁状态,存储的技术就是重量级锁的指针了。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。JDK5引进的CAS自旋,JDK6开始又引入了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略,这些优化使得Synchronized性能极大提高。 锁是升级的过程是怎样的?MarkWord是怎么变化的? 1.无锁状态:首先,当对象没有被锁时,MarkWord记录着对象的哈希码,这个时候锁标志为为01,是否偏向为0。 2.偏向锁状态:现在几乎所有的锁都是可重入的,也就是说已经获得锁的线程,可以多次锁住/解锁监视对象,每次加锁/解锁都会涉及到一些CAS操作,CAS操作会延迟本地调用,为什么这么说呢?这要从SMP(对称多处理器)架构说起,所有的CPU会共享一条系统总线(BUS),靠此总线连接主内存,每个核都有自己的一级缓存,每个核相对于BUS对称分布。举个例子,我电脑是六核的,假设一个核是Core1,一个核是Core2,这二个核可能会同时把主存中某个位置的值Load到自己的一级缓存中。当Core1在自己的L1 Cache中修改这个位置的值时,会通过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现自己L1 Cache中的值失效,也就是所谓的Cache命中缺失,一旦发现失效就会通过总线从内存中加载该地址最新的值,大家通过总线的来回通信叫做“Cache一致性流量”。如果Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,称为“Cache一致性”,从这个层面来说,锁设计的终极目标便是减少Cache一致性流量。而CAS恰好会导致Cache一致性流量,如果有很多线程都共享同一个对象,当某个Core CAS成功时必然会引起总线风暴,这就是所谓的本地延迟,本质上偏向锁就是为了消除CAS,降低Cache一致性流量。

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID就可以了。具体的流程是这样的,第一步,检测Mark Word是否为可偏向状态,就是是否为偏向锁1,锁标识位为01。第二步,如果是可偏向状态,测试线程ID是不是当前线程ID。如果是,就直接执行同步代码块。第三步,如果测试线程ID不是当前线程ID,就通过CAS操作竞争锁,竞争成功,就把Mark Word的线程ID替换为当前线程ID。第四步,如果CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程,继续往下执行同步代码块。安全点是jvm为了保证在垃圾回收的过程中引用关系不会发生变化,设置的安全状态,在这个状态上会暂停所有线程工作。一般有循环的末尾,方法临返回前,调用方法的call指令后,可能抛异常的位置,这些位置都可以算是安全点。 3.轻量级锁状态:有多个线程竞争同一个锁,那么将在锁升级为轻量级锁。升级过程是,在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Mark Word的拷贝,拷贝对象头中的Mark Word复制到锁记录中,为什么升级为轻量锁时要把对象头里的Mark Word复制到线程栈的锁记录中呢?因为在申请对象锁时,需要以该值作为CAS的比较条件,同时在升级到重量级锁的时候,能通过这个比较判定是否在持有锁的过程中此锁被其他线程申请过,如果被其他线程申请了,则在释放锁的时候要唤醒被挂起的线程。拷贝成功后,虚拟机将使用CAS操作把对象Mark Word中锁记录空间更新为指向当前线程锁记录空间的指针,然后把锁记录空间里的owner指针指向object mark word,如果这个更新动作成功了,那么当前线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。如果这个更新操作失败了,虚拟机首先会检查对象Mark Word中的Lock Word是否指向当前线程的栈帧,如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,进入自旋,若自旋结束时仍未获得锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,当前线程以及后面等待锁的线程也要进入阻塞状态。 4.自旋锁:自旋锁不是一种锁状态,而是一种策略。线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。所以引入自旋锁,当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间。自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好。自旋的次数必须要有一个限度,如果自旋超过了定义的限度仍然没有获取到锁,就应该被挂起。但是这个限度不能固定,程序锁的状况是不可预估的,所以JDK1.6引入自适应的自旋锁,线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少,甚至省略掉自旋过程,以免浪费处理器资源。自旋一段时间成功获得锁,就表示在轻量级锁状态,否则轻量级锁膨胀为重量级锁。 5.重量级锁状态:将锁标志为置为10,将MarkWord中指针指向重量级的monitor,阻塞所有没有获取到锁的线程。Synchronized是通过对象内部的监视器锁(Monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的,操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要比较长的时间,这就是为什么Synchronized效率低的原因,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。

整个图片,歇歇眼,文章大多不换行,排版基本都是一块的,三千五百字,口速快的话,半个小时差不多可以讲完,这篇博文主要是针对面试口述的,备战面试。

0 人点赞