同步机制
临界资源:多线程编程中,有可能会出现多个线程同时访问同一个共享(资源可以由多个线程同时访问)、可变(资源可以由多个线程同时访问)资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等。
由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问。
如何解决线程并发安全问题?
序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。
Java 中,提供了两种方式来实现同步互斥访问:synchronized
和 Lock
。
如何实现跨方法的对象锁?
在需要加锁的方法上使用sun.misc.Unsafe#monitorEnter
。
在需要解锁锁的方法上使用sun.misc.Unsafe#monitorExit
。
对象内存结构
- 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等
- 对象实际数据:即创建对象时,对象中成员变量,方法等
- 对齐填充:对象的大小必须是8字节的整数倍
Java锁体系
对象头MarkWord与锁的关系
锁膨胀升级
JDK1.6版本之后对synchronized的实现进行了各种优化,如自旋锁、偏向锁和轻量级锁,并默认开启偏向锁。
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLockin
无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,但是锁的升级是单向不可逆的。
锁粗化
有效地合并多个相邻的加锁代码块,因此可以减少加锁的成本。
如以下代码:
int i = 0;
synchronized (this){
i++;
}
synchronized (this){
i++;
}
synchronized (this){
i++;
}
等同于
int i = 0;
synchronized (this){
i++;
i++;
i++;
}
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁
,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。
synchronized
JVM内置锁,无需手动加锁解锁,JVM会自动加锁解锁。
synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。
加锁的方式:
- 同步实例方法,锁是当前实例对象
- 同步类方法,锁是当前类对象同步代码块,锁是括号里面的对象
- 同步代码块,锁是括号里面的对象
synchronized原理
JVM内置锁通过synchronized使用,通过内部对象Monitor(监视器锁)实现,基于进入(monitorEnter)与退出(monitorExit)Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁,性能较低。
ObjectMonitor
每一个java对象都有一个监视器(monitor对象),jvm是这样定义ObjectMonitor的:
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL; //对象头
_count = 0; //加锁次数,重入时需要用到
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; //ObjectMonitor对象的线程持有者
_WaitSet = NULL; //_WaitSet 记录处于wait状态的线程
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //_EntryList 记录处于block状态的线程
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
修饰非静态方法
此时锁是加在this当前类对象上的,若想要实现线程安全控制,bean(由容器管理)的作用域必须是singleton
。
修饰静态方法
加锁在类对象上。
逃逸分析
面试提问:实例对象存储在哪?
如果实例对象存储在堆区时:实例对象内存存储在堆区,实例的引用存在栈中,实例的元数据存放在方法区。
当实例对象有线程逃逸行为时,实例对象不是存储在堆区的。
通过vm运行参数关闭逃逸分析
:
-XX:-DoEscapeAnalysis
通过vm运行参数开启逃逸分析
:
-XX:+DoEscapeAnalysis
遗留问题
:JIT即时编译如何对代码进行逃逸分析?
证明:
我们使用循环去实例化五十万个对象。
当关闭逃逸分析时,我们使用jmap -histo 进程ID
命令可以很清楚的看到实例对象个数为500000。
而当逃逸分析开启时,这个数量仅有56378个,此时实例对象存储在线程栈空间中
。