Java线程&JVM问答系列(1)——深度理解锁

前言

分布式底层应用永远离不开2个话题,线程,jvm关联起来的cpu,内存,寄存器,OS等,一切问题的产生都能从这里找出根源,因此无论你是高级专家,还是初级程序员,这2个问题务必精深

在我从业的生涯中,其中有关于深层次的,特别是牵扯到OS底层面的东西,很少涉及,但对于一个精益求精的优秀程序员来说,这都是基本功,大型企业,特别是明星企业,对专家的要求非常严格,至少基本的算法,底层资源知识和经验层面要精深

反过来试想下,如果一个企业要做分布式应用,上线后遇到的各种问题,比如OOM,什么情况会发生?为什么会发生?发生后怎么做?下次有没有规避办法? 难道遇到这些问题后,都采用粗暴办法?应用重启?事后解决?度娘&google? 你要知道,互联网之所以互联网,是因为必须做到实时,平滑的处理,在这方面阿里和阿里云的确是行业的标杆,那么阿里为什么能做到标杆呢?我虽然没有实力进入该企业,但至少间接的了解到他们一定是做到了流程标准严格,资源实时监控,各种未知情况提前布控,后备人力资源实时保障,快速响应;具体的事件大家可以去搜下

    要做到行业标杆,靠的是阿里各种不同的专才人员,经验丰厚的人才的创新和勤劳。因此优秀的程序员,是生命不止,折腾不止,时间不停,竞争不停

  这次准备将线程与JVM这2个问题关联起来,深度进行重新梳理,重新折腾,重新试错!

  这次问答系列不是简单的知识点的论证,是问题重现深度挖掘,深度分析,深度总结,具体方式what,why,how呈现出来

  比如:多线程,为什么会有多线程?多线程应用场景有哪些?应用多线程会发生什么问题?如何处理?如何规避?为什么会有锁机制出现?锁分哪些?锁产生什么不良后果?有哪些优化方式,若何规避,如果没有锁会发生什么?

好了,我们先从锁开始

 jdk发展至今,网络应用世界最广泛,因此从开始的同步锁发展到后来的各种锁,从宏观来说分为悲观锁与乐观锁,我们常常什么线程安全,什么java那个类线程安全,其实线程安全就是说明在多线程环境下资源抢占能否保证运算结果是正确的,因此jvm和java提供了锁的概念

什么是乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

什么是悲观锁

悲观锁是就是悲观思想,即认为写多读少,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock

按锁等级不同,分为 1. 偏向锁 、2. 轻量级锁、3. 重量级锁

具体查看本人之前的(JVM内存机制与JAVA并发详解 https://blog.csdn.net/luozhonghua2014/article/details/79987226)和(Java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁 )

一旦设计多线程应用,必然会有锁,锁什么呢,锁共享内存变量或实例变量,换句话说,没有多线程就没有锁

那么为什么要“锁”?

Java线程&JVM问答系列(1)——深度理解锁_第1张图片

从这个图工作原理  (每个java线程都有一份自己的工作内存,线程访问变量的时候,不能直接访问主内存中的变量,而是先把主内存的变量复制到自己的工作内存,然后操作自己工作内存里的变量,最后再同步给主内存
同步(synchronized)就是:在多个线程并发访问共享数据的时候,保证共享数据在同一个时刻只被一个线程使用)    我们可以看出,如果工作内存想利用主内存变量,如果没有锁,竞争后必然出现意外结果 

我们看看锁的宏观概念和锁等级有什么关联呢?

宏观上,我们说悲观锁,乐观锁,锁等级我们又分偏向锁,轻量锁,重量锁,其实可以归一的,像偏向锁,轻量锁就属于乐观锁,重量锁就是悲观锁

我们再看看对应JAVA语言提供了哪些锁? 如何加锁,如何解锁?内部机制是什么?

首先我们看看HotSpot虚拟机JAVA对象内存结构,对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

Java线程&JVM问答系列(1)——深度理解锁_第2张图片

占用空间

Java线程&JVM问答系列(1)——深度理解锁_第3张图片

HotSpot虚拟机的对象头 
markword 
第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志线程持有的锁偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”。
klass 
对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.
数组长度(只有数组对象有) 
如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.

  • 详细了解(java对象结构  https://blog.csdn.net/zqz_zqz/article/details/70246212,https://www.cnblogs.com/duanxz/p/4967042.html)

上面标红之处就是告诉我们的各种锁存储在什么地方,都有哪些存储信息

接下来,我们看看运行时怎么查看这些信息?

参考(搞定Hotspot-api, HotSpotVM 对象机制实现浅析#1, 查看java对象在内存中的布局)

Java线程&JVM问答系列(1)——深度理解锁_第4张图片

偏向锁

Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁

偏向锁的实现

偏向锁获取过程:
1、 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
2、 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
3、 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
4、 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
5、 执行同步代码。

  注意:第4步中到达安全点safepoint会导致stop the word(JVM GC中Stop the world案例实战),时间很短。
偏向锁的释放:

偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

偏向锁的适用场景

始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作; 
在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用;

 

其他锁,特别是重量锁请参考(深入理解Java并发之synchronized实现原理  java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁)

 

本节最后

推荐书籍

深入理解Java虚拟机_JVM高级特性与最佳实践 第2版_220

Java 并发编程实战

深入理解JVM & G1 GC (周明耀) 完整扫描版

你可能感兴趣的:(多线程,多线程,jvm)