并发原理、Java 内存模型 (JMM)
image线程共享变量存储在主内存中,每个线程都有一个本地的私有内存,本地内存中存储着该线程以读或写共享变量的副本,本地内存是一个抽象概念,它涵盖了缓存、写缓冲区、cpu寄存器
线程要读取一个共享变量,会先将其从主内存中读取到本地内存,然后进行运算,最后在将共享变量写回主内存
并发产生的原因
原因:
1.操作的非原子性
2.多个线程之间的内存不可见性
解决:
- volatile:多线程内存可见性,对单个变量的读或写操作是原子性的
- CAS: 对单个变量的 读-改-写 操作原子性
- synchronize: 对同步区域的代码具有原子性和可见性
一般情况下 CAS 都是和 volatile 一起使用的,这样既保证了变量的修改的操作的原子性,又保证了变量的可见性。Java 中 Lock 还有原子类的实现就是基于 CAS 和 volatile
volatile
内存语义:
- 读写具有原子性:对任意单个volatile变量的读或写具有原子性,但是对于 i++ 这样的符合操作是不具有原子性的
- 禁止指令重排序:利用内存屏障来禁止volatile 前后的指令重新排序
- 及时刷新内存:把缓冲区的数据刷新到主内存中,并且使其他线程的缓存区的数据无效(这样其他线程在操作变量时会重新从主内存拉取新数据)
指令重排序
JVM 指令重排是为了优化执行速度,在单线程下指令重排不会影响到程序的执行结果,因为具有关联关系的指令是不会被重排的,但是在多线程下指令重排就不保证最终结果的正确性了
例如:当如果线程A指令重排,2 先于 1 执行,然后线程B就会进入if方法,但是此时 a 还未赋值,就会出现 i 的结果为 0;
class ReorderExample{
int a = 0;
boolean flag = false;
//线程A执行该方法
public void writer(){
a = 1; ----------- 1
flag = true;------- 2
}
//线程B执行该方法
public void reader(){
if(flag){ ----------3
int i = a*a; ---4
}
}
}
读写原子性和立即刷新内存
因为读或写的操作具有原子性,所以同一时间只会有一个线程对单个 volatile 进行读或写,并且写完后立刻刷新到主内存,这样的话其他线程无论何时访问主内存获取到的都是最新的值
CAS(compareAndSwap)
实现原理:底层指令
//获取 Unsafe
private static final Unsafe unsafe = Unsafe.getUnsafe();
//该方法为 native 方法,在 Unsafe 类里面
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
先读取变量的值判断该变量是否是被其他线程修改过,如果没有修改就更新,并且返回true,如果发现变量被修改了则返回false,开发者可以根据返回结果进行自旋重试。
此操作具有volatile 读和写的内存语义,即对单个变量读写的原子性,正是因为这样才能正确的读取到内存中的值,从而进行判断内存中的值和当前的预期的值是否一致。
synchronized
被称之为重量级锁,1.6 对其进行了优化,引入偏向锁和轻量级锁,使其不是那么重了。
实现原理:
synchronized 的锁是存储在对象头中的,
将锁放入对象头中,每个线程利用CAS争抢锁,在代码执行进入到同步块的时候获取锁,结束时释放锁,如果没有锁竞争的情况下只有一个线程,还是会执行获得锁和释放锁这样的操作。
java1.6 为了减少获得锁和释放锁带来的性能消耗,在没有锁竞争的时候使用 偏向锁 ,获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出 同步块时不需要进行CAS操作来加锁和解锁。
当出现锁竞争时,会升级为 轻量级锁
如果在轻量级锁竞争时失败了,会升级为 重量级锁
CAS、volatile、synchronized 的优缺点:
- volatile 和 CAS 只能实现单个变量的读或写的安全性,但是如果是像更新变量这种操作包含了三步操(读-运算-写),这种情况 volatile 已经无法保证线程安全了。原因是这个操作是非原子操作
- CAS 存在ABA问题,重试机制会一直消耗cpu资源
- synchronized 可以对多个步骤实现线程安全操作,性能不如 volatile 和 CAS
下面演示了,普通变量、volatile变量和Atomic变量的原子性,其中 Atomic 类的底层实现就是利用 CAS+重试
public class AtomicTest {
/**使用 volatile 修饰不能保证 volatileCount = volatileCount + 1 线程安全;*/
private volatile static int volatileCount;
private static int count;
private static AtomicInteger atomicCount = new AtomicInteger();
public static void main(String[] args){
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<1000;i++){
executorService.execute(new Runnable() {
@Override
public void run() {
//非原子性操作
count++;
//原子性操作
atomicCount.getAndIncrement();
//非原子性操作
volatileCount = volatileCount + 1;
}
});
}
//等待上面任务执行完成
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印出线程池信息,检查线程是否执行完了
println(executorService.toString());
//volatileCount 和 count 的值有可能小于 1000
println("count= "+count);
println("atomicCount= "+atomicCount.get());
println("volatileCount= "+volatileCount);
}
}
说明 volatile 不能保证复合运算的安全性
class VolatileFeaturesExample {
volatile long vl = 0L; // 使用volatile声明64位的long型变量 public void set(long l) {
vl = l; // 单个volatile变量的写
}
public void getAndIncrement () {
vl++; //复合(多个)volatile变量的读/写
}
public long get() {
return vl; // 单个volatile变量的读
}
}
class VolatileFeaturesExample {
long vl = 0L; // 64位的long型普通变量
public synchronized void set(long l) {// 对单个的普通变量的写用同一个锁同步
vl = l;
}
public void getAndIncrement () {
// 普通方法调用
long temp = get(); //调用已同步的读方法
temp += 1L; // 普通写操作
set(temp); // 调用已同步的写方法
}
public synchronized long get() {
// 对单个的普通变量的读用同一个锁同步 return vl;
}
}
网友评论