多线程(三)偏向锁,轻量级锁,重量级锁,自旋锁,线程池的实现

一、偏向锁
1.1 、java对象在内存中的存储结构主要有以下三部分

1.对象头(主要是一些运行时的数据) 2.实例数据 3.对齐填充

1.2、java虚拟机的对象头里主要包含两部分信息
长度内容说明
32/64bitMark Word用于存储自身运行时数据,如hashCode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。
32/64bitClass Meta Address指向对象类型数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
32/64bitArray Length数组的长度(当对象为数据时)

MarkWord这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,
32bit空间的25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0.
32位虚拟机在不同状态下markword的结构如下图。
在这里插入图片描述

1.3、偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得的代价更低而引入偏向锁。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
如果有两个线程来竞争锁的话,此时锁就会膨胀,偏向锁就被升级为轻量级锁,这也是常说的锁膨胀

偏向锁的获取过程:

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

锁撤销的过程:

  • 1.在一个安全点停止拥有锁的线程,(撤销偏向锁会导致stop the word操作,导致性能下降)
  • 2.遍历线程栈,如果存在锁记录的话,需要修复锁记录和Markword,使其变成无锁状态。
  • 3.唤醒当前线程,将当前锁升级为轻量级锁。

由于锁撤销花销挺大,所以,如果某些同步代码块大多数情况下都是有两个或者两个以上的线程来竞争的话,那么偏向锁就是一种累赘,我们可以一开始就把偏向锁关掉。

1.4、轻量级锁

轻量级锁的加锁过程:

  • 1.在代码进入同步块的时候,如果同步对象锁为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先在当前线程的线帧中建立一个名为锁记录(Lock Record)
    的空间,用于存储锁对象目前的Markword的拷贝,官方称之为Displaced Mark Word。这个时候线程堆栈与对象头的状态如图所示:
    在这里插入图片描述
  • 2.拷贝对象头中的Markword复制到锁记录中
  • 3.拷贝成功后,虚拟机将使用CAS操作尝试将对象的Markword更新为指向Lock Record的指针,并将Lock Record中的owner指针指向object Mark word。如果更新成功则
    执行步骤4,否则执行5
  • 4.如果这个更新成功了,那么这个线程就拥有了这个对象的锁,并且对象Markword的标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示:
    在这里插入图片描述
  • 5.如果这个更新失败了,虚拟机首先会检查对象的Mark word是否指向当前线程的线帧,如果是说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。

轻量级锁的释放
由轻量级锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的Markword,在释放锁的时候如果它发现它持有锁的期间有其他线程来
尝试获取锁了,并且该线程对Markword做了修改,两者对比发现不一致,则切换到重量锁。

1.5 、自旋锁

轻量级锁有两种
1.自旋锁       2.自适应自旋锁

JDK6以前,Java虚拟机的锁都是通过互斥来实现的。互斥同步对性能最大的影响是线程的阻塞,线程的阻塞和唤醒需要CPU从用户态转为核心态。挂起和恢复线程会增加CPU负担,影响并发能力。而且,很多情况下数据的锁定状态只会持续很短的时间,为此挂起和恢复线程不值得。

自旋锁:指当另一个线程来竞争时,这个线程会在原地等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁。

但是,锁在原地循环的时候,是会消耗CPU的,相当于一个什么也不做的for循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程在原地等待较短的时间就可以获取锁了。

由于线程在原地空转是消耗CPU的,我们可以给空循环设置一个次数,当线程超过了这个次数,我们就认为,继续自旋不合适了,此时锁会继续膨胀,升级为重量级锁。
默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin来进行更改

自适应自旋锁
自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

大概原理:
假如线程1刚刚获得了锁,释放之后,线程2获得了锁,但是线程2在运行的过程中,线程1又想获取锁,但是线程2还没有释放,所以线程1只能自旋等待,但是虚拟机认为由于线程2刚刚通过自旋获得过锁,所以线程1这次获得锁的几率还是很大的,所以会延长线程1的自旋次数。

1.6、重量级锁

重量级锁是依赖对象内部的monitor对象来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被称为互斥锁。

为什么说重量级锁开销大呢?

当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗CPU,
但是阻塞或者唤醒一个线程时,都需要操作系统帮忙,这就需要从用户态转换到内核态,
而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

参考网址:https://www.cnblogs.com/lzh-blogs/p/7477157.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值