从synchronized深入浅出java锁的机制

synchronized在多线程并发编程中一直是元老级人物,而且在JDK1.6之前由于实现同步所带来的性能消耗过大,因而被称为重量级锁,随着JDK1.6对synchronized的各种优化,它现在也就没有那么重量级了。

这里就涉及到jdk1.6升级新增的偏向锁,下面锁升级和锁的内存重点以偏向锁为线索来介绍。

锁升级

synchronized在jdk1.6之前一开始便是重量级锁,jdk1.6新添加了偏向锁,首先一上来,他是一个无锁的状态,当有线程获得锁了,那么升级为偏向锁,偏向锁也还没有真正的上锁,他在对象头上,记录持有线程的ID,然后下次过来,先比较ID,如果是同一个线程ID,则直接放行,这个过程没有上锁解锁操作。

如果现在另外一个线程进入了,那么就升级为CAS机制的锁资源争抢,就是线程A获取到资源了,那么线程B使用CAS自旋的方式不断的尝试获取锁。如果长时间获取不到,或者后面又来了更多的线程同时争抢,那么最终就会升级到重量级锁。

重量级锁就会涉及到用户态和内核态的一个切换,但是也比大量的CAS要好一些。

所以注意了,并不是任何时候CAS争抢都要比重量级锁好的,CAS自旋的时候,消耗了CPU资源,但是大部分时间其实是空转的一个情况,因为你抢不到锁,10个线程同时争抢,只有1个线程抢成功,其他9个就必须自旋。上面提到的锁升级建立的条件是jdk1.6到jdk15之间的,jdk15已经删除了偏向锁,无锁升级变为轻量级锁。

锁的内存

Java对象的对象头在HotSpot虚拟机中,对象在内存中的存储的布局

我们这里可以写一个简单的程序看看,下面是打印信息说明

这是JDK8的打印结果

这里可以看出来,在这里可以看出来,new出来的对象有3个部分构成:对象头(header),实例数据,填充(gap),对象头(header)又分为2部分,一个是mark,另一个是class,锁这边会引起对象头的变化,下面表格是上面程序执行结果打印信息介绍

刚创建对象时候是001(无锁状态),睡眠了4100毫秒,这时候创建出来的对象就是偏向锁了,原因是jdk默认开启偏向锁的,但偏向锁是延迟加载的,程序启动时候,new出来的对象,打印时候是无锁,等待4100毫秒之后,再创建的对象,这时候jdk的偏向锁加载完成了,打印出来就是有101(偏向锁)了(PS:启动程序中的打印对象偏向锁,也是属于线程调用,所以打印时候就由无锁变为偏向锁)。而在后续的很多jdk版本中都有过对偏向锁的优化,但偏向锁的释放的耗时优化上面一直不尽人意,导致高并发时候性能上受到影响,所以jdk15的时候,已经去掉了偏向锁,在锁升级过程中无锁直接会变成轻量级锁(嘿嘿,还在背八股文的同学们,你们背的偏向锁已经是过去式了)。

根据Mark Word信息来介绍锁升级过程

1.首先JVM刚启动,所有的对象锁都会变成偏向锁

2.当第一个线程访问对象锁的时候,CAS交换(在对象头的Mark Word字段保存自己的线程ID),如果成功则获取到锁。

3.当第二个线程访问这个对象锁的时候,CAS交换,此时第一个线程还在持有锁,这个时候肯定CAS交换失败,存在竞争,先撤销偏向锁,升级成为轻量锁。

4.当第三个线程访问这个对象锁的时候,发现这个锁时轻量锁,线程开始自旋获取锁,如果获取成功(自旋的时候第二个线程已经释放锁了),这个锁就被第三个线程获取到了(还是轻量级锁)。

5.当第四个线程访问这个对象锁的时候,线程开始自旋获取锁,如果获取失败,这个锁会升级成为重量级锁。后面如果还有线程来获取这个对象锁的话,就会进入阻塞状态。

自旋锁(CAS)

上面说升级提到了自旋锁,这里我们就展开了介绍自选锁的原理。

自旋锁的诞生

在理解自旋锁之前,必须要先知道自旋锁要解决的难题是什么:阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需要等一等(也叫做自旋),在等待持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程在用户态和内核态之间的频繁切换而导致的时间消耗。

自旋锁的时间阈值

自旋锁用于使当前线程占着CPU资源不释放,等到下次自旋获取锁资源后立即执行相关操作,但如何选择自旋锁的执行时间呢?如果自旋的执行时间太长,则会有大量的线程处于自旋状态且占用CPU资源,造成系统资源的浪费。因此,对自旋的周期选择将直接影响系统的性能。
Java中JDK的不同版本所采用的自选周期不同,JDK1.5为固定的时间,JDK1.6引入了适应性自旋锁,适应性自旋锁的自旋时间不再是固定值,而是由上一次在同一锁上的自旋时间及锁的拥有者的状态来决定的,可基本认为一个线程上下文切换的时间就是一个锁自旋的最佳时间。

自旋锁的优缺点

优点

自旋锁可以减少CPU上下文的切换,对于占用锁时间非常短或锁竞争不激烈的代码块来说性能大幅提升,因为自旋的CPU耗时明显少于线程阻塞、挂起、再次唤醒时两次CPU上下文切换的耗时。

缺点

在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,将引起CPU资源的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁。

自旋锁的原子性

上面我们知道synchronized在锁升级中用到了自旋,我们知道cpu执行线程的时候会出现上下文切换,那么上下文切换时候如何保证他的原子性的呢?

java也有自旋锁实现的功能:

AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();

例如这自增就是才有的自旋模式,我们阅读源代码实现,发现整个CAS被三个值主导:V,A,B。首先CAS,compareAndSet方法将A与实际值V进行比对,如果相等,那么就可以操作修改为B,如果不等那就放弃操作。但准备执行修改操作时候,上下文切换,另一个自旋的线程这时候也执行修改操作,那不是就会出问题?但事实上不会出问题的,那这又是如何保证原子性,这里就又涉及到了HotSpot虚拟机的处理,后续介绍hotSpot如何操作保证原子性(感觉写不完了,后面又牵扯到字节码,读写屏障,后续写多线程相关文章再介绍吧,这里做个埋笔,多线程文章写完后再添加链接)。

  • 16
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值