Java synchronized 原理从开始到放弃详解编程语言

synchronized 到底是什么鬼东西

synchronized 在java中是同步机制的关键字,用来同步代码块或者同步方法,避免并发线程造成的问题。

自己在使用的过程中,一直存在很多疑问,自言自语,尝试解答一些问题来加深理解:

  1. 为什么说 synchronized 是重量级锁?
  2. synchronized 内部是怎么做到同步的?
  3. 为什么每个对象明明不是线程,却都有wait和notify方法?
  4. synchronized 怎么是可重入的?

synchronized 的常见使用

  1. 修饰方法:

    // 修饰普通方法 
    public synchronized void test1() {
          
     
    } 
    
  2. 修饰代码块或者修饰静态方法:

    class People {
          
    	// 修饰静态方法 
    	public synchronized static void test2() {
          
     
    	} 
    	// 修饰代码块 
    	public void test3() {
          
    		synchronize(instance) {
          
     
    		} 
    	} 
    } 
    

以上是我们经常使用synchonize的几种情景,按照常见的说法就是

修饰普通方法:锁的是当前的 this 对象
修饰静态方法:锁的是类锁,锁定该类的Class对象
修饰代码块:锁的是确定的对象,上面例子就是instance

也许你会有疑问,修饰静态方法时候怎么会无缘无故出现class对象?

回忆下类加载机制:当new一个对象的时候,类加载器会检查对应的类对象是否已经加载到方法区常量池中,否则它会创建一个类实例java.lang.Class这个类对象,它保存着类相关的类型信息,new对象的时候才会通过class对象实例化具体对象。

类锁实际上实现为对象锁,其实修饰静态方法跟我们使用 synchronize(People.class) 直接加锁People.class是一样的。

小结

synchronize锁住的是对象


两种使用方法底层的区别

  • 对于同步方法:执行的方法会被加上 ACC_SYNCHRONIZED 标记符,实现同步,被称为隐式同步。

  • 对于同步代码块或者静态方法:在起始位置加上monitorenter指令,和在结束位置monitorexit 指令,实现同步,被称为显式同步。

接下来的问题:ACC_SYNCHRONIZED、monitorenter和monitorexit是怎么实现同步?

  1. 每个对象有一个监视器锁(monitor),当monitor被占用时就会处于锁定状态。

  2. 每个对象有一个被锁定的计数器,未上锁的对象的计数是0,当线程第一次获得锁定时,计数增加到1。同一个线程再次获取锁也会加1,而线程释放锁的时候,计数就会减1,到计数为0时候,才算真正释放锁,可以有其他的线程使用这个锁。

  3. monitorenter、monitorexit:当线程执行 monitorenter 指令上锁,该被上锁的对象的计数器会加1,再次获取锁也会加1,当线程执行monitorexit指令,该对象的计数器会减1,直至计数器为0时,真正释放锁。

  4. ACC_SYNCHRONIZED:实现在方法调用和返回操作之间。当调用方法时,调用指令将会检查从方法常量池中的方法表的 ACC_SYNCHRONIZED 访问标志是否设置。
    如果设置了,线程调用方法时要先获取锁,最后在方法完成时释放锁。同样的底层也是通过 monitor 进行锁定和释放。

    在这里插入图片描述

接下来的问题:Java对象的锁是在哪里?怎么表示?还有monitor又是什么东西?


Java对象内存结构是怎么样的?

在说明对象的锁是什么之前,先看下Java对象内存结构。

JVM在HotSpot中采用了OOP-Klass内存模型,OOP(Ordinary Object Pointer)指的是普通对象指针,Klass用来描述对象实例的具体类型,简单来说OOP表示的是对象的实例数据,Klass表示的是对象的类型信息。

在这里插入图片描述

OOP

OOP 其实也就是OopDesc,被称为对象头,JVM每次创建一个对象就会创建对应的OOP对象。
有instanceOopDesc 用于描述类对象,arrayOopDesc对象用于描述数组类型。

oopDesc中包含两个数据成员:markOop _mark 和 Union _metadata

  • _mark 是Mark Word;
  • _metadata是元数据指针,是一个联合体,_klass是普通指针,_compressed_klass是压缩类指针,都指向instanceKlass对象,用来描述对象的具体类型。

JVM为创建对象,在堆里面分配内存,对象在堆中的内存布局有三部分:对象头、实例数据、对齐填充。

在这里插入图片描述

Klass

Klass表示元数据,一个 Klass 对象代表一个类的元数据,类加载的时候会创建一个 instanceKlass 对象表示这个类的运行时的数据

在这里插入图片描述

小结
  1. Java虚拟机栈中会存对象实例的引用
  2. Java堆中有对象实例的对象头instanceOopDesc,instanceOopDesc包括Mark Word和元数据指针,元数据指针指向instanceKlass对象
  3. Java方法区存着 instanceKlass 对象元数据,描述类的具体类型

Mark Word

Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位。
其中的锁标记位跟Synchronized息息相关,看一下Mark Word是什么样子的?

在这里插入图片描述

32位的机器下,markword总共有32位,但是在不同的状态下,每个区间表示的含义都不太相同,我们主要看的是2bit锁标志位,分别是无锁、偏向锁、轻量级锁、重量锁状态,以及线程信息。

其中偏向锁和轻量级锁都是通过自旋等技术避免真正的加锁,而重量级锁才是获取锁和释放锁,重量级锁指向的指针就是线程监视器 monitor。

小结:

mark word记录着锁状态和线程信息


monitor是什么东西?

monitor是一种同步机制,使得多线程并发下,只能由一个线程进入临界区,是Synchronized实现的关键,底层是C++ 的 Thread Mutex实现。上面的monitorenter、monitorexit、ACC_SYNCHRONIZED都跟
在这里插入图片描述

上图就是monitor的工作原理的例子,每个线程都要先进入Entry Set集合中等待,调度器保证了只有一个线程成为The Owner,也就是monitor的拥有者,而挂起的线程会进入Wait Set中等待。

接下来看一下 monitor的具体实现ObjectMonitor的数据结构,会发现很多应对我们上面的东西:

ObjectMonitor::ObjectMonitor() {
    
  _header       = NULL; //markOop对象头 
  _count        = 0;  // 重入次数 
  _waiters      = 0,  
  _recursions   = 0; 
  _object       = NULL;  
  _owner        = NULL ;// 获得锁的线程 
  _WaitSet      = NULL; // 挂起的线程列表 
  _WaitSetLock  = 0 ; 
  _Responsible  = NULL ; 
  _succ         = NULL ; 
  _cxq          = NULL ; 
  FreeNext      = NULL ; 
  _EntryList    = NULL ; // 处于等待获取锁线程列表 
  _SpinFreq     = 0 ; 
  _SpinClock    = 0 ; 
  OwnerIsThread = 0 ; //上一个拥有者的线程 
} 
  1. 等待锁的线程都会被封装成 ObjectWaiter 对象会被存在_EntryList 中,挂起状态的被保存_EntryList
  2. 线程获取ObjectMonitor 成功后,把该对象的ObjectMonitor的_owner 会设置为获取锁的线程 ,_count 加 1;
  3. 获取ObjectMonitor的线程被wait挂起,会进入_EntryList,释放ObjectMonitor,_count 减1,同样执行完推出也会释放ObjectMonitor,_count 减1。
  4. 整体历程:执行 monitorenter 指令,尝试获取对象的 monitor,只有一个线程可以和 monitor 获取,如果线程加锁失败,进入entry队列进行等待,如果线程加锁成功,到monitorexit指令释放monitor。

为什么对象都有notify和wait方法?

回到最原来的问题,现在可以回答为什么有notify和wait方法?

每个对象都有一个monitor,那么当前线程通过调用对象的notify和wait,也就执行monitor释放和获取操作了,而且都需要在获得monitor的前提下。

  1. Object的wait方法调用的就是objectMonitor的wait方法,会将当前获得monitor的线程封装ObjectWaiter 进入到_WaitSet中,调用ObjectMonitor的exit 释放锁。

  2. 同样的,Object的notify方法也是调用objectMonitor的notify方法,取出_WaitSet 的 ObjectWaiter 第一个节点加入_EntryList,但是当前线程并没有释放monitor。


为什么说synchronize是重量级锁

synchronized 调用到了ObjectMonitor的enter和exit操作,依赖到操作系统,而java的线程很多映射的是操作系统的轻量级进程,往往会引起系统内核态和用户态的切换,所以说synchronize是重量级锁


偏向锁、轻量级锁怎么做到的?

Synchronize是重量级锁,所以引入了很多技术优化,减少真正的加锁操作。
偏向锁、轻量级锁都跟自旋、CAS、Mark Word有关

  1. 偏向锁是乐观锁,意思是除了我不会有其他线程跟我竞争,标记在MarkWord的状态位上。
  2. 如果线程在MarkWord发现有其他锁竞争,则偏向锁会膨胀成轻量级锁,轻量级锁也是乐观锁,意思是不会有其他线程跟我在同一时间竞争锁,会尝试将Mark Word上通过CAS操作更新成线程栈的锁记录
  3. 如果锁记录更新失败则会膨胀成重量级锁

锁状态是 从偏向锁 -> 轻量级锁 -> 重量级锁 方向膨胀的,不能往后倒退。

这部分具体的以后在研究。


结论:

不管你有没有放弃,反正我要放弃了

参考文章

IT虾米网
IT虾米网
IT虾米网

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/19401.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论