synchronized进化之路详解

文章详细介绍了Java中synchronized的优化过程,包括锁消除、锁粗化和四种锁状态(无锁、偏向锁、轻量级锁、重量级锁),强调了锁升级的优化策略。同时提到了CAS(CompareandSwap)在锁机制中的作用,以及死锁的四个必要条件。

synchronized的进化过程

jdk1.6 synchronized就是单纯是个重量级锁,后来通过加入了偏向锁等等四种锁状态,使得synchronized性能大大提升。引入了锁消除,即,通过编译阶段对上下文的扫描,删除不必要的加锁操作。

注意事项
1】一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
【2】每个实例都对应有自己的一把锁(this),不同实例之间互不影响;
    例外:锁对象是*.class以及 synchronized修饰的是 static方法的时候,所有对象公用同一把锁;
【3synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁;

锁消除

编译器认为不会发生线程安全问题,就无视了你的锁,不执行monitor相关操作。

锁粗化

比如执行插入数据商品时,是对店铺加锁。那么批量执行的时候,只需要加一次锁。而不是每插入一次就加/释放一次锁。

  StringBuffer sb = new StringBuffer();
     sb.append(s1);
     sb.append(s2);
     sb.append(s3);
  // 线程安全的buffer类,append会加锁,但显然这是可以锁粗话的,会优化成只获得/释放一次锁

synchronized锁状态

一个对象其实有四种锁状态,级别由低到高:

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

这是对于synchronized来说的,所以它底层是偏向锁+CAS(偏向锁/轻量级锁)+wait/notify(重量级锁)实现的

锁升级容易,降级很难

Java对象头:非数组类型,则用2个字宽来存储对象头,如果是数组,则会用3个字宽来存储对象头。在32位处理器中,一个字宽是32位;在64位虚拟机中,一个字宽是64位。其中mark word 存储对象的hashCode或锁信息等。Mark Word如下:

锁状态29 bit 或 61 bit1 bit 是否是偏向锁?2 bit 锁标志位
无锁001
偏向锁线程ID101
轻量级锁指向栈中锁记录的指针此时这一位不用于标识偏向锁00
重量级锁指向互斥量(重量级锁)的指针此时这一位不用于标识偏向锁10
GC标记此时这一位不用于标识偏向锁11

偏向锁

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。于是有了偏向锁。

偏向于第一个访问锁的线程。偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做了,提高了程序的运行性能。

具体原理:第一次拿到锁,给锁加上自己的线程ID。再次访问,锁ID==自己ID,不做CAS。否则尝试使用CAS来替换锁的线程ID变量为我的ID

  • 成功,表示之前的线程不存在了,仍然为偏向锁
  • 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁。

CAS

Compare and Swap

比较并交换。用于在硬件层面上提供原子性操作。在 Intel 处理器中,CAS通过指令cmpxchg实现。 比较是否和给定的数值一致,如果一致则修改,不一致则不修改。

CAS有如下三个变量

  • V:要更新的变量(var)
  • E:预期值(expected) 就是旧值
  • N:新值(new)

CAS操作:判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。

当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

CAS与原子操作详见此处,JAVA本质是调用native的C++的方法来实现的CAS

CAS实现原子操作三大问题
ABA问题

A->B->A,CAS检查不出变化。加上版本号或者时间戳

循环时间长开销大

如果自旋CAS长时间不成功,会占用大量的CPU资源。

让JVM支持处理器提供的pause指令

只能保证一个共享变量的原子操作

锁的升级流程

每一个线程在准备获取共享资源时:

  • 第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。
  • 第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。
  • 第三步,两个线程都把锁对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作, 把锁对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord。
  • 第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋 。
  • 第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则依然处于轻量级锁的状态,如果自旋失败(默认自旋阈值为10次,也就是CAS10次还没拿到锁,就是自旋失败)
  • 第六步,自旋失败,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。

具体实现可以看这篇文章,以及各种锁的优缺点对比 好文

死锁

死锁只有同时满足以下四个条件才会发生:

  • 互斥条件;
  • 持有并等待条件;
  • 不可剥夺条件;
  • 环路等待条件;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值