理解java关键字Synchronized(学习笔记)

之前学习了线程的一些相关知识,今天系统的总结下来

目录

1. Java对象在堆内存中的存储结构

2. Monitor管程

3. synchronized锁的状态变换以及优化

4. synchronized的同步性和可见性

5. jvm调优参数设置

6. 总结

1.Java对象在堆内存中的存储结构

要想明白synchronized,必须先搞清楚Java对象在堆中的内存区域

实例数据存放类的属性信息及其父类的属性信息,在JVM分配策略的影响下,相同的宽度的字段会被分配到一起

(longs和doubles宽度相同,shorts和chars宽度相同等),而且子类的数据可能会插入到父类的字段间隙。

填充数据:填充补齐的作用,HotSpot要求对象所占空间的大小必须为8的整数倍,

若对象的实例数据以及对象头所占空间大小已经是8字节的整数倍,则该区域不会存在,否则补全。

对象头:该区域是我们的重点,它主要分为两个部分:运行时数据区MarkWord和类型指针以及数组长度(不一定存在)。

一.   运行时数据区保存了运行时对象的信息:HashCode,GC分代,GC标志,锁状态标志以及线程持有的锁,偏向线程ID等信息。

二.   类型指针就是指向类的元数据指针,在Hotspot中该指针指向对象的类对象数据区(jdk1.8中方法区已经被替代成元数据区)。

三.   数组长度,若该对象为数组,还要保存数组的长度信息。

我们的重点是MarkWord数据区,该区域的数据结构不是固定的,一般大小为32bit/32位,64bit/64位。

下图为64位下的MarkWord存储结构。

偏向锁标识位

锁标识位

锁状态

存储内容

0

01

未锁定

hash code(31),年龄(4)

1

01

偏向锁

线程ID(54),时间戳(2),年龄(4)

00

轻量级锁

栈中锁记录的指针(64)

10

重量级锁

monitor的指针(64)

11

GC标记

空,不需要记录信息

2.Monitor管程

Synchronized在不同的情境下有四种状态:无锁,偏向锁,轻量级锁,重量级锁。而四种方式的实现主要依靠monitor对象。Monitor是对象天生自带的一个对象,它有多种实现方式,其中一个为对象创建同时也创建了monitor(也可能是在线程持有该锁时创建),若一个线程持有了该对象的锁,那么该对象的monitor对象状态为锁定状态。

在openjdk中查看objectmonitor.hpp的源码部分如下

ObjectMonitor() {
         _header       = NULL;
         _count        = 0;
         _waiters      = 0,//等待线程数
         _recursions   = 0;//重入次数
         _object       = NULL;//寄生的对象。
         _owner        = NULL;//指向获得ObjectMonitor对象的线程
         _WaitSet      = NULL;//处于wait状态的线程,会被加入到wait set;
         _WaitSetLock  = 0 ;
         _Responsible  = NULL ;
         _succ         = NULL ;
         _cxq          = NULL ;
         FreeNext      = NULL ;
         _EntryList    = NULL ;//处于等待锁block状态的线程,会被加入到entry list;
         _SpinFreq     = 0 ;
         _SpinClock    = 0 ;
         OwnerIsThread = 0 ;
         previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
         }

 

对象锁的线程状态被记录在该对象中。

我们只看对象封装的几个重要字段:

_owner记录当前获取该对象锁的线程。

_WaitSet:当前正在等待获取该锁处于阻塞状态的线程集合(Set保证等待中的线程不可重复)

_EntryList: 当前处于等待状态的线程处于该集合中。

_count: 记录了持有该锁的线程数,若一个线程获取了该对象锁,则计数器+1,执行wait方法后该对象减1,

同时 _owner对象被置位NULL,代表此时没有线程持有该锁。

3.synchronized锁的状态变换以及优化

在jdk1.6之后,java的锁就进行了一系列的优化以解决资源抢占以及程序执行效率问题。

锁的膨胀方向:无锁->偏向锁->轻量级锁->重量级锁

锁状态的详情如下:

无锁:共享数据 没有没任何线程所占用。

偏向锁:大部分情况下,锁不存在竞争,总是由同一线程多次获取。当锁在第一次被线程所获取的时候,标志位变为01(偏向模式),同时进行CAS操作获取当前线程的id记录到markword中,若CAS操作成功,那么线程在此进入同步代码块且线程id已经和markword中的一致时,则不需要任何同步操作(locking,unlocking,update等),注意是不会有任何同步操作。若当有另一个线程尝试获取该锁时,则宣布偏向模式结束,锁状态将会恢复为01(为锁定)或00(轻量级锁)。

 

轻量级锁:在方法进入同步方法时,若此时同步方法没有被锁定,那么(标志位01),虚拟机会在当前线程所在方法的栈帧中开辟一个空间用来保存锁记录(Lock Record),用于存储当前锁所在的对象的MarkWord的拷贝,在抢占资源时,jvm会进行CAS操作进行失败重试让当前锁所在对象的MarkWord更新为指向Lock Record的指针,若更新成功,代表抢占锁成功,MarkWord变为00,表示处于轻量级锁状态。若更新失败,jvm会先检查当前锁所在对象的MarkWord是否已经指向了线程的栈帧,若已经指向,则说明锁已经被当前线程所持有,那么久可以进入同步块;若没有指向当前线程的栈帧,就说明有至少两个线程在抢占同一把锁,则该锁会膨胀为重量级锁。锁标志位变为10,后面抢占的线程若没有获取到该锁就得进入阻塞状态了。

 

偏向锁和轻量级锁的区别:两中状态非常相似,但在无竞争的情况下却有区别:轻量级锁在无竞争情况下是通过CAS操作进行失败重试来消除锁,而偏向锁在无竞争情况下直接取消了CAS。

 

自旋锁(jdk1.4引入): 在只有两个线程争抢同一把锁的情境下,在线程A试图进入同步方法或者同步代码块时,若该同步方法或同步代码块已经被线程B抢先进入,那么此时线程A需要挂起,直到其他线程B执行完才能恢复继续进行锁的抢占,但是大部分情况下锁被某个线程持有只持续很短的时间(单次持有锁到释放锁所消耗的时间),所以线程A在很短的时间内挂起再恢复是不值得的,于是就让线程A在B持有锁之后不释放CPU资源,而是继续循环等待锁,直到获取到锁,称为自旋。适用于线程单次持有锁到释放同一把锁所消耗的时间很短的情况,否则其他线程长时间循环等待锁,不释放cpu资源只会更加消耗资源。

 

自适应自旋锁(jdk1.6引入):为了解决自旋锁所带来的可能出现的问题,此时一把锁的在等待其他线程释放锁时,自旋的次数由前一次上持有该锁的自旋时间以及当前持有该锁的线程的状态来决定。

锁的去除优化

在JIT编译时,jvm会进行扫描,去除不可能存在竞争情况的锁。看一个例子:

public class Sync{
    private String name;
    
    public Sync(String name){
        this.name = name;
    }
  public synchronized String changeName(String name){
        this.name = name;
        return name;
   }  
}
Public class Test{
    Public void test(String name){
        Sync sync = new Sync();
        Sync.changeName(“John”);
    }

public static void mian(String[] args){
        Test test = new Test();
        for(int i = 0; i < 100; i++){
                test.test(“Jack”);
        }
    }
}
                                

在上边这个例子中,jvm会进行JIT优化,去除synchronized锁,因为sycn对象生存周期始终在java虚拟机栈中,不可能存在锁竞争的情况。

锁的去除优化

public static String test2(String name){
Sync sync = new Sync(“Tom”);
    for(int i = 0 ; i< 100 ; i++){
        sync.changeName(name);
    }
}

在上边这个例子中,由于changeName是同步方法,在这个for循环中,每次进入和退出循环都要进行lock和unlock操作,但是这是没有必要的操作,于是jvm会将synchronized加到循环的外边,只进行一次lock和unlock操作。

4.synchronized的同步性和可见性

同步性:synchronized修饰的方法或者代码块只允许一个线程进入,只有当该线程退出同步区域时,才允许下一个线程进入。

可见性:当线程释放锁是,当前线程会把本地内存中的共享变量立即刷新到主内存中。保证其他线程获取该共享变量的锁时,获取到的是共享变量的最新值。而当线程获取锁时,当线程对共享变量的拷贝会被置为无效,强制当前线程的共享变量是从主内存中拿到的最新值,从而保证可见性。

5.jvm调优参数设置

自旋锁:-XX:PreBlockSpin 来更改自旋的次数,默认为自旋10次。由于jdk1.6只有加入了自适应的自旋锁,自旋锁会自己判断自旋锁的自旋次数,更智能。

偏向锁:-XX:UseBiasedLocking 来设置是否启用偏向锁。-XX:BiasedLockingStartupDelay 来设置java程序启动后延迟开启偏向锁的时间

6.总结

通过synchronized的底层原理可以了解到synchronized是如何保证同步和共享变量的,以及在具体到代码层面时,jvm又是如何进行进行优化的,以及在不同的场景下如何进行参数调优。

阅读参考书籍《深入理解jvm》

原文地址:https://www.cnblogs.com/xingwenchao/p/10551104.html