synchronized 到底是什么鬼东西
synchronized 在java中是同步机制的关键字,用来同步代码块或者同步方法,避免并发线程造成的问题。
自己在使用的过程中,一直存在很多疑问,自言自语,尝试解答一些问题来加深理解:
- 为什么说 synchronized 是重量级锁?
- synchronized 内部是怎么做到同步的?
- 为什么每个对象明明不是线程,却都有wait和notify方法?
- synchronized 怎么是可重入的?
synchronized 的常见使用
-
修饰方法:
// 修饰普通方法 public synchronized void test1() { }
-
修饰代码块或者修饰静态方法:
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是怎么实现同步?
-
每个对象有一个监视器锁(monitor),当monitor被占用时就会处于锁定状态。
-
每个对象有一个被锁定的计数器,未上锁的对象的计数是0,当线程第一次获得锁定时,计数增加到1。同一个线程再次获取锁也会加1,而线程释放锁的时候,计数就会减1,到计数为0时候,才算真正释放锁,可以有其他的线程使用这个锁。
-
monitorenter、monitorexit:当线程执行 monitorenter 指令上锁,该被上锁的对象的计数器会加1,再次获取锁也会加1,当线程执行monitorexit指令,该对象的计数器会减1,直至计数器为0时,真正释放锁。
-
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 对象表示这个类的运行时的数据
小结
- Java虚拟机栈中会存对象实例的引用
- Java堆中有对象实例的对象头instanceOopDesc,instanceOopDesc包括Mark Word和元数据指针,元数据指针指向instanceKlass对象
- 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 ; //上一个拥有者的线程
}
- 等待锁的线程都会被封装成 ObjectWaiter 对象会被存在_EntryList 中,挂起状态的被保存_EntryList
- 线程获取ObjectMonitor 成功后,把该对象的ObjectMonitor的_owner 会设置为获取锁的线程 ,_count 加 1;
- 获取ObjectMonitor的线程被wait挂起,会进入_EntryList,释放ObjectMonitor,_count 减1,同样执行完推出也会释放ObjectMonitor,_count 减1。
- 整体历程:执行 monitorenter 指令,尝试获取对象的 monitor,只有一个线程可以和 monitor 获取,如果线程加锁失败,进入entry队列进行等待,如果线程加锁成功,到monitorexit指令释放monitor。
为什么对象都有notify和wait方法?
回到最原来的问题,现在可以回答为什么有notify和wait方法?
每个对象都有一个monitor,那么当前线程通过调用对象的notify和wait,也就执行monitor释放和获取操作了,而且都需要在获得monitor的前提下。
-
Object的wait方法调用的就是objectMonitor的wait方法,会将当前获得monitor的线程封装ObjectWaiter 进入到_WaitSet中,调用ObjectMonitor的exit 释放锁。
-
同样的,Object的notify方法也是调用objectMonitor的notify方法,取出_WaitSet 的 ObjectWaiter 第一个节点加入_EntryList,但是当前线程并没有释放monitor。
为什么说synchronize是重量级锁
synchronized 调用到了ObjectMonitor的enter和exit操作,依赖到操作系统,而java的线程很多映射的是操作系统的轻量级进程,往往会引起系统内核态和用户态的切换,所以说synchronize是重量级锁
偏向锁、轻量级锁怎么做到的?
Synchronize是重量级锁,所以引入了很多技术优化,减少真正的加锁操作。
偏向锁、轻量级锁都跟自旋、CAS、Mark Word有关
- 偏向锁是乐观锁,意思是除了我不会有其他线程跟我竞争,标记在MarkWord的状态位上。
- 如果线程在MarkWord发现有其他锁竞争,则偏向锁会膨胀成轻量级锁,轻量级锁也是乐观锁,意思是不会有其他线程跟我在同一时间竞争锁,会尝试将Mark Word上通过CAS操作更新成线程栈的锁记录
- 如果锁记录更新失败则会膨胀成重量级锁
锁状态是 从偏向锁 -> 轻量级锁 -> 重量级锁 方向膨胀的,不能往后倒退。
这部分具体的以后在研究。
结论:
不管你有没有放弃,反正我要放弃了
参考文章
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/19401.html