Synchronized 学习总结
1.用法
A.synchronized 修饰普通的方法,此时锁的是当前实例的对象
B.修饰静态方法
public static synchronized methodC() {}//对类加锁,即对所有此类的对象加锁
C.synchronized修饰代码块: 锁定的是括号内的对象
public void synMethod0() {
synchronized (this){
//do something
}
}
2.底层语义
synchronized 修饰实例方法 其本质与修饰this对象,修饰静态方法相当于修饰this.class ,修饰代码块 形式为ynchronized(Object)
。归根结底 ,synchronized相关的代码都是synchronized(obj)这样的格式,即对对象加锁
同步代码块是通过 monitorentor monitorexit 来实现的,而对方法 是在 方法标识中加入 ACCSYNCHORNIZED
monitorenter:每个对象都有一个监视器锁(monitor),当monitor被占用时就处于锁定状态,线程在执行monitorenter时,会尝试获取monitor的所有权过程如下:如果该monitor的进入者为0 ,这该线程进入monitor,将进入数设置为1 ,该线程即为该monitor的拥有者。如果该monitor已经被其他线程占有,则进入数++,本线程进入阻塞状态,直到该monitor的进入数为0,再重新尝试获取monitor的拥有权。
monitorexit:执行monitorexit的线程必须是object所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,则线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
ACCSYNCHORNIZED:当方法被调用执行的时候,会检查方法的ACCSYNCHORNIZED访问标志是否被设置了,如果设置了,线程在执行该方法的时候,会先去获取monitor锁,获取完成之后才会执行方法,在方法完成之后,会释放monitor锁。
synchronized的几种使用方式的本质都是一样的,在语义底层都是通过monitorenter、monitorexit指令实现对monitor对象的获取和释放。
3.java对象头与monitor
java对象头:在jvm中,对象在内存中的布局分为三块,对象头,实例变量,填充数据
实例变量存放类的属性数据信息。填充数据:用于保证对象大小为8字节的整数倍。
对象头
如果是普通对象使用两个字宽存储对象头,如果对象是数组,则使用三个字宽 ,在32位虚拟机中一字宽为4字节,64位一字宽为8字节。
对象头的组成:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vbNjUmwm-1616297989736)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210317171059008.png)]
对象运行期间,Markword中的存储结构也会发生变化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UD3JNV9E-1616297989738)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210317172649764.png)]
其中,重量级锁状态时:指向互斥量(重量级锁)的指针 指向的就是monitor对象的起始地址。
monitor对象 —针对于重量级锁
在不同的锁状态下,Mark word会存储不同的信息,当锁为重量级锁(所得标志位为 10) 时,markword中会指向Monitor对象的指针,这个monitor对象称为管程或者监视器锁,每个对象都存在着一个monitor对象与之关联。
java虚拟机HotSpot中,monitor对象是由ObjectMonitor 构成的
monitor中包含有部分重要变量:
a.owner:指向一个持有当前monitor的ObjectWaiter对象(每一个等待锁的线程都会被封装成为一个ObjectWaiter对象)
b._cxq:存储ObjectWaiter对象的单向列表,在多个线程竞争锁的时候,线程们会先进入这个列表
c._EntryList:存储位于Blocked状态的ObjectWaiter对象列表
d._waitSet:存储处于wait状态ObjectWaiter列表 调用了java中的wait()方法会被放入其中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q0QUck9G-1616297989739)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210317193115477.png)]
线程锁的获取就是将owner指向自己
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jzFQSltl-1616297989741)(C:\研-作业报告代码\Java多线程学习归纳\watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mjc2MjEzMw==,size_16,color_FFFFFF,t_70)]
多个线程在竞争monitor锁时,会先进入_cxq列表(所有请求锁的线程都会放在则个List里面)
有资格成为候选资源的线程会进入EntryList。
在任何时候,最多只有一个线程正在竞争锁 该线程为OneDeck
在获取到锁之后 就会将owner赋值为自己
如果当前线程调用了wait方法,会失去monitor锁,进入waitSet列表
JVM每次会从cxq的队尾中取出一个数据,作为线程竞争者,但是在并发情况下 ,cxq会被大量进程进行CAS访问,为了降低竞争,JVM将部分进程移入到EntryList中作为候选进程,并指定EntryList中的某个线程为OneDeck线程(一般为最先进去的),owner线程一般不会直接将锁传递给oneDeck ,而是给他一个竞争的机会,也称为(竞争切换)
oneDeck线程获取到了锁资源之后,会变为Owner线程,如果线程执行了wait方法阻塞,会失去锁,进入waitSet ,然后再接收到notify或者Notufyall 之后会进入EntryLsit中 进入下一趟循环。
Synchronized是非公平的锁,线程在进入cxq之前会自旋试图获取锁,获取不到才会进入到cxq中,这对队列中到的对象不公平
Synchronized的优化
在java1.6中,实现了对Synchronized的优化,引入了偏向锁,轻量锁
偏向锁
在大多数情况,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,偏向锁是为了让获得锁的代价更低。
当锁对象第一次被线程获取的时候,虚拟机把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中的偏向线程ID,并将是否偏向锁的状态位置置为1。
以后该线程进入这个同步块时就不用进行CAS操作来加锁,去锁,只用测试以下对象头中的markword 中是否存储着指向当前线程的偏向锁。如果测试成功,说明该线程已经获得了锁,虚拟机就可以不再进行任何同步操作
如果没有,还有判断当前MarkWord 中 偏向锁的标志是否为1 (为1 代表 当前是偏向锁),如果没有设置,则使用CAS竞争锁,设置了的话,则尝试CAS将对象头的偏向锁的指向当前线程。
偏向锁使用了一种 竞争才会释放锁的机制
轻量级锁
在锁对象为偏向锁的情况下 产生竞争,这个时候会升级成为轻量级锁(自旋锁,无锁)。自旋的原因时为了一直去争夺锁对象
每个要获取该锁对象的线程都会在自己的栈帧中生成一个LockRecord 对象 ,此时锁对象的对象头中的markword 前面部分是一个lockReocrd对象的地址,线程们争夺锁就是为了将自己的LockRecord 对象的地址 赋值给 锁对象 。这个赋值过程是一个CAS 操作 (从锁中读取LR地址,修改为自己的LR,写回 查看之前LR地址有无被修改)并且一直自旋进行。
重量级锁
在轻量锁的情况下,如果竞争激烈 会升级为重量级锁。在jdk1.6之前,如果有线程自旋超过了10次,或者在自旋的线程数目超过了cpu核数的一半会升级为中级锁,在jdk1.6之后,出现了自适应自旋 Adpative spinning ,由jvm自己控制锁的升级。
重量级锁与轻量级锁的区别
操作系统中存在两种状态:用户态,核心态 一些指令只能在核心态下才能进行,用户态下不可进行,因此会需要执行系统调用,让自己进入核心态,然后执行。
在java中,使用 Sychronized 重量锁(monitor)时,需要系统分配锁对象(monitor),需要进入到核心态,因此开销大
为什么要从轻量级锁升级到重量级锁:
在轻量级锁中,所有的线程都在自旋争夺锁,这是建立在线程争夺并不严重,且每个线程占用锁的时间不会太长这一情况下的,所以在jdk1.6之前,要求线程的自旋次数不超过10次,如果竞争过大,且有线程占锁时间很长,cpu就被消耗了。
什么要从轻量级锁升级到重量级锁:
在轻量级锁中,所有的线程都在自旋争夺锁,这是建立在线程争夺并不严重,且每个线程占用锁的时间不会太长这一情况下的,所以在jdk1.6之前,要求线程的自旋次数不超过10次,如果竞争过大,且有线程占锁时间很长,cpu就被消耗了。
与轻量级锁相对比,重量锁需要切换状态,进入核心态。但是所有需要该锁但是没有获取到的线程会进入Bolcked(阻塞)状态(获取锁的线程执行wait() 或者sleep()方法之后 会进入waiting状态),不会消耗cpu,只有获取到锁的线程才会消耗cpu