一、synchronized关键字
使用方式:
详见另一篇博客:线程8锁
1) 静态同步方法
2) 非静态同步方法
3) 静态同步代码块
4) 非静态同步代码块
作用范围:
1)类锁(所有的对象)
2)对象锁(同一对象)
原理:
同步方法、同步代码块的加锁、解锁,本质上是对同一对象监视器(monitor)的获取、释放,获取的过程具有拍它性,保证同一时刻只有一个线程能获取到锁
实现:
编译后,在调用同步方法/代码块前加入monitorenter指令,在结束调用或异常处加入monitorexit指令
优点:
1)synchronized锁的释放由虚拟机来完成,不用人工干预
2)发生异常时,线程会自动释放占有的锁,不会发生死锁问题
缺点:
1)不能响应中断
2)同一时刻不管是读还是写都只能有一个线程对共享资源操作,其他线程只能等待
3)大量并发情况下,synchronized锁的效率远低于lock锁
二、编译解析
- 1. 同步代码块源码
public class SynchronizedBlock {
public void method() {
synchronized (this) {
System.out.println("...");
}
}
- 2.同步代码块反编译源码
同步代码块源码解析:
同步代码块是使用MonitorEnter
和MoniterExit
指令实现的,在编译时,MonitorEnter指令被插入到同步代码块的开始位置
,MoniterExit指令被插入到同步代码块的结束位置和异常位置
。任何对象都有一个Monitor与之关联,当Monitor被持有后将处于锁定状态。MonitorEnter指令会尝试获取Monitor的持有权,即尝试获取锁。 - 3. 同步方法源码
public class SynchronizedMethod {
//普通同步方法
public synchronized void method1() {
System.out.println("...");
}
//静态同步方法
public synchronized static void method2() {
System.out.println("...");
}
}
- 4. 同步方法反编译
同步方法源码解析:
同步方法依赖flags标志ACC_SYNCHRONIZED
实现。ACC_SYNCHRONIZED标志表示方法为同步方法,非静态方法(没有ACC_STATIC标志)
,使用调用该方法的对象作为对象锁
;如果为静态方法(有ACC_STATIC标志)
,使用该方法所属的Class类在JVM的内部对象作为类锁
下面是摘自《Java虚拟机规范》的话:
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。
虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获得同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
同步一段指令集序列通常是由Java语言中的synchronized块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要编译器与Java虚拟机两者协作支持。
Java虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现。无论是显式同步(有明确的monitorenter和monitorexit指令)还是隐式同步(依赖方法调用和返回指令实现的)都是如此
编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须有执行其对应monitorexit指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit指令。
三、Synchronized锁原理
- 1. Java头对象
分类 | 作用 | 内容 |
---|---|---|
Mark Word | 对象自身运行时数据 | 存储对象自身运行时数据,对象头默认是无锁状态下的存储结构,存储对象的HashCode、分代年龄、锁标志位等信息。考虑到JVM的空间效率,对象头被设计成非固定的数据结构,其数据结构在轻量级锁、重量级锁、偏向锁等场景下是变化的 |
Class Metadata Address | 存储类元数据的指针 | 存放类的属性数据信息,包括父类的属性信息,如果是数组实例,还包括数组的长度(按4字节对齐) |
Array length | 数组长度 | 虚拟机要求对象的起始地址必须是8字节,填充数据非必须存在,只有数组类型才有 |
- Mark Word如下图:
- 2.synchronized 锁优化:
synchronized锁通常被称为重量级锁,但jdk1.6之后对synchronized进行了优化,目的是减少获取/释放锁带来的性能消耗
,引入了偏向锁、轻量级锁、重量级锁
偏向锁:
同一个线程的执行过程中,再次进入、退出同步代码时,不需要进行加锁、解锁操作,而是执行下面的步骤:
判断当前线程id和Markword中线程id是否一致
一致,则说明当前线程已经获取到锁,继续执行同步代码
不一致,判断Markword中的偏向锁的标志位
如果没有偏向,利用cas竞争锁,即第一次获取锁,如果成功获取到锁,则执行同步代码
如果是偏向锁,且自己不是偏向锁的持有者,说明存在竞争,则偏向锁膨胀升级为轻量级锁
锁撤销:
由偏向锁到轻量级锁,需要先执行锁撤销操作,将偏向锁变为无状态锁,步骤如下:
1)将持有偏向锁的线程停止
2)遍历线程栈,如果存在锁记录,修复锁记录和Markword,使其变为无锁状态
3)唤醒线程,将锁升级为轻量级锁
轻量级锁:
锁撤销升级为轻量级锁,Markword会有相应的变化,步骤如下:
1)当前线程会在自己的栈帧中,创建锁记录区域(lock record)
2)将对象头中的Markword复制到lock record中
3)同时尝试使用cas将markword更新为指向锁记录的指针
更新成功,则当前线程获取到锁,继续执行同步代码
更新失败,则对比Markword中的锁记录指针,是否指向当前线程的锁记录
如果是,则当前线程获取到锁,继续执行同步代码
如果不是,说明其它线程获取到锁,即存在锁竞争,则轻量级锁会膨胀升级为重量级锁
轻量级锁分类:自旋锁、自适应自旋锁:
- 自旋锁
当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上去竞争获取锁- 自旋锁问题:
1)性能:当同步代码执行时间过长,其它自旋转等待的线程会一直消耗cpu
2)无法连续获取锁:当持有锁的线程释放锁,其它等待的线程会和当前线程一起来竞争锁,当前线程不一定能再次获取到锁,可能一直获取不到,需要一直在原地旋转消耗cpu- 问题解决:
给线程设置空旋转次数,超过设置的次数,轻量级锁膨胀升级为重量级锁,默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin来进行更改- 自适应自旋锁
线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数
1)当线程1刚刚释放了锁,线程2获得了锁,当线程1在线程2执行过程中,想再次获取锁,线程1只能原地等待,但jvm认为线程1刚获取到锁,会增加线程1的自旋次数
2)当某个线程自旋后,很少成功获取到锁,那么以后这个线程想要再获取锁时,可能直接忽略自旋,直接升级为重量级锁,以免空旋转浪费资源
重量级锁:
轻量级锁膨胀后升级为重量级锁,重量级锁利用对象内部的monitor(管程锁)实现,而monitor又依赖操作系统的MutexLock(互斥锁)来实现,所有重量级锁又称为互斥锁
- 为什么重量级锁开销大?
当操作系统检查到锁是重量级锁,会阻塞等待获取锁的线程,被阻塞的线程不会消耗cpu,当阻塞/唤醒线程,需要操作系统执行,这就需要从用户状态切换到内核状体啊,这个状态切换需要消耗很多的时间,甚至比代码执行的时间还长
四、扩展
Monitor:
可以理解为一种同步工具或同步机制
1)互斥,一个Monitor在同一时间只能被一个线程持有,即Monitor中的所有方法都是互斥的
2)signal机制:如果条件变量不满足,允许正在持有Monitor的线程暂时释放特权,当变量条件满足时,当前线程可以唤醒正在等待该条件变量的线程,然后重新获取Monitor的持有权
所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁(Monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成)。
Monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高
轻量级锁和重量级锁对比:
1)轻量级锁:非阻塞式同步、乐观锁,线程在等待锁的过程,并没有将线程阻塞挂起,而是让线程空旋转等待,串行执行
2)重量级锁(互斥锁):阻塞式同步、悲观锁
CAS:Compare And Swap
比较并交换
CAS(V,E,N):V表示要更新的变量(内存地址),E表示预期值(旧的值),N表示新的值,当V中存储的值和E相等时,将V中存储的值修改为N,否则什么都不做,最终返回最新的结果