此文只是记录了个人对Synchronized的了解和认知,基本上都是参考网上文献,在了解的过程中对Synchronized的原理上产生一些疑问,经过多番查阅网上并没有比较准确或者相对权威的参考资料,疑问如下:
1.多线程在竞争琐时是先进入contentionList还是entryList?两者是什么关系?
2.一个线程释放锁时,是怎么通知阻塞的线程来竞争锁的?是如何竞争的?
3.执行完任务且释放锁的线程会被放到WaitList吗?为什么?不会被回收吗?
有知道的大佬希望能解答一下~
下面是个人对Synchronized的理解:
先了解一下几个概念
原子性
是指一个操作或多个操作要么全部执行,且执行的过程不会被任何因素打断,要么就都不执行。不可中断的一个或一系列操作。
可见性
指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
通过volatile关键字修饰内存中的变量,该变量在线程之间共享。是轻量级的锁(synchronized),消耗的成本比synchronized小很多。volatile用于修饰变量。
如何保证可见性?
对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
有序性
即程序执行的顺序按照代码的先后顺序执行。
如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的
Synchronized的使用
修饰实例方法:锁是当前实例对象。
修饰静态方法:锁是当前类的class对象。
修饰代码块:锁是括号中的对象。
Java 对象存储在内存中,分为三个部分,即对象头、实例数据和对齐填充,而在其对象头中,保存了锁标识。Synchronized的实现依赖于下对象头:
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
1.Mark Word(标记字段)
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等;
Mark Word被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,处于不同的状态,里面的结构也有不同。
锁标志位 10 对应的是重量级锁,其指针指向的是 Monitor 对象(管程)的起始地址。
Monitor(管程)Synchronized实现的关键
Monitor是一种同步工具或机制,通常被描述成一个对象,由
ObjectMonitor.hpp文件,C++实现的。
ObjectMonitor中关键属性:
- _owner: 指向持有ObjectMonitor对象的线程
- _WaitSet: 存放处于wait状态的线程队列
- _EntryList: 存放处于等待锁block状态的线程队列
- _recursions:记录owner线程获取锁的次数,这也决定了synchronized是可重入的。
- _count: 一种说法是当前线程获取锁的次数,持有_count+1,释放_count-1;另一种说法是没有获取到锁的线程数量和调用Sleep方法的线程数总和。
synchronized实现原理
JVM基于进入和退出monitor对象来实现同步,同步代码块采用添加moniterenter、moniterexit,同步方法使用ACC_SYNCHRONIZED标记符隐式实现。每个对象都有一个monitor与之关联,运行到moniterenter时尝试获取对应monitor的所有权,获取成功就将monitor的进入数加1(所以是可重入锁,也被称为重量级锁),否则就阻塞,拥有monitor的线程运行到moniterexit时进入数减1,为0时释放monitor。
java中每个对象都有一个对象头,synchronized所用的锁就是存在对象头里的。如果是非数组的对象是8个字节(32位JVM)或者16字节(64位JVM),数组对象还会有一个数组长度(4个字节)
当多个线程同时访问一段同步代码时候,首先会进入_EntryList,当某个线程获取到Monitor后,会把_owner变量设置为当前线程,同时_count计数器加1,既获得对象锁。
若持有Monitor的线程调用wait()方法或执行完毕,将释放当前持有的Monitor,_owner变量设为null,_count计数器减1,同时该线程进入_WaitSet集合中等待被唤醒。
为什么说Synchronized是java语言中一个重量级操作?
Java 线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统帮忙,这就要从用户态转换到核心态,状态的转换需要花费处理器很多时间来处理,可能比用户代码执行时间还要长,所以说Synchronized是java语言中一个重量级操作。
2.Klass Point(类型指针)
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:
每个Class的属性指针(即静态变量)
每个对象的属性指针(即对象变量)
普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
3.array length
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。
为什么“wait/notify”方法必须要在synchronized的方法或者段中调用呢?
因为只有进入了synchronized方法或者方法段中的线程才拥有了该锁对象,只有拥有了该锁对象才能调用该锁对象的wait和notify方法。
此文只是记录了个人对Synchronized的了解和认知,基本上都是参考网上文献,在了解的过程中对Synchronized的原理上产生一些疑问,经过多番查阅网上并没有比较准确或者相对权威的参考资料,疑问如下:
1.多线程在竞争琐时是先进入contentionList还是entryList?两者是什么关系?
2.一个线程释放锁时,是怎么通知阻塞的线程来竞争锁的?是如何竞争的?
3.执行完任务且释放锁的线程会被放到WaitList吗?为什么?不会被回收吗?
有知道的大佬希望能解答一下~