当前位置: 首页 > news >正文

9.synchronized的三把锁

我们知道,同步锁无非是多个线程抢占一个资源,如果抢占成功就获得了锁,失败的线程则阻塞等待,直到获取到锁被释放再重新抢。貌似该过程就只有一种情况 ,但是观察上面的对象头结构,会发现里面标记了偏向锁、轻量级锁和重量级锁三种,又表示什么呢?

其实在jdk6之前,synchronized确实只有一种方式——重量级锁,其基本原理是使用底层操作系统的MutexLock来实现,该过程会把当前线程挂起,并从用户态切换到内核态。其问题是开销太大、性能不足,因为很多场景可能不需要这么重的操作。这在生活中也很常见,例如,如果平时买火车票,你只要提前一两天订好就行了,如果买春节的票就需要召唤小伙伴和七大姑八大姨,让他们一起帮你抢。前者竞争没那么大,我们使用轻量级操作就行了,而后者竞争激烈,需要使用大招。

从JDK6开始,synchronized做了很多优化工作,其中就包括上说的三种锁,其核心设计理论就是如何让线程在不阻塞的情况下保证线程安全。本节我们先看一下三种锁的特征,后面再分析其原理。

1 偏向锁

偏向锁就是在没有竞争时加的锁!这句话是不是很奇怪,没有竞争为什么要加锁?这是因为这种场景可能存在竞争的情况,我们加锁是为防范,就像上面说的买票的例子,一开始我们怕抢不到,就召唤一群人来帮忙,但是实际可能当天就没人买票,这样确实能防范可能存在的竞争问题,但是代价太大,因此我们使用代价较低的偏向锁来解决。这种按照最坏情况来处理的锁也称为“悲观锁”。

而偏向锁是“乐观思维”,思想是,线程再没有竞争的情况下访问资源时,会先通过CAS方式来抢占资源,如果成功则修改对象头的标记,也就是昭告天下“这个对象是我的”,具体操作是将偏向锁标记修改为1,锁标记修改为01,并将线程ID写入到对象中。这样其他线程再访问,发现这个对象已经被其他的抢占了,就只能先阻塞一段时间再去抢占。

那为啥叫“偏向锁”呢?假如线程A抢到了资源,如果线程B再抢则会被阻塞,但如果线程A再次抢占呢?例如代码里出现对某个资源嵌套加锁,此时该让A阻塞吗?不是的,因为资源就被A的还没释放,A再次抢占就应该直接放行。这是不是对其他线程不公平呢?是的,因此这种机制叫做偏向锁,偏向早就获得资源的锁。

我们看一个偏向锁的例子:

public class BiasedLockExample {
    public static void main(String[] args) throws InterruptedException {
        BiasedLockExample example=new BiasedLockExample();
        System.out.println("加锁之前");
        System.out.println(ClassLayout.parseInstance(example).toPrintable());

        synchronized (example){
            System.out.println("加锁之后");
            System.out.println(ClassLayout.parseInstance(example).toPrintable());
        }
    }
}

在上述代码中,BiasedLockExample演示了针对example这个锁对象,在加锁之前和之后分别打印的内存对象布局,我们看一下:

part_b_inside.chapter2_synchronzied.BiasedLockExample object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

在加锁之前,我们发现对象头中的第一个字节00000001最后三位001,其中低位的两位数表示锁标记,它的值是[01],表示当前为无锁状态。

加锁之后的我们再看一下:

part_b_inside.chapter2_synchronzied.BiasedLockExample object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           90 79 bc 0c (10010000 01111001 10111100 00001100) (213678480)
      4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

在加锁之后,我们发现对象头中的第一个字节10010000最后000,其中低位的两位数表示锁标记,它的值是[00],表示轻量级锁。

这里不存在竞争,应该是偏向锁,那为什么是轻量级锁呢。网上说是因为偏向锁开启会先延迟4秒,需要添加参数

-XX:BiasedLockingStartupDelay=0

然后再看:

此时看到第一个字节为00000101,末尾三位是101,第一个1表示偏向锁,后面的01表示当前是偏向锁状态。

我们可以将偏向锁的执行过程归纳如下:

补充

如果你调试一下上面的例子,就会发现加锁之前打印的结果第一个字节也是00000101,但是此时并没有加锁。一种解释是此时标记表示是可偏向的状态。

2 轻量级锁

偏向锁是没有竞争时获得锁资源,这种方式比单纯的加锁性能要高,但是如果此时真的有多个线程来竞争了该让竞争失败的先阻塞,直到被唤醒再重新抢锁。那是否有效率更高的方法呢? 我们可以让没有抢到资源的线程进行一定次数的重试(自旋转),例如重复抢3次或者5次等等。如果抢到了就不用重试了, 否则继续阻塞。这就是轻量级锁。 上面进行一定次数的重试的过程就叫自旋锁,也是基于CAS方式实现的。

 当然自旋的次数不是没有限制的,一般是10次,而且JVM还会执行自适应的策略来优化。

轻量级锁也可以使用上面的BiasedLockExample来展示,将参数BiasedLockingStartupDelay取消掉展示的就是轻量级锁。

3 重量级锁

轻量级锁只适合在较短的时间里能获得锁的场景,如果长时间获取不到就不能一直自旋了,因为此时线程还占用了资源,但是什么都做不了,因此自旋到一定次数之后就要让其阻塞。

从上面的例子可以看到,如果在偏向锁、轻量级锁这些类型中无法让线程获得锁资源,那么这些没获得锁的线程最终的结果仍然是阻塞等待,直到获得锁的线程释放锁之后才能被唤醒,而在整个优化过程中,我们通过乐观锁的机制来保证线程的安全性。

下面这个例子演示了在加锁之前、单个线程抢占锁、多个线程抢占锁的场景,对象头中的锁的状态变化。

public class HeavyLockExample {
    public static void main(String[] args) throws InterruptedException {
        HeavyLockExample heavy=new HeavyLockExample();
        System.out.println("加锁之前");
        System.out.println(ClassLayout.parseInstance(heavy).toPrintable());
        Thread t1=new Thread(()->{
            synchronized (heavy){
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        //确保t1线程已经运行
        TimeUnit.MILLISECONDS.sleep(500);
        System.out.println("t1线程抢占了锁");
        System.out.println(ClassLayout.parseInstance(heavy).toPrintable());
        synchronized (heavy){
            System.out.println("main线程来抢占锁");
            System.out.println(ClassLayout.parseInstance(heavy).toPrintable());
        }
        System.gc();
        System.out.println(ClassLayout.parseInstance(heavy).toPrintable());
    }
}

 结果比较长,我们不再贴出来,读者可以从中看到锁状态的逐步升级。

相关文章:

  • 为什么开发人员正在成为供应链攻击中的最薄弱环节
  • MySQL之事务、锁
  • 项目第二天
  • Windows与网络基础-10-windows用户管理
  • 计算机网络笔记(王道考研) 第三章:数据链路层
  • apifox 提取cookie字段添加自动鉴权
  • ATF启动(一):整体启动流程
  • 25. Python 字符串的切片方法
  • 接口测试自动化脚本框架4
  • HadoopSpark
  • 51单片机4位抢答器_倒计时可调仿真设计
  • 设计模式之模板方法模式的理解
  • 小型功率放大器的设计与制作——功率放大器电路总结
  • 接口测试自动化脚本框架5
  • Elasticsearch ik分词器的安装和使用
  • 9月CHINA-PUB-OPENDAY技术沙龙——IPHONE
  • 「前端」从UglifyJSPlugin强制开启css压缩探究webpack插件运行机制
  • 【comparator, comparable】小总结
  • Apache Zeppelin在Apache Trafodion上的可视化
  • CentOS7简单部署NFS
  • crontab执行失败的多种原因
  • CSS居中完全指南——构建CSS居中决策树
  • css系列之关于字体的事
  • ES6简单总结(搭配简单的讲解和小案例)
  • Lsb图片隐写
  • maya建模与骨骼动画快速实现人工鱼
  • OSS Web直传 (文件图片)
  • use Google search engine
  • vue-router的history模式发布配置
  • vue从创建到完整的饿了么(18)购物车详细信息的展示与删除
  • 对JS继承的一点思考
  • 计算机在识别图像时“看到”了什么?
  • 今年的LC3大会没了?
  • 事件委托的小应用
  • 适配mpvue平台的的微信小程序日历组件mpvue-calendar
  • 我看到的前端
  • 原生Ajax
  • 在Docker Swarm上部署Apache Storm:第1部分
  • 深度学习之轻量级神经网络在TWS蓝牙音频处理器上的部署
  • 支付宝花15年解决的这个问题,顶得上做出十个支付宝 ...
  • ###C语言程序设计-----C语言学习(3)#
  • #我与Java虚拟机的故事#连载05:Java虚拟机的修炼之道
  • $.extend({},旧的,新的);合并对象,后面的覆盖前面的
  • $emit传递多个参数_PPC和MIPS指令集下二进制代码中函数参数个数的识别方法
  • (4)(4.6) Triducer
  • (5)STL算法之复制
  • (C语言)strcpy与strcpy详解,与模拟实现
  • (笔试题)分解质因式
  • (动态规划)5. 最长回文子串 java解决
  • (仿QQ聊天消息列表加载)wp7 listbox 列表项逐一加载的一种实现方式,以及加入渐显动画...
  • (十三)Maven插件解析运行机制
  • (十五)Flask覆写wsgi_app函数实现自定义中间件
  • (完整代码)R语言中利用SVM-RFE机器学习算法筛选关键因子
  • (原創) X61用戶,小心你的上蓋!! (NB) (ThinkPad) (X61)
  • **Java有哪些悲观锁的实现_乐观锁、悲观锁、Redis分布式锁和Zookeeper分布式锁的实现以及流程原理...