多线程与高并发 -- synchronized & volatile

多线程与高并发 – synchronized & volatile

1、synchronized

synchronized是一种同步锁,可以修饰方法、代码块、类等,锁的不是方法、代码块,锁的是对象

  • 对于方法与代码块:锁住的是调用方法与代码块的对象;
  • 对于静态方法与类:锁住的是所有实例化对象

synchronized保证了代码执行的原子性,即要么全部执行成功,要么都不成功。当前线程获得锁,那么其他线程进入阻塞状态等待被唤醒竞争锁。

synchronized在执行过程锁的等级自动升级

  1. 无锁态:即虽然加上了synchronized关键字,但实际运行并不需要锁 -> 即不上锁
  2. 偏向锁:类似于贴上自身线程ID的标签,让其他线程无法执行 -> 单线程
  3. 自旋锁(无锁态):基于CAS实现,先比较再赋值。 ->多线程
    • CAS: 当线程要修改某个值时,线程先读取该值,经过一系列需要进行的操作后,再读取一遍,如果该值没有变,则赋予新值;如果值改变,则再进行一次(读取、系列操作、再读取),直至值前后没有变化。这是一个不断自旋的过程,不断地消耗CPU
    • 在这个过程可能涉及到了一个ABA问题,ABA问题就是该值虽然前后没有改变,但已经经过某个线程的一系列操作,这一系列操作可能对后来其他线程的执行产生影响,可以通过添加版本号的方式解决,虽然该值没有变化,但是版本号改变
  4. 重量级锁:当一个线程执行时,其他线程不能抢占,而是进入一个等待队列,只有等到上一个线程执行完毕,其他线程抢锁,获得锁的线程执行 -> 多线程竞争

当锁升级为重量锁时,就不能再退回轻量级锁了,即锁能升级无法降级。

synchronized底层实现过程:

  1. 同步代码块

    字节码:进入方法前添加monitorenter指令、推出方法后添加一个monitorexit指令

    执行过程自动升级

    底层代码: lock

  2. 同步方法

    字节码:ACC_SYNCHRONIZED 标识,相当于告诉 JVM 这是个同步方法

本质上都是获取monitor对象监视器

2、volatile

2.1 线程可见性

当某个值被某个线程改动时,对其他线程该值的变化是可见的,即保证变量的可见性。

注意:不能保证多个线程共同修改running变量时所带来的不一致性,即不能保证原子性,若多个线程同时对该变量写操作,还是会造成数据丢失问题。

当前的 Java 内存模型下,线程可以把变量保存本地内存中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致

  • 本地内存 :每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。

使用volatile关键字指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

2.2 防止指令重排

CPU存在乱序执行,使用volatile可以禁止指令重排。

以单例模式为例子:详情看博客单例模式

public class Singleton {
    private static Singleton instance;
    private String name;

    public String getName() {
        return name;
    }
    //构造函数私有化--即无法在类外通过Singleton singleton = new Singleton()创建实例化对象
    private Singleton(){
    }
    public void setName(String name) {
        this.name = name;
    }
    //被调用时才实例化对象
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized (Singleton.class){
            	if(instance == null){
            		instance = new Singleton();	
            	}
            }
        }
        return instance;
    }
}

创建对象可以分为三个步骤:

  1. new 分配空间
  2. 初始构造方法
  3. 赋值给对象

前景:假设线程1进行判断为空后,线程2判断为空进行锁资源、释放锁住的资源;线程2实例化对象,线程1锁住资源,再判断一次是否为空,结果不为空,获取得到线程2创建的对象。

问题 :若线程2创建对象时指令重排,2、3步骤调换,即先将引用地址赋值给instance变量后,再执行构造方法,那么线程1拿到的将是未初始化完毕的单例对象

解决:使用volatile修饰 - > private static volatile Singleton instance 防止指令重排

2.3 内存屏障

内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。

内存屏障的作用:

  • 阻止屏障两侧的指令重排序;
  • 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据;

对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad,实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。

LoadLoad屏障: 语句:Load1; LoadLoad; Load2

在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障: 语句:Store1; StoreStore; Store2,

在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障: 语句:Load1; LoadStore; Store2,

在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障: 语句:Store1; StoreLoad; Load2,

在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。即保证写入的数据在被读之前已更新到主存。

它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

  • 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
  • 在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;

volatile实现过程:

  1. 源码 volatile
  2. 字节码 ACC_volatile
  3. jvm内存屏障:指令不可重排,保障有序
  4. 底层代码:lock

~~分割线 ~~

注:内存屏障部分参考该博客并发关键字volatile(重排序和内存屏障)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值