一文搞懂“synchronized的用法和底层实现”

本文详细介绍了Java中`synchronized`关键字的用法、内存语义、底层原理,以及Java虚拟机对其的优化策略,包括锁升级、偏向锁、轻量级锁和自旋锁,同时讨论了`synchronized`的可重入性和不可中断特性。
摘要由CSDN通过智能技术生成

synchronized关键字介绍

synchronized块是java提供的一种原子性内置锁,java中的每个对象都可以把它当作一个同步锁来使用。

synchronized的三种应用方式

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

  • 修饰代码块,指定加锁对象,对给定对象(类或者指定对象)加锁,进入同步代码库前要获得给定对象的锁。

synchronized的内存语义

synchronized可以解决共享变量内存可见性问题。

进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的语义就是把在synchronized块内对共享变量的修改刷新到主内存。

synchronized的底层原理

修饰代码块

synchronized 同步语句块使用的是 monitorenter(对应JMM模型lock指令) 和 monitorexit (unlock)指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。
monitor指令

修饰方法

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,有synchronized标识指明了该方法是一个同步方法。如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。

在这里插入图片描述

总结:本质都是对对象监视器monitor的获取

加锁时,对锁计数器+1;释放锁时,对锁计数器-1;锁计数器为0时,表示可以获取该锁。

Java虚拟机对synchronized的优化

在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。

在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,接下来我们将简单了解一下Java官方在JVM层面对synchronized锁的优化。

锁升级

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

在这里插入图片描述

偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。

偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时, 无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

所以,对于没有锁竞争 的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。

锁对比

优点缺点适用场景
偏向锁加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距若线程间存在锁竞争,会带来额外的锁撤销的消耗只有一个线程访问同步块或者同步方法
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度若线程长时间竞争不到锁,自旋会消耗 CPU 性能线程交替执行同步块或者同步方法,追求响应时间,锁占用时间很短,阻塞还不如自旋的场景
重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗追求吞吐量,锁占用时间较长

synchronized特性

可重入

synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁啦,在执行完同步代码块时,计数器的数量会-1,知道计数器的数量为0,就释放这个锁。

不可中断

一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。

synchronized不可以被中断,指的是synchronized等待不可中断,处于阻塞状态的线程会一直等待锁。比如:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class InterruptTest {

    public synchronized void foo1() {
        System.out.println("foo1 begin");
        for (int i =0; i < 5; ++i) {
            System.out.println("foo1 ...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("foo1 sleep is interrupted, msg=" + e.getMessage());
            }
        }
    }

    public synchronized void foo2() throws InterruptedException {
        System.out.println("foo2 begin");
        for (int i =0; i < 100; ++i) {
            System.out.println("foo2 ...");
            Thread.sleep(1000);
        }
    }

    public static void main(String[] args) {
        InterruptTest it = new InterruptTest();
        ExecutorService es = Executors.newCachedThreadPool();
        es.execute(() -> it.foo1());
        es.execute(() -> {
            try {
                it.foo2();
            } catch (InterruptedException e) {
                System.out.println("foo2 is interrupted, msg=" + e.getMessage());
            }
        });
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        es.shutdownNow();
        System.out.println("Main end");
    }
}

foo2的synchronized在等待foo1时不可被中断,只有在foo2拿到锁之后才可被中断,执行结果为:

foo1 begin
foo1 ...
foo1 ...
foo1 ...
Main end
foo1 sleep is interrupted, msg=sleep interrupted
foo1 ...
foo1 ...
foo2 begin
foo2 ...
foo2 is interrupted, msg=sleep interrupted

  • 个人公众号
    个人公众号
  • 个人小游戏
    个人小游戏
  • 23
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

会飞的大鱼人

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

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

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

打赏作者

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

抵扣说明:

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

余额充值