一、线程安全问题
缘起:count++是一个线程不安全操作,想要保证该操作线程安全,必须使用加锁机制,那么可以使用Synchronized关键字来实现加锁。
1)synchronized修饰同步方法
/**
* 线程不安全的数值序列生成器
* @author whf
* @create 2021/5/30
*/
public class UnsafeSequence {
private int count;
public synchronized void addCount() { // 同步方法
count++;
}
public int getCount() {
return count;
}
}
2)synchronized修饰同步代码块
/**
* 线程不安全的数值序列生成器
* @author whf
* @create 2021/5/30
*/
public class UnsafeSequence {
private int count;
public void addCount() {
synchronized (this) { // 同步代码块
count++;
}
}
public int getCount() {
return count;
}
}
二、反编译字节码文件查看synchronized实现原理
通过javap反编译字节码文件
1)synchronized修饰同步方法
2)synchronized修饰同步代码块
总结:
1、synchronized修饰同步代码块是基于monitorenter和monitorexit指令实现线程同步
关于monitorenter和monitorexit两条指令,参考JVM规范中描述
monitorenter :
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
每个对象关联一个监视器锁,如果一个监视器锁被占有那么就会锁定,一个线程执行monitorenter指令会尝试获取与对象关联的监视器锁。
如果监视器锁的进入次数为0,那么线程会进入对象关联的监视器锁,并且将进入数记为1,该线程就是监视器锁的拥有者。
如果线程已经拥有对象关联的监视器锁,在重入监视器锁时,会将进入数加1。
如果其他线程已经获得监视器的所有权,那么该线程会阻塞等待监视器锁的进入数为0,然后重新尝试获取对象关联的监视器锁。
monitorexit :
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
执行monitorexit命令的线程必须是对象关联的监视器锁的拥有者,执行该命令监视器锁的进入数会-1,当进入数的值为0时,该线程会释放监视器锁,不再是监视器锁的拥有者,其他阻塞的线程可以尝试获取监视器锁。
2、synchronized修饰同步方法是基于标志位ACC_SYNCHRONIZED来实现的,JVM会让每个调用该方法的线程去获取monitor锁,相当于monitorenter和monitorexit指令的隐式实现
三、JVM的synchronized底层源码实现
JVM各个厂家实现:
Hotspot(SUN)、J9(IBM)、JRocket(BEA),后面SUN和JRocket都被Oracle收购了。
J9和JRocket都号称是最快的JVM。而Hotspot是平时用的最多的JVM。
jvm源码下载地址:
http://openjdk.java.net/ -> Mercurial -> jdk8 -> hotspot -> zip
monitor监视器锁实现
在hotspot虚拟机中,monitor监视器锁是由objectMonitor实现的,是基于C++来实现的,本质是依赖于底层操作系统的Mutex Lock实现的。
源文件路径:
src/share/vm/runtime/objectMonitor.hpp
主要包含的有以下属性
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; // 进入数
_object = NULL;
_owner = NULL; // 拥有者
_WaitSet = NULL; // 调用wait方法而阻塞的线程会被放到这里
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 竞争队列,所有线程会先放入这个队列
FreeNext = NULL ;
_EntryList = NULL ; // _cxq队列中能成为候选资源的线程会被放到这里
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
所有候选线程在EntryList中,如果有线程获取到对象关联的监视器锁,那么就可以将owner标记为该线程拥有。如果在线程执行中调用wait方法,线程会进入WaitSet。当线程执行完退出,并且recursions的值为0,owner会重新置为null,当前线程释放监视器锁,其他线程可以尝试获取锁。
monitor监视器锁获取
monitorenter执行源码,源码路径:
src/share/vm/interpreter/interpreterRuntime.cpp 561行
代码如下:
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (UseBiasedLocking) { // 判断是否偏向锁
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
四、java偏向锁、轻量级锁、重量级锁、适应性自旋
java锁状态:无锁状态,偏向锁,轻量级锁,重量级锁。锁竞争会导致锁升级,但是锁不会降级。
java锁对象是存储在对象头中的,对象头包括两部分:
Mark Word 存储对象的hashCode、锁信息、分代年龄、GC标志等信息
Class Metadata Address 类的元信息指针,JVM通过这个指针确定对象是哪个类的实例
以32位的JDK:
偏向锁:
偏向锁是jdk6引入的一项锁优化技术,目的是在没有线程竞争情况下,每次执行同步代码时无需CAS加锁解锁操作。提供程序运行效率,适用于单线程运行情况。
偏向锁获取:
1)判断是否是无锁状态,是否可获取偏向锁状态(锁标志位01表示可偏向,偏向锁为1代表对象锁已经偏向某个线程)
2)如果可偏向,判断对象头中偏向锁的线程ID是否跟当前线程ID一致,如果一致则直接执行同步代码。
3)如果对象头中的线程ID不指向当前线程,那么就需要通过CAS操作加锁,将当前线程ID写入到对象头Mark Word的线程ID设置为当前线程。
4)如果CAS获取偏向锁失败,代表有其他线程竞争锁,当到达全局安全点挂起持有偏向锁的线程,将锁升级为轻量级锁,然后继续执行同步代码。
偏向锁的释放:
当其他线程竞争锁时才会在释放锁。
总结:
偏向锁是默认开启的,可以使用JVM参数关闭偏向锁
-XX:-useBiasedLocking
轻量级锁:
轻量级加锁获取:
1)当其他线程竞争锁时,此时会释放偏向锁,同时锁标志位就会设置为"00",表示此对象处于轻量级锁状态。
2)拷贝对象头Mark Word到线程栈帧的锁记录中。
3)拷贝成功后,虚拟机会尝试使用CAS操作将对象头Mark Word更新为指向锁记录的指针,并将锁记录Lock Record中的owner指针指向object mark word。
5)如果CAS失败,则会自旋重试,jvm会根据竞争情况控制自旋的时间。java使用的是自适应性自旋,避免长时间CPU空转浪费CPU资源。
总结:
使用重量级锁会直接禁用偏向锁和轻量级锁
-XX:+UseHeavyMonitors
重量级锁:
若长时间自旋无法获取到锁的情况下,轻量级锁会升级为重量级锁,无法获取到锁的线程执行停止自旋进入阻塞状态。重量级锁就是monitor监视器锁,依赖于底层操作系统的Mutex Lock,锁标识设置为"10",对象头指向监视器锁的地址。当多个线程进入同步代码时,会从entry set中竞争锁,当竞争到monitor锁时,会将monitor锁的owner设置为当前线程,并且进入数记为1,重入获取锁时进入数会叠加1,释放monitor锁时进入数也会相应减1,当进入数为0时,会释放monitor锁,并且将monitor锁的owner重新设置为null。
重量级锁底层依赖Mutex Lock来保持资源竞争互斥,会涉及到用户态和内核态的转换,会十分消耗性能。
偏向锁、轻量级锁、重量级锁总结:
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。