首页 > 编程语言 > 详细

并发与高并发(九)-线程安全性-可见性

时间:2019-12-15 23:24:28      阅读:117      评论:0      收藏:0      [点我收藏+]

前言

乍看可见性,不明白它的意思。联想到线程,意思就是一个线程对主内存的修改及时的被另一个线程观察到,即为可见性

那么既然有可见性,会不会存在不可见性呢?

答案是肯定的,导致线程不可见的原因是什么呢?

有三个原因:

(1)线程交叉执行。

(2)重排序结合线程交叉执行。

(3)共享变量更新后的值没有在工作内存与主存间及时更新。

主体内容

一、这里的可见性涉及到synchronized,顺便了解一些一下JMM对synchronized的两条规定:

  1.线程解锁前,必须把共享变量的最新值刷新到主内存中

  2.线程加锁时,将清空工作内存中存储的共享变量的值,从而使用共享变量时,必须从主内存中重新读取最新的值。(注意:解锁和加锁,是指同一把锁)

二、同时涉及到volatile。

  1.volatile通过内存屏障和禁止重排序优化来实现内存可见性。

  (1)对volatile变量进行的操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存。

  (2)对volatile变量进行的操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。

  这样就能保证线程读写的都是最新的值。

  此处有个小疑问,重排序是什么意思呢?举个栗子:

int a=2;
int b=1;

  从顺序上看a应该先执行,而b会后执行,但实际上却不一定是,因为cpu执行程序的时候,为了提高运算效率,所有的指令都是并发的乱序执行,如果a和b两个变量之间没有任何依赖关系,那么有可能是b先执行,而a后执行,因为不存在依赖关系,所以谁先谁后并不影响程序最终的结果。这就是所谓的指令重排序

  然后,我们简单的通过两张图分别看一下读写操作时的过程。

技术分享图片                     

      volatile写插入内存屏障示意图

 

  技术分享图片

       volatile读插入内存屏障示意图

2.那么猜想一下,如果我们用volatile修饰之前我们计数器的变量,会不会得到线程安全的结果呢?

package com.controller.volatile_1;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import com.annoations.NotThreadSafe;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@NotThreadSafe
public class VolatileTest {
    //请求数
    public static int clientTotal=5000;
    //并发数
    public static int threadTotal=200;
    //计数值
    public static volatile int count=0;
    
    public static void main(String[] args) throws InterruptedException{
        //创建线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定义信号量(允许并发数)
        final Semaphore semaphore = new Semaphore(threadTotal);
        //定义计数器
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for(int i =0;i<clientTotal;i++){
            executorService.execute(()->{
                try {
                    //.acquire方法用于判断是否内部程序达到允许的并发量,未达到才能继续执行
                    semaphore.acquire();
                    add();
                    //.release相当于关闭信号量
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception",e);
                }
                countDownLatch.countDown();
            });
        }
        //等待计数值为0,也就是所有的过程执行完,才会继续向下执行
        countDownLatch.await();
        //关闭线程池
        executorService.shutdown();
        log.info("count:{}",count);
    }
    
    private static void add(){
        count++;
    }
}

结果发现出现了:

21:45:10.666 [main] INFO com.controller.volatile_1.VolatileTest - count:4914

由此可见,即使给计数器加上volatile,也无法保证线程安全,上面的猜想错误!那么错误的原因是什么呢?

答:其实在执行add()方法中的count++操作的时候执行了三步,哪三步呢?

(1)取出内存里的count值,这时的count值是最新的,是没有问题的

(2)进行+1操作

(3)重新将count写回主存

问题就出现了,当两个线程同时运行count++这个操作,如果两个线程同时给count进行+1操作,并同时写回主存,这一来,count本该算起来+2,最终结果却只+1。

最终说明volatile这个关键字不具备原子性。

3.如果说volatile不适合计数的这种场景,那么它会适用于什么场景呢?下面来正式谈一谈volatile的使用。

通常来说,使用volatile必须具备两个条件:

(1)对变量的写操作不依赖于当前值。

(2)该变量没有包含在具有其他变量的不变式中。

因此,volatile特别适合作为状态标记量。下面看一个例子:

    volatile boolean inited =false;
    
    //线程一
    context = loadContext();
    init = true;
    
    //线程二
    while(!inited){
        sleep();
    }
    doSomethingWithConfig(context);    

解释:这里面有两个线程,线程二的执行必须保证初始化完成,线程一中的context = loadContext()表示初始化,init=true给其打一个初始化完成的标识,当init被打为true,一直观察的线程二立马就知道上面的初始化已经完成,然后走到下面这个doSomethingWithConfig(context)操作里来,这时候线程二使用已经初始好的context也不会出现问题了。

并发与高并发(九)-线程安全性-可见性

原文:https://www.cnblogs.com/jmy520/p/12046052.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!