面试官: 有了解过Synchronized吗 说说看

简介: 面试官: 有了解过Synchronized吗 说说看

前言

目前正在出一个Java多线程专题长期系列教程,从入门到进阶含源码解读, 篇幅会较多, 喜欢的话,给个关注❤️ ~ 本篇内容纯理论一点


相信很多同学对synchronized的使用上不陌生,之前也给大家讲解过它的使用。本篇主要带大家深入了解一下它,大家也可以自己试着总结一下,这也是面试中常常问到的,单纯的回答它的基本使用,是惊艳不到面试官的~


synchronized 介绍

从字面意思翻译过来就是同步的意思,所以它也叫同步锁,我们通常会给某个方法或者某块代码加上Synchronized锁来解决多线程中并发带来的问题,它也是最常用,最简单的一种方法


在Java中,基本上都是基于对象而言的,所以又称为对象锁, 一个类通常只有一个class对象和n个实例对象,它们共享class对象,而我们有时候会对class对象加锁,所以又称为class对象锁


这里大家要注意的是对象需要是一个非null的对象,我们通常也叫做对象监视器(Object Monitor)


重量级锁

JDK 1.5之前,它是一个重量级锁,我们通常都会使用它来保证线程同步。在1.5的时候还提供了一个Lock接口来实现同步锁的功能,我们只需要显式的获取锁和释放锁。

重在哪❓


在1.5的时候,Synchronized它依赖于操作系统底层的Mutex Lock实现,每次释放锁和获取锁都会导致用户态和内核态的切换,从而增加系统性能的开销,当出现大并发的情况下,锁竞争会比较激烈,性能显得非常糟糕,所以称为重量级锁,所以大家往往会选择Lock锁。


锁优化

但是Synchronized又是那么的简单好用,又是官方自带的,怎么可能放弃呢?所以在1.6之后,引入了大量的锁优化,比如自旋锁轻量级锁, 偏向锁等,下面我们逐个看一下~


synchronized 实现原理

我们了解优化之前,我们先看一下它的实现原理。


首先我们看下同步块中,因为它是关键字,我们看不到源码实现,所以只能反编译看一下,通过 javap -v **.class

public static void main(String[] args) {
        synchronized(Demo.class) {
            System.out.println("hello");
        }
    }
复制代码


public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
        stack=2, locals=3, args_size=1
        0: ldc           #2                  // class com/thread/base/Demo
        2: dup
        3: astore_1
        4: monitorenter
        5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        8: ldc           #4                  // String hello
        10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return
复制代码


我们重点关注monitorentermonitorexit,那么他俩是什么意思呢❓

monitorenter,如果当前 monitor 的进入数为 0 时,线程就会进入 monitor,并且把进入数 + 1,那么该线程就是 monitor 的拥有者 (owner)。如果该线程已经是 monitor 的拥有者,又重新进入,就会把进入数再次 + 1。也就是可重入。


monitorexit,执行 monitorexit 的线程必须是 monitor 的拥有者,指令执行后,monitor 的进入数减 1,如果减 1 后进入数为 0,则该线程会退出 monitor。其他被阻塞的线程就可以尝试去获取 monitor 的所有权。指令出现了两次,第 1 次为同步正常退出释放锁;第2次为发生异步退出释放锁;


我们再来看一下, 修饰实例方法中的表现:

class Demo {
    public synchronized void hello() {
        System.out.println("hello");
    }
}
复制代码


public synchronized void hello();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 25: 0
        line 26: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/thread/base/Demo;
}
复制代码


我们重点关注ACC_SYNCHRONIZED,它作用就是一旦执行到这个方法时,就会先判断是否有标志位,如果有,就会先尝试获取 monitor,获取成功才能执行方法,方法执行完成后再释放 monitor。在方法执行期间,其他线程都无法获取同一个 monitor。归根结底还是对 monitor 对象的争夺,只是同步方法是一种隐式的方式来实现。


synchronized 在 JVM 里的实现就是基于进入和退出 monitor 来实现的,底层则是通过成对的 MonitorEnter 和 MonitorExit 指令来实现


有了以上的认识,下面我们就看看锁优化


Synchronized中的锁优化

自适应自旋锁

自旋锁,之前我们讲FutureTask源码的时候,有一个内部方法awaitDone(),给大家有介绍过,就是基于它实现的,今天再给大家总结一下。


它的目的是为了避免阻塞和唤醒的切换,在没有获得锁的时候就不进入阻塞,不断地循环检测锁是否被释放。但是,它也有弊端,我们通常来讲,一个线程占用的时间相对较短,但是万一占用很长时间怎么办?这样会占用大量cpu时间,这样会导致性能变差,所以在1.6引入了自适应自旋锁来满足这样的场景。


那么什么是自适应自旋锁呢❓自旋的次数不是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果此次自旋成功了,很有可能下一次也能成功,于是允许自旋的次数就会更多,反过来说,如果很少有线程能够自旋成功,很有可能下一次也是失败,则自旋次数就更少。这样一来,就能够更好的利用系统资源。


锁消除

锁消除是一种锁的优化策略,这种优化更加彻底,在 JVM 编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。这种优化策略可以消除没有必要的锁,去除获取锁的时间。


锁粗化

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


偏向锁

偏向锁是JDK 1.6引入的,它解决的场景是什么呢❓我们大部分使用锁都是解决多线程场景下的问题,但有时候往往一个线程也会存在这样的问题,偏向锁是在单线程执行代码块时使用的机制。


锁的争夺实际上是 Monitor 对象的争夺,还有每个对象都有一个对象头,对象头是由 Mark Word 和 Klass pointer 组成的。一旦有线程持有了这个锁对象,标志位修改为 1,就进入偏向模式,同时会把这个线程的 ID 记录在对象的 Mark Word 中,当同一个线程再次进入时,就不再进行同步操作,大大减少了锁获取的时间,从而提高了性能。


轻量级锁

我们上边提到的偏向锁,在多线程情况下如果偏向锁失败就会升级为轻量级锁, Mark Word 的结构也变为轻量级锁的结构。


执行同步代码块之前,JVM 会在线程的栈帧中创建一个锁记录(Lock Record),并将 Mark Word 拷贝复制到锁记录中。然后尝试通过 CAS 操作将 Mark Word 中的锁记录的指针,指向创建的 Lock Record。如果成功表示获取锁状态成功,如果失败,则进入自旋获取锁状态。


如果自旋锁失败,就会升级为重量级锁,也就是我们之前讲的,会把线程阻塞,需等待唤醒。


重量级锁

它又称为悲观锁, 升级到这种情况下,锁竞争比较激烈,占用时间也比较长,为了减少cpu的消耗,会将线程阻塞,进入阻塞队列。


synchronized就是通过锁升级策略来适应不同的场景,所以现在synchronized被优化的很好,也是我们项目中往往都会使用它的理由。


结束语

本节的内容比较多,大家好好理解,特别是的升级策略。本节我们提到了Lock锁,下一节,带大家深入学习一下Java的Lock ~

相关文章
|
6月前
|
存储 Java 开发者
面试官:小伙子知道synchronized的优化过程吗?我:嘚吧嘚吧嘚,面试官:出去!
面试官:小伙子知道synchronized的优化过程吗?我:嘚吧嘚吧嘚,面试官:出去!
72 1
|
6月前
|
Java
【面试问题】Synchronized 和 ReentrantLock 区别?
【1月更文挑战第27天】【面试问题】Synchronized 和 ReentrantLock 区别?
|
10天前
|
存储 安全 Java
面试高频:Synchronized 原理,建议收藏备用 !
本文详解Synchronized原理,包括其作用、使用方式、底层实现及锁升级机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
面试高频:Synchronized 原理,建议收藏备用 !
|
4月前
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
46 0
|
1月前
|
存储 安全 Java
面试题:再谈Synchronized实现原理!
面试题:再谈Synchronized实现原理!
|
3月前
|
存储 安全 Java
【多线程面试题十七】、如果不使用synchronized和Lock,如何保证线程安全?
这篇文章探讨了在不使用`synchronized`和`Lock`的情况下保证线程安全的方法,包括使用`volatile`关键字、原子变量、线程本地存储(`ThreadLocal`)以及设计不可变对象。
|
3月前
|
Java
【多线程面试题十五】、synchronized可以修饰静态方法和静态代码块吗?
这篇文章讨论了Java中的`synchronized`关键字是否可以修饰静态方法和静态代码块,指出`synchronized`可以修饰静态方法,创建一个类全局锁,但不能修饰静态代码块。
|
3月前
|
Java 调度
【多线程面试题十四】、说一说synchronized的底层实现原理
这篇文章解释了Java中的`synchronized`关键字的底层实现原理,包括它在代码块和方法同步中的实现方式,以及通过`monitorenter`和`monitorexit`指令以及`ACC_SYNCHRONIZED`访问标志来控制线程同步和锁的获取与释放。
|
3月前
|
Java
【多线程面试题十三】、说一说synchronized与Lock的区别
这篇文章讨论了Java中`synchronized`和`Lock`接口在多线程编程中的区别,包括它们在实现、使用、锁的释放、超时设置、锁状态查询以及锁的属性等方面的不同点。
|
4月前
|
安全 Java
Java面试题:解释synchronized关键字在Java内存模型中的语义
Java面试题:解释synchronized关键字在Java内存模型中的语义
46 1