并发与volatile总结

并发基本问题

要想并发程序正确地执行,必须要保证原子性可见性,以及有序性

  • 原子性

    即一个操作或多个操作,要么全部执行成功,且执行的过程不被打断,要么就不执行。

    经典的例子是转账问题:从A账户中扣100元,再在B账户中加100元。两个操作要么全部执行成功,要么不执行,这就是操作的原子性

  • 可见性

    即当多个线程访问同一变量时,若一个线程修改了该变量的值,其他的线程能够立即看到修改后的值,如

    //下面是线程1执行的代码
    int i = 0;
    i = 10;
    //下面是线程2执行的代码
    j = i;
    

    线程一执行时,会先将0写入到CPU缓存中,接着赋值为10,最后再写入主存。

    若在线程一将10赋值给i后,写入到主存之前,线程二执行 j = i;,它会先去主存读取i的值,并加载到它的工作缓存中,此时主存中的i是0,那么就会使j为0。这就产生了可见性问题:线程一对i做了修改后,线程二并没有立即看到修改后的i值

  • 有序性

    即程序执行的顺序按照代码的先后顺序,如下

    int i = 0;    //语句1
    int j = 1;    //语句2
    i++;
    j++;
    

    语句1和语句2,从代码顺序上看,1在2的前面,但在JVM真正执行这段代码时,并不能保证1就在2前面,因为可能会发生指令重排(Instruction Reorder)

    指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中的各个语句的执行顺序和代码中的顺序一致,但是会保证程序最终执行的结果和代码顺序执行的结果是一致的。上述代码中,语句1和2谁先执行对最终结果并没有影响,那么这种语句是可能发生重排的。虽然会进行指令重排,但是相互之间有数据依赖的指令会被考虑,若指令a会用到指令b的结果,那么处理器会保证b在a之前执行。指令重排对单线程的执行结果没有影响,但是在涉及到有共享变量的多线程中,则需要考虑指令重排的问题。

    指令重排可以分为编译器重排处理器重排,编译器的重排才是真正对指令顺序做了调整,处理器的重排并不是刻意为之,而是由于CPU是流水线执行的,可能由于后面的指令命中cache而先于前面的指令完成,也可能前面的指令执行时间较长,而后面的指令执行较短而先完成。

    为什么会发生指令重排呢?-> 参考链接

java语言本身对于原子性可见性有序性,都提供了哪些保证呢?

  • 原子性:java中,对基本数据类型的变量的读取赋值的操作都是原子性的,如下

    x = 10;   //语句1
    y = x;	  //语句2
    x++;      //语句3
    x = x +1; //语句4
    

    上述代码中,只有语句1是原子性操作。

    语句2会先读取x的值,然后再赋值给y,包含了2个原子性操作。

    语句3和4先读取x的值,然后执行+1操作,最后写入x

  • 可见性:java中,提供了volatile关键字来保证可见性

    当一个共享变量被volatile修饰时,它会保证修改的新值立即更新到主存中,当其他线程需要读取该变量时,会直接去主存中读取新值,另,通过synchronizedLock也能保证可见性,因为它们能保证同一时刻只有一个线程获取锁,并且执行同步代码,释放锁之前会将变量的修改更新到主存中。

  • 有序性:java内存模型中,允许编译器处理器对指令进行重排,重排不会影响到单线程,却会影响到多线程并发的正确性。在Java中,可以通过volatile来保证可见性,通过synchronized和Lock来保证有序性

volatile解析

java内存模型规定所有的变量都存在主存(内存)中,每个线程都有自己的工作缓存(类似于CPU高速缓存),线程对变量的所有操作都是在工作缓存中进行,而不能直接对主存进行操作,每个线程不能访问其他线程的工作缓存。如i = 10;这条语句,执行线程必须先在自己的工作缓存中对变量i进行赋值操作,然后再写入到主存里,而不是直接将数值10写入主存。

volatile的两层语义:

一旦一个共享变量(类的成员变量,类的静态成员变量)被volatile修饰后,它就具备了两层语义

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了该变量的值,这个新的值对其他线程来说是立即可见的
  • 禁止进行指令重排

注意:volatile并不能保证原子性,它只能保证每次读取到的是最新的值,所以仅仅用volatile来实现并发程序的正确执行,是不够的。例子如下:

public class TestVolatile {
    public volatile int index = 0;
    public void incre(){
        this.index++;
    }
}
public class Test{
	private static final int MAX_THREADS = 10;
    public static void main(String[] args) throws InterruptedException{
        final TestVolatile test = new TestVolatile();
        final CountDownLatch coutDown = new CountDownLatch(MAX_THREADS);
        for (int i = 0; i < MAX_THREADS; i++) {
            //启动10个线程
            new Thread(){
                @Override
                public void run(){
                    //每个线程对test执行1000遍incre()调用
                    try{
                        for (int j = 0; j < 1000; j++) {
                            test.incre();
                        }
                    }finally{
                        System.out.println("线程"+Thread.currentThread().getName()+"执行结束");
                        coutDown.countDown();
                    }
                }
            }.start();
        }
        //等待全部线程执行完毕
        coutDown.await();
        System.out.println("全部线程执行完毕,test.index="+test.index);
    }
}

每次的执行结果都会小于10000

.....
线程Thread-8执行结束
线程Thread-7执行结束
线程Thread-9执行结束
全部线程执行完毕,test.index=9853

这是因为自增操作并不是原子性操作

使用以下任意一种方式可实现上述程序的正确执行

  • 使用synchronized修饰自增方法incre

    public class TestVolatile {
        public volatile int index = 0;
        public synchronized void incre(){
            this.index++;
        }
    }
    

    执行结果如下

    ...
    线程Thread-5执行结束
    线程Thread-8执行结束
    线程Thread-9执行结束
    全部线程执行完毕,test.index=10000
    
  • 使用Lock(在incre方法内部使用Lock)

    public class TestVolatile {
        public volatile int index = 0;
        Lock lock = new ReentrantLock();
        public void incre(){
            lock.lock();
            try{
                this.index++;
            }finally{
                lock.unlock();
            }
        }
    }
    
  • 使用AtomicInteger

    public class TestVolatile {
        public AtomicInteger index = new AtomicInteger();
        public void incre(){
                this.index.getAndIncrement();
        }
    }
    

    jdk1.5的java.util.concurrent.atomic包下提供了一些原子操作类,对基本数据类型的自增,自减,加法,减法操作进行了封装,保证这些操作是原子性操作。atmoic包下的类是通过CAS(Compare and Swap)来实现原子性操作的

volatile可以禁止指令重排,一定程度上保证有序性

  • 当程序执行到volatile变量的读写操作时,在其前面的操作肯定已经全部完成,且结果对后面的操作可见,在其后面的操作肯定都还没执行
  • 在进行指令重排时,不能将在对volatile变量访问的语句之前的语句放在其后执行,也不能将volatile变量后的语句放到其漆面执行
public int x;
public int y;
public volatile boolean flag;

x = 2;           //语句1
y = 3;           //语句2
flag = true;     //语句3
x = 4;           //语句4
y = -1;          //语句5

通俗来说,语句3的位置一定是不变的:在执行到语句3时,1和2一定是已经执行完成了,且1和2的执行结果对3,4,5是可见的,并且此时4,5一定还没有被执行。但是语句1和2的顺序,是不被保证的,语句4和5的顺序也一样不会被保证。

volatile的原理和实现机制

下面的内容讨论一下volatile到底如何保证可见性禁止指令重排

以下内容摘自《深入理解Java虚拟机》

观察加入volatile关键字和没有i骄傲如volatile关键字时所生成的汇编代码可以发现,加入了volatile关键字时,会多出一个lock前缀指令

lock前缀指令实际相当于一个内存屏障(-> 参考链接),内存屏障会提供3个功能

  • 确保指令重排序时,不会把屏障后面的指令排到屏障前面,也不会把前面的指令排到后面
  • 会强制将对缓存的修改立即写入主存
  • 若是写操作,会导致其他CPU中对应的缓存行无效(其他线程若要访问对应的变量,需要重新去主存中取)

内存屏障

Memory Barrier,有时也叫做内存栅栏(Memory Fence),是一种CPU指令,用于控制特定条件下的重排序内存可见性问题,Java编译器也uhi根据内存屏障的规则禁止重排序

内存屏障可以被分为以下几个类型:

  • LoadLoad屏障

    对于语句Load1; LoadLoad; Load2; 在Load2以及后续读取操作之前,保证Load1要读取的数据被读取完毕

  • StoreStore屏障

    对于语句Store1; StoreStore; Store2;在Store2以及后续的写入操作之前,保证Store1的写入操作对其他处理器可见

  • LoadStore屏障

    对于语句Load1; LoadStore; Store2;在Store2以及后续的写入操作之前,保证Load1要读取的数据被读取完毕

  • StoreLoad屏障

    对于语句Store1; StoreLoad; Load2; 在Load2以及后续的读取操作之前,保证Store1的写入操作对其他处理器可见。该屏障是4种屏障中开销最大的,在大多数处理器中,这个屏障是万能屏障,兼具其他3种屏障的功能

volatile的使用场景

synchronized是为了防止多个线程同时执行一段代码,这样很影响程序的并发效率,而volatile在有些情况下的性能要优于synchronized。但是需要注意volatile无法替代synchronized,因为volatile无法保证操作的原子性,一般来讲,使用volatile需要具备以下2个条件(其实就是保证了原子性)

  • 对变量的写操作不依赖于当前值
  • 该变量没有包含在具有其他变量的不变式中 ???

下面列举几个volatile的使用场景

  • 状态标记

    //以下是2个共享变量
    volatile boolean initilized = false;
    Context context;
    
    //下面是线程1
    context = loadContext(); //加载上下文环境
    initilized = true;
    
    //下面是线程2
    while(!initilized){
        //上下文环境未就绪时,sleep
        sleep();
    }
    doSomethingWithContext(context);
    
    
    //volatile保证了,在改变状态标记时,之前的代码已经全部执行完毕
    
  • double check( 参考链接)

    //以下是一个双重校验单例模型
    class SingleTon{
        private volatile static SingleTon instance = null;
        private SingleTon(){}
        public static SingleTon getInstance(){
            if(instance == null){
                synchronized(SingleTon.class){//1
                    if(instance == null)//2
                        instance = new SingleTon();//3
                }
            }
            return instance;
        }
    }
    
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值