Synchronized关键字理解

2020-06-08  本文已影响0人  yaco

1 synchronized的三种应用方式

2 synchronized底层实现原理

理解synchronized底层实现原理之前,首先要理解一下Java对象头于Monitor监视器锁对象。

关于Java对象头和Monitor监视器对象的介绍见底部补充内容

有了上面的基础,这里主要从三个方面展开对synchronized底层实现原理进行分析

2.1 从Java字节码出发

synchronized关键字根据不同的使用方法,会有不同的字节码实现,但是最终得原理都是一样的,都是获取Monitor对象实现同步。

public class SynchronizedDemo2 {
    public synchronized void method() {
        System.out.println("synchronized 方法");
    }
}

​ 编译后的字节码文件为:

img

​ 可以看到修饰同步方法的时候,使用到了ACC_SYNCHRONIZED 标识,区别与同步代码块,JVM 通过该ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。底层都是获取Monitor监视器对象。

2.2 JVM实现锁升级的过程

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。

锁升级的详细过程如下:

3 Java虚拟机对synchronized的优化

synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。

[图片上传失败...(image-784773-1591625443207)]

首先大致了解一下每种锁存在的意义、

4 synchronized对锁的可重入性理解

public class Child extends Father{
    public static void main(String[] args) {
        new Child().doSomething();
        
    }
    
    public synchronized void doSomething(){
        System.out.println("child");
        super.doSomething();
    }
 
}
 
class Father{
    public synchronized void doSomething(){
        System.out.println("Father");
    }
}

首先执行main方法,会执行Child类中doSomething()方法,该方法会拿到child对象的锁。

然后执行super.doSomething()方法,又会重新获取一下实例对象的锁,注意,这里在两个类中的方法锁对象均为(new Child()),所以这里就发生了重入锁,不会造成死锁的情况。

5 释放锁的时机

  1. 当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。
  2. 当一个线程执行的代码出现异常时,其所持有的锁会自动释放

-------------------以下是补充内容--------------------------

6 Java对象的内存布局

参考 —— Java对象头详解

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。

img
1. 对象头形式

前面已经简单说了对象头中存储的内容,这里详细展开一下;JVM中对象头的方式有以下两种(以32位JVM为例):

|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|
|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|
2 对象头详解

Mark Word:

这部分主要用来存储对象自身的运行时数据,如hashCode、GC分代年龄等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下

|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|

这里以32位JVM为例,64位的情况下是一样的,32位JVM一个字是32为,64位JVM一个字是64为,JVM均用一个字的大小记录当前Mark Word中的信息。

Class_Metadata_Addresss:

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

注意: 这里在64位的JVM情况,可以出现指针压缩,将类型指针压缩成32位

array length:

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位


7 Monitor监视器锁

在上面对象头分析中,可以看到当对象头锁状态位重量级锁的时候,有一位ptr_to_heavyweight_monitor空间,它存放的就是monitor对象(也称为管理或监视器锁)的起始地址。

每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

这里我们主要关系几个量:

简单总结一下:


8 synchronized 关键字的具体使用——手写双重检验锁方式实现单例模式的原理

public class Singleton {
    
    // 这里必须要加volatile修饰,在多线程的情况下可能会发生指令重排,如果不加此修饰,可能会出现创        建对象的可能
    private volatile static Singleton singleton;
    
    // 私有化构造方法,只有他自己可以调用
    private Singleton(){
        
    }
    
    // 创建静态方法返回当前的对象实列
    public static Singleton getUniqueInstance(){
        if(singleton == null){
            // 如果检查到当前的singleton对象为空,则上锁创建新对象,其他线程不得入内
            synchronized (Singleton.class){
                // 双重判断,避免另一个线程在创建完对象之后获取了锁进入当前的同步代码块
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
}

参考:

面试官经常问的synchronized实现原理和锁升级过程,你真的了解吗

深入理解Java并发之synchronized实现原理

上一篇下一篇

猜你喜欢

热点阅读