synchronized的实现原理

8 篇文章 0 订阅
1 篇文章 0 订阅

synchronized在底层是如何实现加锁的呢?

使用monitorenter和monitorexit指令实现的

	//count进行累加
    public  void incCount(){
        synchronized (this){
            count = count + 1;//count++;
       }
    }

synchronized关键字包围的同步块,查看.class

 public void incCount();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=5, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: aload_0
         6: getfield      #2                  // Field count:J
         9: lconst_1
        10: ladd
        11: putfield      #2                  // Field count:J
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return

3到15之间是实现+1
在实现count = count + 1; 的前后加了monitorenter和monitorexit,这是编译器帮我们插入的

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处
每个monitorenter必须有对应的monitorexit与之配对
任何对象都有一个monitor与之关联

如果不用同步代码块,而是把synchronized加到方法上

//count进行累加
    public synchronized void incCount(){
     //   synchronized (this){
            count = count + 1;//count++;
      // }
    }
public synchronized void incCount();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=5, locals=1, args_size=1
         0: aload_0
         1: aload_0
         2: getfield      #2                  // Field count:J
         5: lconst_1
         6: ladd
         7: putfield      #2                  // Field count:J
        10: return

改之前:flags: ACC_PUBLIC
incCount()方法是public方法
改之后:flags: ACC_PUBLIC, ACC_SYNCHRONIZED
多了一个ACC_SYNCHRONIZED,表示是一个同步方法,在底层还是monitorenter和monitorexit,只不过在字节码中不能体现,在执行中使用这两个指令实现的

锁存放的位置

synchronized关键字放到java的对象头里面
在代码中 new Object();每一个对象在内存中被存放时还要有对方头
对象头包含一些关键的信息
1.GC年龄…,MarkWord
2.对象类(类型指针) KlassPoint
3.数组长度(一般对象没有)
在这里插入图片描述
在这里插入图片描述
对象头的内容会随着对象的运行会有所改变的
在这里插入图片描述
可能加锁范围之内操作的指令30个*0.6ns = 18ns
上下文切换2w个周期 3~5个微秒
上下文切换费事很亏,于是引入轻量级锁和偏向锁

轻量级锁

重量级锁的情况下,没有拿到锁的线程都要被挂起的,
轻量级锁没拿到锁的不挂起,进行自旋,通过CAS操作来加锁和解锁。
自旋锁:不停地尝试,不去阻塞上下文切换
如果加锁代码块很快能执行完,用轻量级锁就很好

如果加锁代码块需要长的时间去执行,比如等待拿数据,那不停自旋就很亏,所以又引入适应性自旋锁

适应性自旋锁: 自旋次数控制(默认定义为10次),在1.6中自选次数不再是固定的值了,由虚拟机动态进行调整(自旋的时间大概是一个线程的上下文切换的时间)

偏向锁

大部分的情况下同一把锁总是同一个线程获得
在线程拿锁的过程中,锁的获得者总是偏向于第一个拿到锁的线程
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。无竞争时不需要进行CAS操作来加锁和解锁。

CAS不想做了,直接测试下拿锁的是不是我自己
偏向锁第一次还是需要CAS操作,把线程的ID和时间戳…替换掉
第二次开始检查对象头线程ID是不是自己就行了

有第二个线程来竞争,那就锁就升级到轻量级锁
一旦发生竞争就要升级为轻量级锁,把偏向锁撤销,换成轻量级锁
偏向锁的撤销会发生 STW(Stop The Word)
线程1是持有偏向锁的线程,当线程2准备去撤销偏向锁的时候,是会修改线程1的堆栈上的内容的,线程2会修改线程1的相关的数据,偏向锁引入STW,停止线程进行修改
java后台一般会禁止偏向锁,因为一般是多线程并发竞争

总结

锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态

不同锁的比较:
在这里插入图片描述
偏向锁适用场景是没有竞争
一旦发生竞争,偏向锁就会撤销,晋升为轻量级锁
轻量级锁用的是自旋的CAS操作,自选次数太多性能会下降
轻量级锁会膨胀为重量级锁,引入阻塞状态,让CPU做更多有效的事情
锁被膨胀后不能降级
偏向锁、轻量级锁、重量级锁全部是synchronized底层实现,是JVM里面C++代码实现的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值