Java内存与volatile的分析

cpu cache的出现

由于CPU直接访问主存的速度过慢,导致CPU资源受到很大限制,降低了CPU的整体的吞吐量。所以就有了CPU缓存的出现。现在的缓存一般有2-3级,最靠近CPU的是1级缓存。CPU的cache比较大,一般128KB,被分为多个固定大小的cache line,cache line通常是32byte或者64byte.

CPU缓存模型

 

因为CPU cache的访问主存的速度比CPU直接访问主存的速度快很多,所以程序运行时,运算所需数据首先会先复制一份到cache中,CPU计算时就可以直接去cache直接或取,然后将计算结果写入到cache中,再由cache将结果刷新进主存。从而cache代替了CPU直接访问主存,极大的提高了CPU的吞吐能力。 

 

CPU cache数据一致性的问题

缓存的出现,极大提高了CPU的吞吐量,但是引入了缓存不一致的问题。比如i++操作,在程序运行时,会先从主存复制一份数据到cache中,然后CPU寄存器直接从缓存中获取数据进行计算,再写入cache中,最后将cache的结果刷新到主存中。如果在单线程模式下,此方式没什么问题,如果是多线程下 访问就会出现数据不一致的问题。

CPU解决缓存不一致的问题主要有两种方式

1、总线加锁(CPU与其他组件通信都是靠数据总线、控制总线、地址总线)

2、缓存一致性协议

总线加锁机制效率低,因为每次只要一个总线能获取到锁,其他总线就需要等待,这是一只悲观锁。

缓存一致性协议,最出名的是MESI协议。此协议保证了缓存使用的每一个共享变量都是是一致的。大致的思想:CPU操作cache的数据是,发现变量是一个共享变量,也就是说在其他缓存也存在这样的一个副本,它大概会做如下的操作

1、读取操作,不做任何操作,寄存器直接获取cache中的值。

2、写入操作,发出信号通知其他cache中的改变了cache line置为无效状态,其他CPU当需要读取这个共享变量的时候就必须去主存重新获取。

 

Java内存模型

java内存模型(java memory mode,JMM)指定了Java虚拟机如何与计算机的的主存进行工作。JMM决定了一个线程对共享变量何时对其他线程可见,JMM定义了线程和主存之间的抽象关系:

A、共享变量保存在主内存中,每个线程都可以访问

B、每个线程都有自己的工作内存或者称本地内存

C、工作内存只存储改线程对共享变量的副本

D、线程不能直接操作主存,必须先操作工作内存,然后由工作内存刷新到主存

E、工作内存和JMM都是抽象概念

 

并发编程的特性

并发编程的三个重要特性,原子性,有序性,可见性

  • 原子性:一次操作或者多次操作,要么都成功要么都失败。比如i++并不是原子性(get i,i+1,set i)
  • 有序性:处理器为了提高运行的效率,会进行指令重排序,指令重排序会严格遵守指令之间的数据依赖关系。有序性保证了无论指令如何重排序,最终的结果必须和顺序执行的结果一致。
  • 可见性:一个线程修改了共享变量,那么其他线程可以立即看的到修改后的最新值。
  •  

JMM如何保证三大特性

1、原子性

Java语言中,对基本数据类型的读取和赋值是原子操作,对引用类型的变量读取和赋值操作也是原子性的,因此此类操作是不可被打断的。

  • 多个原子操作在一起就不是原子操作了
  • 简单的读取和赋值操作是原子操作,将一个变量赋值给另一个变量就不是原子操作(首先获取变量值再赋值给另一个变量)
  • JMM只保证简单的读取和赋值操作是原子性的,其他均不保证

2、可见性

在多线程环境下,某个线程首次读取共享变量,需要先从主存获取到工作内存,然后再从工作内存直接读取。同样,如果修改某个共享变量,需要先写入到工作内存中,再由工作内存刷新到住内存中,但是什么时候刷新到主内是不确定的。Java是通过下述3种方式保证可见性的:

  • 使用volatile关键字。当一个变量被volatile修饰时,读取操作直接从主内存获取(当然也会缓存到工作内存中,当其他线程修改了值,就会失效,所以必须从主存中获取),当做写操作是,必须立刻将值刷新到主存中。当然也会先写入到工作内存,然后在立刻刷新到主存。
  • 使用synchronized关键字,在释放锁之前会将对变量的修改值刷新到主存中
  • 通过JUC的显示锁lock,在释放锁之前会将对变量的修改值刷新到主存中

3、有序性

在JMM中,允许编译器和处理器对指令的重排序,在单线程下没问题,如果在多线程下就会出现问题。JMM具备天生的有序规则,所以不需要同步手段就可以保证有序性,这个规则就是happens-before规则。

 

volatile关键字

Volatile只能修饰实例变量和类变量。

volatile语义:

1、保证了不同线程之间的对共享变量的可见性,即一个变量被修改了,其他线程可以立即可见。

2、禁止指令重排序

所以volatile只保证可见性和有序性。volatile并不保证原子性。

 

理解volatile保证可见性

例如一个简单的多线程例子:

1、线程1从主存获取intValue的值到工作内存中

2、线程1将intValue的值修改为1,并将其刷新到主存中

3、线程2工作内存中的intValue的值失效

4、线程2需要从主存再次获取intValue值到工作内存中使用

其实实现的主要原因是因为只要是volatile修饰的过的指令,在他修改值之后会立即刷新至主存中,并通知其他线程的值为无效值。

 

理解volatile保证有序性

Volatile直接禁止JVM对有volatile关键字修饰过的指令重排序,但是对于前后无依赖关系的指令可以随便排序。

int x =0;
int y = 10;
volatile int z = 20;
x++;
y--;

z的赋值跟x、y没有依赖关系,可以重排序。

private volatile boolean flage = false;
private Context context;
public Context load(){
    if(!flage){
        context = doLoad();
        flage = true;   
    }

    return context;
}

在此段代码中,如果if内重排序,线程1重排序后修改了为true,线程2发现可以直接调用,但是如果context还未初始化完毕,就会导致空指针报错。

 

volatile实现机制和原理

通过OpenJDK下的unsafe.cpp源码阅读。会发现被volatile修饰的变量存在一个“lock”的前缀,源码如下

"lock"前缀实际上相当于内存屏障,改内存屏障提供以下几个保障:

  • 确保指令重排序时不会将内存屏障后面的代码排到其之前;不会将内存屏障之前的代码排到其之后。
  • 确保在执行内存屏障修饰的指令时,其前面的代码全部被执行完成。
  • 强制将工作内存中修改的值刷新至主存中。
  • 如果是写操作,将会导致其他线程的工作内存中缓存数据失效。

 

volatile使用场景

1、利用volatile的可见性特性作开关控制。

public class ThreadTest extends Thread{

    private volatile boolean flage = true;
    
    @Override
    public void run(){
        
        while(flage){
        
            //doing
        }
    }


    public void shutDown(){
    
        this.flage = false;
    }
}

当外部线程执行ThreadTest的shutDown方法时,ThreadTest会立即看到flage的变化(flage的值会立即被刷新到缓存中)。如果没有volatile修饰,有可能线程在其工作内存中修改了其值,但并没有刷新至主存中,或者ThreadTest一直在自己的工作内存中读取flage的值,都有可能导致flage=false不生效,导致线程无法关闭。

2、利用volatile的有序性作状态标记

就是使用指令重排序的特性,在前面doLoad()方法就有体现,这里就不在叙述。

3、在单例模式中double-check模式中使用其有序性

public final class Singleton{

    private static Singleton instance = null;

    private Socket socket;

    private Singleton(){

        this.socket //初始化
    }

    public static Singleton getInstance(){
    
        if(null == instance){

            synchronized(Singleton.class){

                if(null == instance){
                    
                    instance = new Singleton();
                }
            }
        }

        return instance;

    }

}

分析:当两个线程同时进入null == instance时候,只有一个线程能获取到锁,完成对instance的初始化,随后的线程发现instance不为null,则不需要做任何操作了,以后对getInstance不需要数据同步的保护了。

虽然这种方案既满足了懒加载有保证的instance的唯一性,但是这种方式在多线程环境下还是有可能发生空指针的情况。在Singleton构造函数中还需要实例化Socket资源,还有Singleton自身,根据JVM运行时指令重排序和happen-before规则,这两者实例化的顺序无前后关系的约束,那么就有可能在sokcet未完成实例化,而instance已经完成实例化了,这个时候如果调用socket的方法就会报空指针的错误。

如果有volatile关键字的修饰,就可以防止指令重排序,保证实例化了instance之前,socket是被实例化的。

修改: private volatile static Singleton instance = null;

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值