synchronized的实现原理

1. CAS操作

CUP去执行,修改哪个对象的哪个属性,把内存中的从oldvalue,改成newvalue。

1.1 什么是CAS?

在使用锁时,线程获取锁是一种悲观锁策略,当多线程发生竞争关系时,只能一个线程执行,其他线程陷入阻塞状态。而CAS是一种乐观锁的状态,它会认为没有发生冲突,不会陷入阻塞状态,一直自旋获取锁。直到没有冲突,获取锁。

1.2 CAS的操作过程

CAS(比较and替换)操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。这个过程不需要中断或原子锁,因此可以提供更高的并发性。

1.3 CAS带来的问题:
  1. ABA问题 :就比如转账一个人的账户余额从100变成200,再从200变成100,CAS会认为它没有发生变化,但其实是有变化的,我们可以通过数据库事务原理MVCC通过版本控制实现。
  2. 因为自旋线程不会陷入阻塞,会一致占用cpu资源,自旋会浪费大量的处理器资源。
  3. 公平性,自旋会插队在阻塞状态前。
  4. CAS只能保证一个共享变量的原子操作。
2. synchronized的三种用法:
  • 对于普通同步方法,锁是当前的实例对象
  • 对于static同步方法,锁是当前class对象
  • 对于同步代码块,锁是()中配置的对象

当一个线程访问同步代码块时,它必须获得锁,在退出或者抛出异常时必须释放锁。

3. synchronized的优化
3.1 java对象头

Synchronized用的锁是存放在对象头中的,java对象头中的Mark word存储对象运行时数据,如默认存储着对象的hashcode,分代年龄和锁标记位
运行期间,Mark word存放的数据会随着锁标记位的变化而变化。这几个状态会随着竞争情况逐渐升级,但是不能降级。下面是其存储结构。
在这里插入图片描述

3.2偏向锁

下面就说下锁升级的情况,偏向锁。

场景:大多数情况下,锁不仅不存在多线程竞争的关系,反而每次都是同一个线程获得,为了使获得锁的代价更低,就引入的偏向锁。
3.2.1 偏向锁的获取

当一个线程访问同步代码块时并获取锁时,会在对象头和栈帧中存放偏向锁的线程id,以后该线程进入和推出同步代码块就不需要CAS加锁和解锁了,只需要简单的测试一下该对象头中Mark word中是否存放着指向该线程的偏向锁,
—1.如果测试成功,则获得锁,
—2.如果测试失败,则需要再测试一下Mark word中的偏向锁标识是否设置成了1(表示当前是偏向锁),
——3.如果没有设置(就是没线程占),就使用CAS竞争锁,
——4.如果设置了(就是有线程占着),则尝试CAS将对象头中的偏向锁指向当前线程。

3.2.2 偏向锁的撤销

偏向锁只有等到竞争关系出现才会释放锁,(线程1持有线程id指向自己,线程2竞争,就是上面第4步的情况)
锁撤销**–>等到安全节点(当前没有字节码运行)–>暂停拥有偏向锁的线程–>**判断该线程是否存活,
假设线程1持有锁,线程ID指向自己,此时线程2竞争锁,等到全局安全点,暂停线程1,判断线程1是否存活,
if存活:
——1. 先判断该对象头中的Epoch值(该值表示此对象的偏向锁撤销次数,默认撤销40次以上)
————若该值大于40直接升级为轻量级锁,不管线程1是否继续竞争。
——2. 然后判断线程1是否继续竞争
————2.1. 若竞争,升级轻量级锁,
————2.2 若不竞争,线程2获得偏向锁指向自己。
if不存活
——1. 将对象头设置成无锁状态,线程2继续竞争锁
啥也不说了,上图
在这里插入图片描述

3.2.3 偏向锁的获取和撤销流程

在这里插入图片描述

3.2.3 关闭偏向锁

有时候系统确定存在多个线程竞争,想要关闭偏向锁,直接进入轻量级锁状态。
可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,程序就会默认进入轻量级锁状态。

3.3轻量级锁
场景:多个线程在不同的时刻请求同一把锁,也就是不存在竞争关系,针对这种情况jvm采用了轻量级锁来避免线程的阻塞和唤醒。
3.3.1轻量级锁的加锁

线程执行同步代码块之前,

  1. jvm会在当前线程中的栈帧中创建用于存放锁记录的空间,
  2. 并将对象头中的Mark word复制到锁记录空间中,
  3. 然后线程尝试使用CAS将对象头中的Mark word指向锁记录的指针,
  4. 如果成功当前线程获得锁,
  5. 如果失败,表示其他线程竞争锁,当前线程就会使用自旋获取锁,然后失败,锁膨胀,修改为重量级锁,线程阻塞。

当上面的线程1获得轻量级锁,线程2想要使用CAS将对象头中的Mar word替换成指向自己的指针,就会失败,然后CAS自旋获取锁,失败,升级为重量级锁,然后线程2陷入阻塞状态。

3.3.2轻量级锁的解锁

轻量级锁解锁时,会使用CAS把Mark word替换会对象头,如果成功,则表示没有竞争发生,如果失败,表示当前锁存在竞争,释放锁并唤醒等待的线程,重新争夺锁访问同步块。
直接上图:
在这里插入图片描述

4.锁的特点
  • 偏向锁
      偏向锁只会在第一次请求锁时使用CAS操作,并在锁对象的标记字段中记录当前线程ID。在此后的运行过程中,持有偏向锁的线程无需加锁操作针对的是锁仅会被同一线程持有的状况。
  • 轻量级锁
      轻量级锁采用CAS操作,将对象头中的Mark Word替换为指向锁记录的指针。针对的是多个线程在不同时间段申请同一把锁的情况。
  • 重量级锁
      重量级锁会阻塞、唤醒请求加锁的线程。针对的是多个线程同时竞争同一把锁的情况。JVM采用自适应自旋,来避免在面对非常小的同步代码块时,仍会被阻塞和唤醒的状况。
4.锁粗化

锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次操作。将多个联系的锁扩展为一个范围更大的锁。

public class Test{
    //全局变量有线程安全问题
    public static StringBuffer sb = new StringBuffer();
    public static void main(String[] args) {
        sb.append("a");
        sb.append("b");
        sb.append("c");
    }
}
//StringBuffer的append方法
public synchronized StringBuffer append(String str){
.......

每次调用StringBuffer的append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一对象加锁和解锁操作,就会在在第一次append方法时进行加锁,在最后一次append方法结束后进行解锁。

5.锁消除

删除不必要的加锁操作,如果判断一段代码中,堆上的数据不会逃逸出当前线程,则认为此代码是线程安全的,无需加锁。

public class Test{
    public static void main(String[] args) {
        //sb变量是局部变量,不会有线程安全问题,加锁、解锁没有意义
        StringBuffer sb = new StringBuffer();
        sb.append("a");
        sb.append("b");
        sb.append("c");
    }
}

参考:这篇文章
书籍:java并发编程的艺术
更多:邓新

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值