Synchronized与Volatile详解

Synchronized

想搞明白 synchronized的机制,我们需要先搞清楚CAS方法的底层原理。

CAS

多线程的情况下,i++可能会出现脏数据情况,所以需要加synchronized 。
jdk 1.5之后出现了CAS方法,一种乐观锁,自旋锁,在线程安全的情况下可以进行数据更改
CAS操作时会拿到三个值(目前值,期望值,结果值),当目前值==期望值,则对数据进行操作。
可能会出现ABA问题。其他线程对数据进行操作,操作之后又将数据改为期望值。
解决方法:对数据加version。只要出现更改,则将version+1;

CAS底层

CompareAndSwap 比较和交换,CAS底层由C++代码调用汇编指令实现,如果是多核cpu则使用 lock cmpxchg指令,单核则compxch指令。

CPU层级CAS过程:从内存中读取数据到cpu寄存器中,在寄存器中把数据更改,将数据返回内存前检查内存中的数据是否发生改变,如果一致则将数据放回。只要不成功则一直循环。

同时 lock cmpxchg指令也是 synchronized和volatile的底层实现

AtomicInteger中CAS的实现:

有一个increamAndGet()方法

public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

其中 调用unsafe类的getAndAddInt()

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
}

synchronized锁升级过程:

无锁–>偏向锁–>轻量级锁–> 自旋锁

当加synchronized关键字后,会生成monitorenter字节码监视,当sunchronized代码块结束后会执行monitorexit退出监视。

在监视过程中,会执行C++代码,大概过程如下:

if(支持偏向锁){
	执行 fast_enter快速上锁
}else{
	执行 slow_enter慢速上锁
}
fast_enter(){
	为对象加偏向锁
	如果偏向锁失败了,就会执行 slow_enter();
}
slow_enter(){
	为对象加轻量级锁(自旋锁、无锁)
	如果失败,则进行锁粗化。膨胀为重量解锁
}

对象的内存布局

在这里插入图片描述
我们使用JOL包里面的 ClassLayout.parseInstance()方法打印出 Object o对象的对象头详细信息:

 Object o=new Object();
        String s= ClassLayout.parseInstance(o).toPrintable();
        System.out.println(s);
//结果如下:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header) (前两个称为MarkWor)       00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)  (指向类)                 e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

new一个新的对象时,大小为16个字节。最后四个四节用于对齐。任何一个对象的大小必须是8的倍数。

对象头MarkWord:

在这里插入图片描述

锁升级:

  • 偏向锁:某个线程进入synchronized时,将自己的线程ID存放在对象头中,之后CPU轮询其他线程,此线程中断。等恢复后此线程查看对象头中的线程ID如果是自己,则继续使用

  • 轻量级锁(自旋锁):当偏向锁状态时有其他线程来争夺资源时,进入轻量级锁。 对象头内记录了指向栈中锁记录的指针。进入轻量级锁时,多个线程使用CAS方式抢夺这个指针(使其的值等于线程自身内的LR(LockRecord 锁记录)地址) 。此时对象头中的其他信息(对象的HashCode、分代年龄等)会存放在线程栈中

  • 重量级锁:在竞争加剧时(有些线程自旋次数超过10次、自旋线程数超过CPU核数的一半)会升级为重量级锁。重量级锁会惊动操作系统,从用户态转换为内核态,操作系统会进行调度。 其他线程想使用资源时,首先转换为内核态,然后排队等待资源

  • 对于是否竞争加剧的判断,1.6之后加入了自适应自旋,整个过程由JVM自行判断

锁消除

public void add(String str1,String str2){
    StringBuffer sb=new StringBuffer();
    sb.append(str1).append(str2);
}

当某个对象不可能被其他线程引用(比如局部变量,栈私有)时,JVM会自动消除对象的锁。如上代码中,sb使StringBuffer对象,属于线程安全。但sb为局部变量,只能在add方法中调用

锁粗化

public String test(String str){
	int i=0;
    StringBuffer sb=new StringBuffer();
    while(i<100){
        sb.append(str);
        i++;
    }
    return sb.toString();
}

JVM会检测到一连串的操作都是堆同一个对象加锁,此时JVM会把加锁的范围粗化到这串操作的外部,使得只加一次锁。

volatile

volatile的用途

  1. 线程可见性

    某个值在线程A中被改变后,会将值刷新到主内存,通知其他线程去获取新的值。

  2. 防止指令重排序

    指令重排序:CPU在读等待同时会执行不影响结果的其他指令,而写的同时可以进行合并写。

volatile实现细节

加了volatile关键字后,编译出的字节码中会出现 ACC_VOLATIlE修饰符,告诉虚拟机这个部分需要实现内存屏障。

内存屏障实现

  1. JVM层级内存屏障

    四种内存屏障

    1. StroreStoreBarrier 两条写指令之间不可以重排序
    2. StoreLoadBarrier 写指令在前,读指令在后,不可以重排序
    3. LoadLoadBarrier 两条读指令之间不可以重排序
    4. LoadStoreBarrier 读指令在前,写指令在后,不可以重排序
  2. CPU层级内存屏障

    1. 在指令前加 Lock。(HotSpot虚拟机是通过Lock指令实现Volatile)
      1. 汇编指令:sfence、lfence、mfence
      2. MESI协议 (CPU缓存一致性协议) MESI对应缓存的四种状态,M 修改;E独享互斥;S共享;I无效。
      3. 锁总线。(总线:计算机中的IO总线)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值