您当前的位置: 首页 > 

星夜孤帆

暂无认证

  • 5浏览

    0关注

    626博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Synchronizd底层原理

星夜孤帆 发布时间:2021-07-29 10:11:29 ,浏览量:5

一、CAS解析

CAS详解

Compare And Swap (Compare And Exchange) / 自旋 / 自旋锁 / 无锁

因为经常配合循环操作,直到完成为止,所以泛指一类操作

cas(v, a, b) ,变量v,期待值a, 修改值b

ABA问题,你的女朋友在离开你的这段儿时间经历了别的人,自旋就是你空转等待,一直等到她接纳你为止

解决办法(版本号 AtomicStampedReference),基础类型简单值不需要版本号

public class AtomicIntegerTest {

    private static AtomicInteger m = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        // 100个线程
        Thread[] threads = new Thread[100];

        // 初始化100
        final CountDownLatch countDownLatch = new CountDownLatch(threads.length);


        for (int i = 0; i < threads.length; i++) {
            // 给Thread数组赋值
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    // 正常情况下没有加锁,线程不安全,应该是小于100*10000的。但是,使用了原子整型就可以保证数据一致性了
                    m.incrementAndGet(); // m++
                }
                countDownLatch.countDown();
            });
        }

        // 调用start的方法
        Arrays.stream(threads).forEach(Thread::start);

        // 所有线程结束之后,打印出来,100减到0
        countDownLatch.await();

        System.out.println(m);
    }
}

 

 

jdk8u: unsafe.cpp: cmpxchg = compare and exchange

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);
// 调用到这个Atomic::cmpxchg(x, addr, e)
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

jdk8u: atomic_linux_x86.inline.hpp is_MP = Multi Processor 多核CPU

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint
compare_value) {
// is_MP = Multi Processor多核CPU
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;
}

jdk8u: os.hpp is_MP()

static inline bool is_MP() {
// During bootstrap if _processor_count is not yet initialized
// we claim to be MP as that is safest. If any platform has a
// stub generator that might be triggered in this phase and for
// which being declared MP when in fact not, is a problem ‐ then
// the bootstrap routine for the stub generator needs to check
// the processor count directly and leave the bootstrap routine
// in place until called after initialization has ocurred.
return (_processor_count != 1) || AssumeMP;
}

jdk8u: atomic_linux_x86.inline.hpp

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

最终实现:

cmpxchg = cas修改变量值

# 这条指令不是原子的,单CPU不用加lock
lock cmpxchg 指令

硬件:

lock指令在执行后面指令的时候锁定一个北桥信号(不采用锁总线的方式)

 二、Java对象的布局

Java对象布局详解

在 HotSpot虚拟机中,对象在内存中的存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

如下图:

 2.1 对象头

Mark Word:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等。在32位系统占4字节,在64位系统中占8字节;

Class Pointer(类型指针):用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在32位系统占4字节,在64位系统中占8字节;

Length:如果是数组对象,还有一个保存数组长度的空间,占4个字节;

2.1.1 Mark Word 

Mark Word是一个8字节的头,记录着锁信息,GC信息,还有HashCode。锁定了某个对象后,实际上是修改了Mark Word的内容。

2.2 实例数据

对象实例数据 : 对象的所有成员变量,其中包含父类的成员变量和本类的成员变量,也就是说,除去静态变量和常量值放在方法区,非静态变量的值是随着对象存储在堆中的。

byte、boolean是1个字节,short、char是2个字节,int、float是4个字节,long、double是8个字节,reference是4个字节(64位系统中是8个字节)。

2.3 对齐填充

由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,也就是说对象的大小必须是 8 字节的整数倍。

对象头部分是 8 字节的倍数,所以当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

Java一个对象占多少字节,根据上面所述,我们知道 占用16字节,下面具体详述:

64位系统(未开启指针压缩):Mark Word占用8个字节 + Class Pointer占用8个字节 = 16个字节 (16已经是8的整数倍,所以不需要对齐填充)   -》对象头的大小:16个字节

64位系统(开启指针压缩):Mark Word 占用8个字节 + Class Pointer 占用4个字节 + 对齐填充 4个字节 = 16个字节 (空对象,所以实例数据大小为0) -》对象头的大小:12个字节 

三、synchronized的横切面详解 3.1 java源码层级 

synchronized(o)

 

 3.2 字节码层级

monitorenter moniterexit

3.3 JVM层级(Hotsport)

 InterpreterRuntime:: monitorenter方法

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

synchronizer.cpp revoke_and_rebias

void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
使用偏向锁
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias,
THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
assert(!attempt_rebias, "can not rebias toward VM thread");
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj‐>mark()‐>has_bias_pattern(), "biases should be revoked by now");
}
slow_enter (obj, lock, THREAD) ;
}
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj‐>mark();
assert(!mark‐>has_bias_pattern(), "should not see bias pattern here");
if (mark‐>is_neutral()) {
// Anticipate successful CAS ‐‐ the ST of the displaced mark must
// be visible set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()‐>mark_addr(), mark)) {
TEVENT (slow_enter: release stacklock) ;
return ;
}
// Fall through to inflate() ...
} else
if (mark‐>has_locker() && THREAD‐>is_lock_owned((address)mark‐>locker())) {
assert(lock != mark‐>locker(), "must not re‐lock the same lock");
assert(lock != (BasicLock*)obj‐>mark(), "don't relock with same BasicLock");
lock‐>set_displaced_header(NULL);
return;
}
#if 0
// The following optimization isn't particularly useful.
if (mark‐>has_monitor() && mark‐>monitor()‐>is_entered(THREAD)) {
lock‐>set_displaced_header (NULL) ;
return ;
}
#endif
// The object header will never be displaced to this lock,
// so it does not matter what the value is, except that it
// must be non‐zero to avoid looking like a re‐entrant lock,
// and must not look locked either.
lock‐>set_displaced_header(markOopDesc::unused_mark());
// inflate方法:膨胀为重量级锁
ObjectSynchronizer::inflate(THREAD, obj())‐>enter(THREAD);
}
四、synchronized锁升级过程 4.1 JDK8 markword实现:

无锁 - 偏向锁 - 轻量级锁 (自旋锁,自适应自旋)- 重量级锁

synchronized优化的过程和markword息息相关

用markword中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位

4.2 锁升级过程

JDK较早的版本 OS的资源 互斥量 用户态 -> 内核态的转换 重量级 效率比较低

现代版本进行了优化

无锁 - 偏向锁 -轻量级锁(自旋锁)-重量级锁

最早的情况下,你只要一加synchronized,直接就是重量级锁,后来优化后,加了一些中间状态。即,偏向锁,轻量级锁(自旋锁)

当我们new了一个普通对象后,往上加锁的时候,优先加偏向锁,轻度竞争变为轻量级锁,然后,重度竞争再变为重量级锁。如果上来就重度竞争直接变为重量级锁。

偏向锁未启动的时候,普通对象直接升级为轻量级锁。

偏向锁

markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁,偏向加锁的第一个线程 。

hashCode备份在线程栈上 线程销毁,锁降级为无锁

比如生活中的公共厕所,进去上锁,出门解锁。大多数情况下只有一个线程在运行,没必要再去跟操作系统申请,把这个门锁上之后再进行操作。

直接把线程id往门上一贴,直接干我们的事就得了,这样不经过操作系统效率更高。

轻量级锁(自旋锁)

有争用,锁升级为轻量级锁,每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁。

如果,再有一个线程,多个线程进行争抢,锁升级为轻量级锁,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁。

这个轻量级锁,也是不需要经过操作系统内核的。

 什么时候升级

偏向锁升级轻量级锁

偏向锁升轻量级锁很简单,只要有另一个线程来了,就会升级。

轻量级锁升级为重量级锁

自旋超过10次,升级为重量级锁 - 如果太多线程自旋 CPU消耗过大,不如升级为重量级锁,进入等待队列(不消耗CPU)-XX:PreBlockSpin

自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,

进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,

直接阻塞线程,避免浪费处理器资源。

偏向锁由于有锁撤销的过程revoke,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁。

为什么又自旋锁还需要重量级锁?

自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗。

重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源。

偏向锁是否一定比自旋锁效率高?

不一定,在明确知道会有多个线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁

JVM启动过程,会有很多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段时间再打开。

 偏向锁的延迟

 偏向锁默认是启动的,但是有个延迟,延迟4s钟,4秒之后,偏向锁才会启动

 

 

 

默认synchronized(o) 00 -> 轻量级锁 默认情况 偏向锁有个时延,默认是4秒

why? 因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,

如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。

‐XX:BiasedLockingStartupDelay=0 设置延迟时间为0,这样开启就是偏向锁

如果设定上述参数 new Object () - > 101 偏向锁 ->线程ID为0 -> Anonymous BiasedLock 打开偏向锁,new出来的对象,默认就是一个可偏向匿名对象101

如果有线程上锁 上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程

偏向锁不可重偏向 批量偏向 批量撤销 

如果有线程竞争 撤销偏向锁,升级轻量级锁 线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁 

如果竞争加剧 竞争加剧:有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半,1.6之后,加入自适应自旋 Adapative Self Spinning ,

JVM自己控制 升级重量级锁:-> 向操作系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,

然后再映射回用户空间 

(以上实验环境是JDK11,打开就是偏向锁,而JDK8默认对象头是无锁)

偏向锁默认是打开的,但是有一个时延,如果要观察到偏向锁,应该设定参数

加锁,指的是锁定对象 

 4.3 锁重入

synchronized是可重入锁

重入次数必须记录,因为要解锁几次必须得对应

偏向锁 自旋锁->线程栈-> LR + 1

重量级锁 -> ObjectMonitor字段上

 4.4 synchronized最底层实现

java -XX:+UnlockDiagonositicVMOptions -XX:+PrintAssembly T

C1 Compile Level 1 (一级优化)

C2 Compile Level 2 (二级优化)

找到m() n()方法的汇编码,会看到 lock comxchg .....指令

4.5 synchronized vs Lock (CAS)

在高争用 高耗时的环境下synchronized效率更高

在低争用 低耗时的环境下CAS效率更高

synchronized到重量级之后是等待队列(不消耗CPU)

CAS(等待期间消耗CPU)

4.6 用hsdis观察synchronized 4.6.1 安装hsdis

java -xX:+Un1ockDiagnosticvMoptions -xX:+PrintAssembly T > 1.txt
4.6.2 输出结果

 由于JIT会为所有代码生成汇编,请搜索T::m T::n,来找到m()和n()方法的汇编码

 

m方法 

 

 

n方法

 

实现内存屏障 

总结:

CAS底层和Synchronized底层,最后到汇编级别,都会用到lock cmpaxchg(compare and exchange)指令。

任何一个语句,最后都要走汇编,走汇编跟走内核申请锁是两个概念。

CAS是不需要向内核申请锁信息的

Synchronzied,会向内核申请锁信息

 五、锁消除 lock eliminate
public void add(String str1,String str2){
    StringBuffer sb = new StringBuffer();
    sb.append(str1).append(str2);
}

我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,

我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),

因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。

六、 锁粗化 lock coarsening
    public String test(String str){
        int i = 0;
        StringBuffer sb = new StringBuffer():
        while(i < 100){
            sb.append(str);
            i++;
        }
        return sb.toString():
    }

JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行100 次加锁/解锁),

此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。

视频教程、参考博客

关注
打赏
1636984416
查看更多评论
立即登录/注册

微信扫码登录

0.1165s