为何要设计锁:
多线程编程中,多个线程同时访问同一个共享可变资源,并发访问会导致线程安全问题,锁用来保证线程安全。
内存模型
原子操作
Synchronize锁
不加锁:不能保证代码段的原子性
加锁:保证代码段逻辑的原子性。序列化访问临界资源,同一时刻只能有一个线程访问临界资源(同步互斥访问)
锁定义:
- 显示锁:reentrantLock,ReentrantReadWriteLock,手动加锁与解锁
- 隐式锁:JVM内置锁,不需要手动加锁与解锁
synchronized(object){
}
加了锁之后,字节码多了monitorenter ,monitorexit
monitorenter 进入
代码逻辑
monitorexit退出
加锁:
- 同步实例方法:锁是当前实例对象
- 同步类方法:锁是当前类对象
- 同步代码块:锁是括号里的对象
原理:
JVM内置锁通过synchronized使用,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex Lock(互斥锁,原理:互斥量)实现。是重量级锁,性能较低。
只有一个线程可以拿到Mutex 互斥量,进入代码段,没有抢到锁的线程,进入waitSet
java1.4以前,申请互斥量时,需要从用户态切换到内核态,悲观锁
T1释放锁后,通知同步队列唤醒阻塞的线程,队列里的线程进入entryList,再次竞争。
JVM内置锁是否可以手动控制加锁与解锁?jdk1.8可以
unsafe类
方法 monitorEnter(Object varl), monitorExit(Object varl)
只能通过反射获取
UnsafeInstance.reflectGetUnsafe().monitorEnter(Object);
UnsafeInstance.reflectGetUnsafe().monitorExit(Object);
就是因为有元数据指针,可以知道对象的类
实例对象内存中存储在哪?
对象实例存在堆中,对象的引用存在线程的栈中,对象的元数据存在方法区或元空间
对象头
锁的升级优化过程(不可逆)
JVM内置锁,升级膨胀过程:1.6以后
无锁升级为偏向锁
如果线程竞争不激烈,从无锁升级为偏向锁,进行CAS操作,将对象头前23位改成线程id。
如果CAS操作成功,对象头和线程id一致,则认为当前线程已经获取了锁,虚拟机就可以不再进行任何同步操作。偏向锁可以提高带有同步但无竞争的程序性能。
进入到monitorenter
到达安全点
从无锁状态变为轻量级锁状态
首先把对象改为无锁状态
线程栈空间开辟锁记录lockRecord空间,把对象头MarkWord 复制到这里,
- 修改Object mark word轻量级锁指针作用:告诉其他线程,该object monitor已被占用
- owner指向object mark word作用:在下面的运行过程中,识别哪个对象被锁住了。
虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后2bit)将转变为“00”,即表示此对象处于轻量级锁定状态,
owner指针指向对象头,对象头中也有指针指向lockRecord
从轻量级锁升级到重量级锁
锁标志的状态值变为”10”,Mark Word中存储的就是指向重量级(互斥量)的指针。
解锁
与加锁一样通过CAS操作进行的,如果对象的Mark Word仍然指向着线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来(这也是为什么之前要复制的原因)
如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。
轻量级锁优缺点
轻量级锁能提高程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。
-
如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销
-
但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。