对象监视器锁、轻量级锁

转自blog.csdn.net/mxiangjian/article/details/53115146

阅读本文,你可以了解到:

1、对象监视器锁(重量级锁)、轻量级锁、偏向锁的含义

2、它们之间的区别

3、每一种锁的使用场景什么。

我们跟着一下几个问题一步一步深入了解:

什么是对象监视器锁 为什么它叫重量级锁 

     我们知道在Java中实现同步的一种机制 是利用synchronized关键字来修饰同步代码块,它的本质就是对象进行加锁在达到同步的目的 我们称这种锁为对象监视器锁。

那么为什么使用这个关键字来修饰代码块,就能让线程达到互斥同步的目的呢?

       根据java虚拟机规范,被synchronized关键字修饰的代码块在被编译成字节码的时候 会在该代码块开始和结束的时候插入monitorenter 和 moniterexist指令,虚拟机在执行这两个指令的时候会检查对象的锁状态是否为空或当前线程是否已经拥有该对象锁 如果是 则将对象锁的计数器加1 直接进入同步代码执行。如果不是当前线程就要阻塞等待 等到锁释放 。

扫描二维码关注公众号,回复: 3525752 查看本文章

那么为什么说这种操作很重呢?

       java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的get 或set方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchronized是java语言中一个重量级的操纵。所以jvm的研究人员在1.6的时候花费了大量的时间来优化重量级锁,于是在1.6中出现了轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化(自旋锁在1.4就有 只不过默认的是关闭的,jdk1.6是默认开启的),这些操作都是为了在线程之间更高效的共享数据 ,解决竞争问题。

那么轻量级锁的原理是什么呢 它为什么要比synchronized要轻呢

     轻量级锁 是在jdk1.6之中加入的 它名字中的“轻量级”是相对不使用系统互斥量来实现的传统锁而言的,首先需要强调的是它不是用来代替重量级锁的,它的本意是在没有多线程竞争的情况下 减少使用操作系统互斥量产生的性能消耗。
   要理解轻量级锁,必须从HotSpot虚拟机的对象头的内存布局来介绍,HotSpot虚拟机的对象头由两部分组成,第一部分是存储对象自身运行时的数据 如哈希码 GC分代年龄,锁标记位等 官方称为Mark Word 它是实现轻量级锁和偏向锁的关键,另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话 还会有一个额外的部分用于
存储数组长度,以32位虚拟机为例  对象头的描述如下图所示:

锁状态

25 bit

4bit

1bit

2bit

23bit

2bit

是否是偏向锁

锁标志位

轻量级锁

指向栈中锁记录的指针

00

重量级锁

指向互斥量(重量级锁)的指针

10

GC标记

11

偏向锁

线程ID

Epoch

对象分代年龄

1

01

无锁

对象的hashCode

对象分代年龄

0

01

根据上图可知 32位虚拟机用2bit表示锁标记  00表示轻量级锁 01表示无锁状态(还有一个bit为表示 有无偏向锁) 10表示重量级锁。
      简单的了解了对象在内存中的布局后,我们把话题返回到轻量级锁的执行过程,在代码进入同步块的时候,如果同步对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储对象目前Mark Word的拷贝,这时候线程堆栈与对象头的状态如图所示:

然后虚拟机将使用CAS操作尝试将对象头中的Mark Word 更新为指向当前线程Lock Record的指针,如果这个更新执行成功了,那么这个线程就拥有了这个对象的锁,并且将Mard Word中的标记位改为00,即表示该对象处于轻量级锁状态,这时候线程堆和对象头的状态如图所示:

如果这个状态更新失败了,虚拟机将会检查对象中的Mark Word 是否指向当前线程的栈帧,如果是直接进入同步代码块 执行,如果不是 说明有线程竞争。如果有两条以上的线程在抢占资源,那轻量级锁就不再有效,要膨胀为重量级锁,锁的状态更改为10, Mard Word中存储的就是指向重量级锁的指针 后面等待的锁就要进入阻塞状态。

       上述描述的轻量级锁的加锁过程,它的解锁过程也是通过CAS来进行操作的,如果Mard Word仍然指向当前线程的锁记录,那就要用CAS操作把当前的Mard Word和线程中复制的Lock Record 替换回来,如果替换成功 那么真个同步过程就完成了 ,如果替换失败,说明有其他线程尝试过获取该锁,那就要释放锁的同时 唤醒被挂起的线程。

       轻量级锁性能提升的依据是 “对于绝大多数的锁”在整个同步周期被都是不存在竞争的,这是一个经验数据,如果没有竞争 轻量级锁使用CAS操作避免使用系统互斥量的开销。但如果存在竞争。除了互斥的开销外 还要额外的发生CAS操作因此在有竞争的情况下 轻量级锁会比传统的重量级锁还要慢
     总结一下 :轻量级锁的 “轻” 在于当没有多线程竞争的情况下 只利用简单的CAS 操作 来代替 操作系统互斥量在减少开销 所以要轻。但是如果存在多线程竞争的情况是 轻量级锁不但不会轻  反而会更重。

那什么是偏向锁呢

    以上介绍了轻量级锁,理解了轻量级锁那么在理解偏向锁就会更容易了,我们首先回顾一下 轻量级锁的引入是 是为了提升在没有线程竞争的情况下 执行同步代码的效率的。那么还有一种特殊的情况就是,始终只有一个线程在执行同步块,在这种情况下 即使使用轻量级锁 也是需要多个CAS操作的,所以也有一部分开销,于是JVM研究人员又引入了另一种锁 即偏向锁 来适用这种情况。偏向锁中的偏 就是偏心的“偏” 它的意思是让这个锁始终偏向第一个获取它的线程,如果接下来的执行过车个中 ,该锁没有被其他线程获取 则持有偏向锁的线程将永远不需要再进行同步。当开启了偏向锁功能,当代码进入同步块的时候,虚拟机会检查当前线程是否处于无锁状态01 且标记位为0 没有偏向锁 ,那么线程就会使用CAS操作吧获取到这个锁的线程ID记录在对象的Mark Word中,如果获取成功 ,那么持有偏向锁的线程以后在每次进入这个锁相关的同步块的时候,虚拟机不再使用任何的同步操作只坚持对象头中是否是当前线程ID,如果是直接进入  省去了CAS操作。当另外一个线程获取该对象锁的时候,偏向模式就会宣告结束,根据锁对象的当前状态,撤销偏向后恢复大无锁状态或偏向锁状态,之后的操作就如同上述轻量级锁的介绍。

    总结一下  偏向锁只适用于在只有一个线程执行同步代码块的情况 ,如果程序中大部分锁总是被不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下有时候禁用偏向锁反而可以提高性能。

以上介绍了 三个比较男理解的锁 以及他们之间的转换 下面介绍下 锁粗话  锁消除 适应性自旋锁的概念

锁粗化:

         原则上,在我们编写代码的时候,我们总是推荐将同步代码块的范围尽量缩小,只有在数据争用的时候才进行同步,这样是为了使得需要同步的操作数量尽可能的变小,如果存在锁竞争,那等待的线程也能很快拿到锁。
大部分情况下 ,上面的原则是正确的,但是如果一些列连续的操作都需要对一个对象反复的加锁和解锁,甚至加锁操作时出现在循环里面的,这样即使没有线程竞争,频繁的加锁也会导致不必要的性能消耗例如代码:
StringBuffer sb = new StringBuffer();
sb.append(1);
sb.append(2);
sb.append(3);
.......
如果出现这样的情况,虚拟机会探索出这样一串零碎的操作都对同一个对象进行加锁,虚拟机就会将加锁同步的范围扩展(粗化)到整个序列的开始和结束,以上述代码为例,就是将锁范围扩展到第一个append之前 到最后一个append之后。这样只需要一次加锁就可以了。

锁消除:
         锁消除是指虚拟机在即时编译器在运行时,对于一些在代码上要求同步,但是被检测到不可能存在数据竞争的锁进行消除。


自旋锁和适应性自旋锁:
        前面我在讨论互斥同步的时候,提到了互斥同步操作对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转移到内核态去执行,这些操作给系统的并发性能带来了很大的压力,同时虚拟机团队也注意到在很多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得,如果物理机器上有
一个以上的处理器,能让两个以上的线程同时并行执行,我们就可以让后面请求的那个线程先“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁,为了让线程不放弃处理器的时间,我们只需要让下城执行一个忙循环(自旋) 这项技术就是自旋锁。在jdk1.6中 自旋锁是默认开启的  自旋不能代替最,因为自旋本身也要一直占用处理器时间,如果锁被占用的时间很短,那么自旋等待的效果就会很好,反之,如果
锁被占用的时间很长,那么自旋的线程只能白白的占用处理器自旋,而不会做任何有用的工作,反而会给性能带来浪费。因此 自旋等待的时间就要有一定的限度如果超过了这个限度仍然没有获取锁,那么就要里用传统的方式将线程挂起。在jdk1.5 这个限度是一定的写死的在1.6之后 虚拟机研究人员引入了适应性自旋锁,适应性自旋锁意味着 自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态
来决定,如果在同一个锁对象上,自旋锁刚刚成功获得过锁,并且持有锁的线程正在运行,那么虚拟机就会认为这次自旋也很有可能再次成功,进而他会将自旋等待的时间相对持续的更长,比如100个循环。另外 如果针对某个锁  通过自旋 从来没有获得过, 现在以后获取这个锁将可能省略了自旋过程,以避免浪费处理器时间。有了适应性自旋 ,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况就会越来越准确,虚拟机就会变得越来越聪明了。

猜你喜欢

转载自blog.csdn.net/qq_32252957/article/details/82966516