多线程与高并发 – synchronized & volatile
1、synchronized
synchronized是一种同步锁,可以修饰方法、代码块、类等,锁的不是方法、代码块,锁的是对象。
- 对于方法与代码块:锁住的是调用方法与代码块的对象;
- 对于静态方法与类:锁住的是所有实例化对象
synchronized保证了代码执行的原子性,即要么全部执行成功,要么都不成功。当前线程获得锁,那么其他线程进入阻塞状态等待被唤醒竞争锁。
synchronized在执行过程锁的等级自动升级:
- 无锁态:即虽然加上了synchronized关键字,但实际运行并不需要锁 -> 即不上锁
- 偏向锁:类似于贴上自身线程ID的标签,让其他线程无法执行 -> 单线程
- 自旋锁(无锁态):基于CAS实现,先比较再赋值。 ->多线程
- CAS: 当线程要修改某个值时,线程先读取该值,经过一系列需要进行的操作后,再读取一遍,如果该值没有变,则赋予新值;如果值改变,则再进行一次(读取、系列操作、再读取),直至值前后没有变化。这是一个不断自旋的过程,不断地消耗CPU
- 在这个过程可能涉及到了一个ABA问题,ABA问题就是该值虽然前后没有改变,但已经经过某个线程的一系列操作,这一系列操作可能对后来其他线程的执行产生影响,可以通过添加版本号的方式解决,虽然该值没有变化,但是版本号改变
- 重量级锁:当一个线程执行时,其他线程不能抢占,而是进入一个等待队列,只有等到上一个线程执行完毕,其他线程抢锁,获得锁的线程执行 -> 多线程竞争
当锁升级为重量锁时,就不能再退回轻量级锁了,即锁能升级无法降级。
synchronized底层实现过程:
-
同步代码块
字节码:进入方法前添加
monitorenter
指令、推出方法后添加一个monitorexit
指令执行过程自动升级
底层代码: lock
-
同步方法
字节码:
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;
}
}
创建对象可以分为三个步骤:
- new 分配空间
- 初始构造方法
- 赋值给对象
前景:假设线程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实现过程:
- 源码 volatile
- 字节码 ACC_volatile
- jvm内存屏障:指令不可重排,保障有序
- 底层代码:lock
~~分割线 ~~
注:内存屏障部分参考该博客并发关键字volatile(重排序和内存屏障)