深度解析synchronized实现原理
什么是线程安全问题?
简单的说,当多个线程在同时访问某个方法时,这个方法无法按照我们预期的行为执行,我们认为这个线程是不安全的。
导致线程不安全的原因主要有三个:
- 原子性
- 有序性
- 可见性
1.多线程环境下的原子性问题
什么是原子性?
类似于数据库中的原子性,即一个或多个指令操作在CPU执行过程中不允许被中断。
public class AtomicExample {
// 排除可见性带来的影响
volatile int i = 0;
public void incr(){
i ++;
}
public static void main(String[] args) throws InterruptedException {
AtomicExample atomicExample = new AtomicExample();
Thread[] threads = new Thread[2];
for (int j = 0; j < 2; j++) {
threads[j] = new Thread(()->{
for (int k = 0; k < 10000; k++) {
atomicExample.incr();
}
});
threads[j].start();
}
// 保证线程执行结束
threads[0].join();
threads[1].join();
System.out.println(atomicExample.i);
}
}
// 14225
// 14562
// 15824
与期望值20000不一致,导致这个现象产生的原因就是原子性问题
1.1 深入分析原子性问题的本质
从本质上说,原子性问题产生的原因有两个
-
CUP时间片切换
-
执行指令的原子性
-
public void incr(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup // 把变量i从内存中加载到CPU的寄存器中 2: getfield #2 // Field i:I 5: iconst_1 // 在寄存器中执行+1的操作 6: iadd // 把结果保存在内存中 7: putfield #2 // Field i:I 10: return LineNumberTable: line 13: 0 line 14: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lcn/zhima/sync/AtomicExample; 也就是说这三个指令不具原子性,CPU在执行过程中会存在中断的情况,这种中断也会导致原子性问题
-
1.2 深入分析原子性问题的本质
关于多线程环境下并行或切换导致的最终执行结果不符合预期的,解决问题的办法可以从两个方面考虑
- 不允许当前非原子指令在执行过程中被中断,也就是说保证i++操作在执行过程中不存在上下文切换
- 多线程并行执行导致的原子性问题可以通过一个互斥条件来实现串行执行
public class AtomicExample {
volatile int i = 0;
public synchronized void incr(){
i ++;
}
public static void main(String[] args) throws InterruptedException {
AtomicExample atomicExample = new AtomicExample();
Thread[] threads = new Thread[2];
for (int j = 0; j < 2; j++) {
threads[j] = new Thread(()->{
for (int k = 0; k < 10000; k++) {
atomicExample.incr();
}
});
threads[j].start();
}
// 保证线程执行结束
threads[0].join();
threads[1].join();
System.out.println(atomicExample.i);
}
}
2.Java中的synchronized同步锁
3.关于synchronized同步锁的思考
同步锁的本质是实现多线程的互斥,同一时刻只有一个线程能够访问加了同步锁的代码,使线程安全性得到保证。为了达到这个目的,我们应该怎么做?
- 同步锁的核心特性是排他,要达到这个目的,多个线程必须去抢占同一资源
- 同一时刻只有一个线程执行加了同步锁的代码,其他线程只能等待
- 处于等待的线程不能一直占有CUP资源,没抢占到锁就要被阻塞等待并且释放CPU资源
- 如果非常多的线程被阻塞,我们要通过一个容器来存储线程,当获得锁的线程完成任务并释放锁后,然后再从这个容器中唤醒一个线程,被唤醒的线程会再次尝试抢占锁
4.synchronized同步锁标记存储分析
如要要实现锁的互斥,必须要满足如下两个条件
- 必须竞争同一资源
- 需要一个标记来识别当前锁的状态是空闲还是繁忙
4.1揭秘Mark Word的存储结构
4.1.1对象头
对象头是由三个部分组成:Mark Word,Klass Pointer Length
Mark Word
4.1.2通过ClassLayout查看对象内存布局
1.引用jar包
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
2.创建对象
public class ClassLayoutExample {
public static void main(String[] args) {
ClassLayoutExample example = new ClassLayoutExample();
System.out.println(ClassLayout.parseInstance(example).toPrintable());
}
}
3.通过JOL工具打印对象的内存布局
System.out.println(ClassLayout.parseInstance(example).toPrintable());
4.查看运行结果
cn.zhima.sync.ClassLayoutExample object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
字段说明:
- OFFSET:偏移地址,单位为字节
- SIZE:占用的内存大小,单位为字节
- TYPE DESCRIPYTION:类型描述,其中object header为对象头
- object header,一共占用12个字节,前面8个对应MarkWord,后4个对应的是类型指针,它占有4个字节是因为默认指针压缩
- loss due to the next object alignment 表示的是对其填充,这个里面填充了4个字节,从而保证了内存大小是8的整数倍。
- VALUE:对应内存中当前存储的值
- Instance size:表示最终输出的大小为16bytes表示当前对象实例占有16个字节
4.1.2.1关于压缩指针
cn.zhima.sync.ClassLayoutExample object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 28 30 e7 17 (00101000 00110000 11100111 00010111) (401027112)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
4.1.2.2详细描述对齐填充的作用
CPU访问数据,以字长(Word Size)为单位访问数据
对其填充主要解决伪共享问题
4.1.3Hotspot虚拟机中的对象存储的源码
class instanceOopDesc : public oopDesc {
public:
// aligned header size.
static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }
// If compressed, the offset of the fields of the instance may not be aligned.
static int base_offset_in_bytes() {
// offset computation code breaks if UseCompressedClassPointers
// only is true
return (UseCompressedOops && UseCompressedClassPointers) ?
klass_gap_offset_in_bytes() :
sizeof(instanceOopDesc);
}
static bool contains_field_offset(int offset, int nonstatic_field_size) {
int base_in_bytes = base_offset_in_bytes();
return (offset >= base_in_bytes &&
(offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
}
};
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;// 对象标记,属于markOop类型,也就是Mark Word,记录对象和锁有关的信息
union _metadata { // 存储的是指向它的类元数据的首地址
Klass* _klass; // 表示普通指针,指向该对象的类元信息,也是就是属于哪一个Class实例
narrowKlass _compressed_klass; // 表示压缩指针,默认开启压缩指针,在开启压缩指针之后,存储中占用的字节数会被压缩
} _metadata;
markOop是markOopDesc类型的指针,它定义oopsHierarchy.hpp文件中
typedef void* OopOrNarrowOopStar;
typedef class markOopDesc* markOop;
在Hotspot中,markOopDesc这个类的定义在markOop.hpp文件中
class markOopDesc: public oopDesc {
private:
// 转换
uintptr_t value() const { return (uintptr_t) this; }
public:
// 常量
enum { age_bits = 4, // 分代年龄,占4个字节
lock_bits = 2, // 锁标记,占2个字节
biased_lock_bits = 1, // 偏向锁标记,占1bit
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits, // 针对于无锁计算hashcode占用的字节数
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits, // hashcode,对于64位虚拟机来说,如果最大字节数大于31则取31,否则取真实字节数
cms_bits = LP64_ONLY(1) NOT_LP64(0), // 如果是64位操作系统就占1byte,否则占0位
epoch_bits = 2 // 保存偏向锁的时钟周期,占2bit
};
Mark Word在32位和64位虚拟机上的存储布局
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
5.锁类型
5.1偏向锁原理分析
偏向锁的作用就是,线程在没有现成竞争的情况下去访问synchronized同步代码块时,会尝试先通过偏向锁来抢占访问资格,这个抢占过程是基于CAS来完成,如果抢占锁成功则直接修改对象头中的锁的标记。
加锁前
cn.zhima.sync.BiasedLockExample object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
// 对象头的第一个字节的最后3位[001],其中低位的两位代表锁的标记,其中[01]表示无锁状态
加锁之后
cn.zhima.sync.BiasedLockExample object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 98 f7 f4 02 (10011000 11110111 11110100 00000010) (49608600)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
// 对象头的第一个字节的最后3位[000],其中低位的两位代表锁的标记,其中[00]表示轻量级锁
当前程序不存在锁的竞争,基于前方铺垫的理论分析,此处获取的应该是偏向锁,但为什么编程轻量级锁?
原因是,JVM在启动的时候,有一个启动参数-XX:BiasedLockingStartuoDelay,这个参数表示偏向锁延迟开启的时间,默认4秒,也就是说在我们运行上面程序的时候,偏向锁还未开启,导致最终只能获取轻量级锁。之所以延迟启动,是因为JVM在启动的时候存在多线程竞争场景,这个时候开启偏向锁的意义不大,开启偏向锁的方式:
- 设置-XX:BiasedLockingStartuoDelay=0
- 让线程延迟4s以上
加锁前
cn.zhima.sync.BiasedLockExample object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
加锁之后
cn.zhima.sync.BiasedLockExample object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 e8 41 02 (00000101 11101000 01000001 00000010) (37873669)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
为什么开启偏向锁之后,没有加锁的时候第一个字节的最后3位变成了[101]?
分析可知,在没有加锁之前没有存储线程ID,加锁之后才有线程ID(37873669),因此在获得偏向锁之前,这个标记表示当前是可偏向状态,并不代表已处于偏向状态
5.2轻量级锁原理分析
轻量级锁就是,多线程环境下,一个线程没有抢占到锁,则通过自旋锁实现锁的抢占。
加锁前
cn.zhima.sync.BiasedLockExample object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
// 对象头的第一个字节的最后3位[001],其中低位的两位代表锁的标记,其中[01]表示无锁状态
加锁之后
cn.zhima.sync.BiasedLockExample object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 98 f7 f4 02 (10011000 11110111 11110100 00000010) (49608600)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
// 对象头的第一个字节的最后3位[000],其中低位的两位代表锁的标记,其中[00]表示轻量级锁
JDK1.6中,轻量级锁自旋次数10次,也可以通过-XX:PreBlockSpin参数调整自旋次数。1.6中还对自旋锁进行了优化,引入了自适应自旋锁
5.3重量级锁原理分析
在轻量级锁自旋一定次数之后仍然没有获取到锁资源,就会进行锁的升级,将轻量级锁升级成重量级锁。
加锁之前
cn.zhima.sync.HeavyLockExample object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
t1线程抢占了锁
cn.zhima.sync.HeavyLockExample object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 08 f1 30 1c (00001000 11110001 00110000 00011100) (472969480)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
main线程来抢占锁
cn.zhima.sync.HeavyLockExample object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) ca 1d 65 18 (11001010 00011101 01100101 00011000) (409279946)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Process finished with exit code 0
JVM的synchronized的重量级锁实现中,通过for(;;)循环实现自旋锁,然后在该循环中通过Atomic::cmpxchg_ptr进行的CAS抢占
int ObjectMonitor::TryLock (Thread * Self) {
for (;;) {
void * own = _owner ;
if (own != NULL) return 0 ;
if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
// Either guarantee _recursions == 0 or set _recursions = 0.
assert (_recursions == 0, "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert that OwnerIsThread == 1
return 1 ;
}
// The lock had been free momentarily, but we lost the race to the lock.
// Interference -- the CAS failed.
// We can either return -1 or retry.
// Retry doesn't make as much sense because the lock was just acquired.
if (true) return -1 ;
}
}
inline intptr_t Atomic::cmpxchg_ptr(intptr_t exchange_value, volatile intptr_t* dest, intptr_t compare_value) {
return (intptr_t)cmpxchg((jlong)exchange_value, (volatile jlong*)dest, (jlong)compare_value);
}
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
6.关于CAS机制的实现原理分析
6.1CAS源码分析
CAS本身代表着比较并替换,是一种无锁化机制,这个方法能再多线程环境下保证对一个共享变量进行修改时的原子性不变。
public final native boolean compareAndSwapInt(Object o, long Offset, int expect, int update);
从方法的定义上来看,包含4个参数:
- o:表示的是当前的实例对象
- offset:表示实例变量的内存地址偏移量
- expect:表示预期值
- update:表示要更新的值
public class CasExample {
public AtomicInteger atomicInteger = new AtomicInteger(0);
public void add(){
atomicInteger.getAndIncrement();
}
}
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public final int getAndAddInt(Object o, long offset, int n) {
int v;
do {
v = this.getIntVolatile(o, offset);
} while(!this.compareAndSwapInt(o, offset, v, v + n));
return v;
}
6.2CAS实现自旋
实现自旋锁的2个方式:
- 通过for(;;)循环不断尝试
- 通过一个线程安全的操作去尝试抢占资源,CAS就是比较好的方法
6.3CAS在JVM的原理分析
// UNSAFE_ENTRY 代表一个宏定义
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
unsigned Atomic::cmpxchg(unsigned int exchange_value,
volatile unsigned int* dest, unsigned int compare_value) {
assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest,
(jint)compare_value);
}
inline jlong Atomic::cmpxchg (jlong exchange_value, volatile jlong* dest, jlong compare_value) {
bool mp = os::is_MP();
__asm__ __volatile__ (LOCK_IF_MP(%4) "cmpxchgq %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
Lock的作用有两个:
- 保证指令执行的原子性
- 禁止该指令与前后的读写操作重排序
7.锁升级的实现流程
7.1偏向锁的实现原理
偏向锁案例
-XX:BiasedLockingStartupDelay=0
public class BulkRevokeExample {
public static void main(String[] args) throws InterruptedException, IOException {
List<BulkRevokeExample> bulks = new ArrayList<>();
Thread t1 = new Thread(()->{
for (int i = 0; i < 100; i++) {
BulkRevokeExample a = new BulkRevokeExample();
synchronized (a){
bulks.add(a);
}
}
});
t1.start();
t1.join();
System.out.println("打印t1线程,bluks中第20个对象的对象头: ");
System.out.println(ClassLayout.parseInstance(bulks.get(19)).toPrintable());
Thread t2 = new Thread(()->{
for (int i = 0; i < 20; i++) {
BulkRevokeExample a = bulks.get(i);
synchronized (a){
// 分别打印第19次和第20次偏向锁的偏向结果
if ( i == 18 || i == 19 ){
System.out.println("第"+(i+1)+"次偏向结果");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
}
});
t2.start();
System.in.read();
}
}
7.2轻量级锁的实现原理
7.3重量级锁的实现原理
void os::PlatformEvent::park() { // AKA "down()"
// Invariant: Only the thread associated with the Event/PlatformEvent
// may call park().
// TODO: assert that _Assoc != NULL or _Assoc == Self
int v ;
for (;;) {
v = _Event ;
if (Atomic::cmpxchg (v-1, &_Event, v) == v) break ;
}
guarantee (v >= 0, "invariant") ;
if (v == 0) {
// Do this the hard way by blocking ...
int status = pthread_mutex_lock(_mutex);
assert_status(status == 0, status, "mutex_lock");
guarantee (_nParked == 0, "invariant") ;
++ _nParked ;
while (_Event < 0) {
status = pthread_cond_wait(_cond, _mutex);
// for some reason, under 2.7 lwp_cond_wait() may return ETIME ...
// Treat this the same as if the wait was interrupted
if (status == ETIME) { status = EINTR; }
assert_status(status == 0 || status == EINTR, status, "cond_wait");
}
-- _nParked ;
_Event = 0 ;
status = pthread_mutex_unlock(_mutex);
assert_status(status == 0, status, "mutex_unlock");
// Paranoia to ensure our locked and lock-free paths interact
// correctly with each other.
OrderAccess::fence();
}
guarantee (_Event >= 0, "invariant") ;
}