synchronized原理

synchronized用于多线程访问共享资源时,通过同步机制保证线程安全。包括同步代码块、同步方法、静态同步方法,详见《Java多线程(下)》。本章介绍下synchronized原理。

synchronized是JDK自带的一个关键字,在JDK1.5之前是一个重量级锁,会切换线程状态,导致性能不好,所以从性能上考虑大部分人会选择Lock锁,不过毕竟是JDK自带的关键字,所以在JDK1.6后对它进行优化,引入了偏向锁轻量级锁自旋锁等概念。

synchronized四大特性

  • 原子性
    synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,这中间的过程无法被中断,即保证了原子性。

  • 可见性
    synchronized对一个类或对象加锁时,锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存当中,保证资源变量的可见性。

  • 有序性
    synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

  • 可重入性
    synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己已经持有对象锁的临界资源时,可直接调用,这种情况属于重入锁。

可重入性最大的作用是避免死锁。

synchronized锁的原理

由之前文章《JVM-Java Virtual Machine(Java虚拟机)》,在JVM中,对象在内存中分为三块区域:对象头,实例数据和对齐填充。

对象头:

  • Mark Word,用于存储对象自身运行时的数据,如哈希码(Hash Code),GC分代年龄,锁状态标志偏向线程ID偏向时间戳等信息,它会根据对象的状态复用自己的存储空间。它是实现偏向锁和轻量级锁的关键
  • 类型指针,对象会指向它的类的元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。
  • Array length,如果对象是一个数组,还必须记录数组长度的数据。

1、同步代码块原理

为了看底层实现原理,使用javap -v xxx.class命令进行反编译。

这是使用同步代码块被标志的地方就是刚刚提到的对象头,它会关联一个monitor对象,也就是括号括起来的对象。

1、monitorenter,如果当前monitor的进入数为0时,线程就会进入monitor,并且把进入数+1,那么该线程就是monitor的拥有者(owner)。

2、如果该线程已经是monitor的拥有者,又重新进入,就会把进入数再次+1。也就是可重入性

3、monitorexit,执行monitorexit的线程必须是monitor的拥有者,指令执行后,monitor的进入数减1,如果减1后进入数为0,则该线程会退出monitor。其他被阻塞的线程就可以尝试去获取monitor的所有权。

总的来说,synchronized的底层原理是通过monitor对象来完成的

2、同步方法原理

synchronized修饰的实例方法:

public synchronized void hello(){
    System.out.println("hello world");
}

javap -v反编译:

可以看到多了一个标志位ACC_SYNCHRONIZED,作用就是一旦执行到这个方法时,就会先判断是否有标志位,如果有这个标志位,就会先尝试获取monitor,获取成功才能执行方法,方法执行完成后再释放monitor。在方法执行期间,其他线程都无法获取同一个monitor。归根结底还是对monitor对象的争夺。

synchronized锁的优化

JDK1.5之前,synchronized是属于重量级锁,重量级需要依赖于底层操作系统的 互斥锁(Mutex Lock)实现的,然后操作系统需要切换用户态和内核态,这种切换的消耗非常大,所以性能相对来说并不好。

JDK1.6后开始对synchronized进行优化,增加了锁消除锁粗化偏向锁轻量级锁自适应的CAS自旋这些优化策略。锁的等级从 无锁->偏向锁->轻量级锁->重量级锁 逐步升级,并且是单向的,不会出现锁的降级。

锁消除

锁消除是一种锁的优化策略,这种优化更加彻底,在JVM编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。 这种优化策略可以消除没有必要的锁,节省毫无意义的请求锁时间。比如StringBuffer的append()方法,就是使用synchronized进行加锁的。

public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

如果在实例方法中StringBuffer作为局部变量使用append()方法,StringBuffer是不可能存在共享资源竞争的,因此会自动将其锁消除。

public String add(String s1, String s2) {
    //sb属于不可能共享的资源,JVM会自动消除内部的锁
    StringBuffer sb = new StringBuffer();
    sb.append(s1).append(s2);
    return sb.toString();
}

锁粗化

如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。意思是将多个连续加锁、解锁的操作连接在一起,扩展成为一个范围更大的锁。

偏向锁

JDK的开发人员经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。也就是说在很多时候我们是假设有多线程的场景,但是实际上却是单线程的。所以偏向锁是在单线程执行代码块时使用的机制

原理是什么呢,我们前面提到锁的争夺实际上是Monitor对象的争夺,还有每个对象都有一个对象头,对象头是由Mark Word和Klass pointer 组成的。一旦有线程持有了这个锁对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中当同一个线程再次进入时,就不再进行同步操作,这样就省去了大量的锁申请的操作,从而提高了性能。

自适应的CAS自旋

上面说过,当线程没有获得monitor对象的所有权时,就会进入阻塞,当持有锁的线程释放了锁,当前线程才可以再去竞争锁,但是如果按照这样的规则,就会浪费大量的性能在阻塞和唤醒的切换上,特别是线程占用锁的时间很短的话。

为了避免阻塞和唤醒的切换,在没有获得锁的时候就不进入阻塞,而是不断地循环检测锁是否被释放,这就是自旋。在占用锁的时间短的情况下,自旋锁表现的性能是很高的。

但是,由于线程是一直在循环检测锁的状态,就会占用cpu资源,如果线程占用锁的时间比较长,那么自旋的次数就会变多,占用cpu时间变长导致性能变差。那自旋次数设置多少比较合适呢?JDK1.6引入了自适应的CAS自旋

自适应性自旋锁的意思是,自旋的次数不是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

轻量级锁

如果获取偏向锁失败,也就是有多个线程竞争锁的话,就会升级为JDK1.6引入的轻量级锁。

轻量级锁的获取流程:

  1. 首先判断当前对象是否处于一个无锁的状态,如果是,Java虚拟机将在当前线程的栈帧建立一个锁记录(Lock Record),用于存储对象目前的Mark Word的拷贝,如图所示。

  2. 将对象的Mark Word复制到栈帧中的Lock Record中,并将Lock Record中的owner指向当前对象,并使用CAS操作 将对象的Mark Word更新为指向Lock Record的指针,如图所示。

  • 如果第二步执行成功,表示该线程获得了这个对象的锁,将对象Mark Word中锁的标志位设置为“00”,执行同步代码块。
  • 如果第二步未执行成功,需要先判断当前对象的Mark Word是否指向当前线程的栈帧,如果是,表示当前线程已经持有了当前对象的锁,这是一次重入,直接执行同步代码块。如果不是表示多个线程存在竞争,该线程通过自旋尝试获得锁,即重复步骤2,自旋超过一定次数,轻量级锁升级为重量级锁,也就是把线程阻塞起来,等待唤醒。

轻量级锁的解锁:

  • 轻量级的解锁同样是通过CAS操作进行的,线程会通过CAS操作将Lock Record中的Mark Word替换回来。如果成功表示没有竞争发生,成功释放锁,恢复到无锁的状态;如果失败,表示当前锁存在竞争,升级为重量级锁。

一句话总结轻量级锁的原理:将对象的Mark Word复制到当前线程的Lock Record中,并将对象的Mark Word更新为指向Lock Record的指针。

重量级锁

重量级锁就是一个悲观锁了,但是其实不是最坏的锁,因为升级到重量级锁,是因为线程占用锁的时间长(自旋获取失败),锁竞争激烈的场景,在这种情况下,让线程进入阻塞状态,进入阻塞队列,能减少cpu消耗

小结

  • 偏向锁:适用于单线程执行。
  • 轻量级锁:适用于锁竞争较不激烈的情况。
  • 重量级锁:适用于锁竞争激烈的情况。

学习synchronized关键字的底层原理不是钻牛角尖,其实是从底层原理上知道了synchronized在什么场景使用会有什么样的效果,我们都知道没有最好的技术,只有最适合的技术,所以说在不同的场景使用最佳的解决方案才是最好的技术。

synchronized在不同的场景会自动选择不同的锁,这样一个升级锁的策略就体现出了这点。

参考文章:
synchronized底层原理是什么?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会叫的狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值