Java中共享变量的内存可见性

  • 我们首先来看一下在多线程下处理共享变量时Java的内存模型,如图所示

    Java内存模型规定,将所有的变量都存放在主存中,当线程使用变量的时候,会把主内存里面的变量赋值到自己的工作区间或者叫工作内存,线程读写变量时操作的是自己的工作内存中的变量,Java内存模型是一个抽象的概念,那么在实际中线程的工作内存是什么呢?

    图中显示的是一个双核CPU系统架构,每一个核都有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辑运算。每一个核都有自己的一级缓存。

    当一个线程操作共享变量的时,它首先从主存复制共享变量到自己的工作内存(私有内存)中,然后对工作内存的变量进行处理,处理完之后将变量值更新到主存中。假如线程A和线程B同时处理一个共享变量,会出现什么情况呢?我们使用上图2-5所示的CPU架构,假设线程A和B使用不同的CPU执行,并且当前两级cache都为空,那么由于这个时候cache的存在,将会导致内存不可见问题:

    1. 线程A首先获取到共享变量X的值,由于两级cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0值缓存到两级cache中,线程A修改X=1,然后将其写入两级cache中,并且刷新到主存中。线程A操作完毕后,线程A所在的CPU的两级cache和主存中的X都为1。
    2. 线程B获取到X的值,首选一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回了一个X=1;到这里一切都是正常的,因为这时候主内存中X=1,然后线程B修改X=2,并将其放到线程B所在的一级cache和二级cache中,最后更新主存中X=2。
    3. 线程A再次要修改X的值,获取时一级缓存中命中,并且X=1,到这里问题就出现了,明明线程B已经把X修改为2了,为何线程A读取X的值还是1呢?这就是共享变量的内存不可见问题。也就是线程B写入的值对线程A不可见。那么如何解决共享变量线程不可见的问题呢?这里就需要使用java中的volatile关键字解决这个问题,下面会讲到。

Java中Synchronized关键字

  • synchronized关键字介绍

    synchronized块是Java提供的一种原子性内置锁,Java中的每一个对象都可以看成一个同步锁来使用。这些Java内置的使用者看不到的锁被称为内置锁,也叫监视器锁。线程的执行代码块在进入synchronized代码块前会自动的获取到内部锁,这时候其他线程访问该同步代码块会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步代码块内调用了该内置锁资源的wait系列方法时会释放该内置锁。内置锁是排它锁,也就是当一个线程获取到这个锁之后,其他线程必须等待该线程释放锁后才能获得该锁。

  • synchronized的内存语义

    前面介绍了共享变量内存可见性问题主要是由于线程当中工作内存所导致的。下面我们来讲解synchronized的一个内存语义,这个内存语义就是解决共享变量内存可见性问题。进入synchronized块的内存语义是把synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时候就不会从工作内存中取,而是直接从主存中取,退出synchronized块的内存语义是把sunchronized块对共享变量的修改刷新到主存中。其实这也是加锁和释放锁的概念。当获取锁后会清空本地内存中将会用到的共享变量,在使用这些共享内存会从主存中加载,在释放锁时会将本地内存中修改的共享变量刷新到主存中。synchronized除了用来解决共享变量内存不可见问题,还可以用来实现原子性操作。另外注意的是,synchronized关键字会不会引起线程上下文切换并带来线程调度开销。

Java中volatile关键字

  • 上面介绍的是使用锁的方式可以解决共享变量内存不可见问题。但是使用锁太笨重,因此它会带来线程上下文切换问题。对于解决内存可见性问题,Java还提供了一种弱形式的同步,也就是使用volatile关键字,该关键字确保一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量的时候不会把值缓存到寄存器或者其他地方,而是会把值刷新返回到主存中。当其他线程读取该共享变量的时候,会直接从主存中重新获取到最新值。而并不是使用工作内存中的值。voltile内存语义和synchronized语义有相似之处,当线程写入volatile变量值的时候就等于线程退出synchronized同步块(把写入工作内存中共享变量的值同步到主内存),读取volatile变量值时就相当于进入进入同步代码块(先清空本地内存中共享变量值,再从主存中获取到最新值)。

  • 下面使用volatile关键字解决内存可见性问题的例子,如下代码中的共享变量value就是不安全的,因为这里没有适当的同步措施。

    public class ThreadNotSafeInteger {
    private int value; public int getValue() {
    return value;
    } public void setValue(int value) {
    this.value = value;
    }
    }
  • 首先来看使用synchronized关键字进行同步的方式

    public class ThreadNotSafeInteger {
    private int value; public synchronized int getValue() {
    return value;
    } public synchronized void setValue(int value) {
    this.value = value;
    }
    }
  • 然后使用volatile进行同步

    public class ThreadNotSafeInteger {
    private volatile int value; public int getValue() {
    return value;
    } public void setValue(int value) {
    this.value = value;
    }
    }
  • 在这里使用volatile和synchronized是等价的。都解决的共享内存变量value不可见问题。但是前者是独占锁,其他线程调用会被阻塞等待,同时还存在线程上下文切换个线程重现调度的开销。这也是使用锁方式不好的地方。后者使用的是非阻塞算法,不会造成线程上下文切换的开销。

Java中原子性操作

  • 所谓原子操作,是指执行一系列操作要么一次性全部执行完,要么全部都不执行。如果不能保证操作室原子性操作,那么就会出现线程安全问题,如下:

    public class ThreadNotSafeCount {
    private Long value; public Long getValue() {
    return value;
    } public void setValue(Long value) {
    this.value = value;
    } private void inc() {
    ++value;
    }
    }

    首先执行javac ThreadNotSafeCount.java命令

    然后执行javap -c ThreadNotSafeCount.class命令

    Compiled from "ThreadNotSafeCount.java"
    public class com.heiye.learn2.ThreadNotSafeCount {
    public com.heiye.learn2.ThreadNotSafeCount();
    Code:
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return public java.lang.Long getValue();
    Code:
    0: aload_0
    1: getfield #2 // Field value:Ljava/lang/Long;
    4: areturn public void setValue(java.lang.Long);
    Code:
    0: aload_0
    1: aload_1
    2: putfield #2 // Field value:Ljava/lang/Long;
    5: return
    }
  • 我们该如何保证多个操作的原子性呢?最简单的办法就是使用synchronized关键字进行同步,代码如下

    public class ThreadNotSafeCount {
    private Long value; public synchronized Long getValue() {
    return value;
    } public synchronized void setValue(Long value) {
    this.value = value;
    } private synchronized void inc() {
    ++value;
    }
    }

    使用synchronized关键字的确可以实现线程安全性,即内存可见性和原子性,但是synchronized是独占锁,内有获取到内部锁的线程会被阻塞掉,但是getValue()只是读操作,多个线程同时调用这个方法并不会引发线程安全问题,但是加了synchronized关键字后,同一时间只能有一个线程可以调用,这显然是不合理的,没有必要。也许会有这样一个疑惑,可以不可把这个方法上的synchronized关键字去掉呢?答案是不能的,因为这里是靠synchronized来实现共享内存可见性的,那么有没有什么更好的办法呢?,答案是有的,下面讲到的在内部使用非阻塞CAS算法实现的原子性操作类AtomicLong就是一个不错的选择。

10-Java中共享内存可见性以及synchronized和volatile关键字的更多相关文章

  1. java多线程之内存可见性-synchronized、volatile

    1.JMM:Java Memory Model(Java内存模型) 关于synchronized的两条规定: 1.线程解锁前,必须把共享变量的最新值刷新到主内存中 2.线程加锁时,将清空工作内存中共享 ...

  2. Java多线程之内存可见性和原子性:Synchronized和Volatile的比较

    Java多线程之内存可见性和原子性:Synchronized和Volatile的比较     [尊重原创,转载请注明出处]http://blog.csdn.net/guyuealian/article ...

  3. 细说Java多线程之内存可见性

    编程这些实践的知识技能,每一次学习使用可能都会有新的认识 一.细说Java多线程之内存可见性(数据挣用)         1.共享变量在线程间的可见性                共享变量:如果一个 ...

  4. Java多线程之内存可见性

    阅读本文约“3分钟” 共享变量在线程间的可见性 synchronized实现可见性 volatile实现可见性 —指令重排序 —as-if-serial语义 —volatile使用注意事项 synch ...

  5. java 语言多线程可见性(synchronized 和 volatile 学习)

    共享变量可见性实现的原理 java 语言层面支持的可见性实现方式: synchronized volatile 1. synchronized 的两条规定: 1 线程解锁前,必须把共享变量的最新值刷新 ...

  6. Java中堆内存和栈内存详解2

    Java中堆内存和栈内存详解   Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,ja ...

  7. java中的内存一般分成几部分?

    java中的内存被分成以下四部分: ①.代码区  ②.栈区  ③.堆区   ④.静态区域 栈区:由编译器自动分配释放,存放函数的参数值.局部变量的值等:具体方法执行结束后,系统自动释放JVM内存资源 ...

  8. Java SE之Java中堆内存和栈内存[转/摘]

    [转/摘]1-3Java中堆内存和栈内存 注解:内存(Memory)即 内存储器,主存,其作用是用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器(辅存)交换的数据. Java中把内存分为两种:栈 ...

  9. Java中的内存处理机制和final、static、final static总结

    Java中的内存处理机制和final.static.final static总结   装载自:http://blog.csdn.net/wqthaha/article/details/20923579 ...

随机推荐

  1. spring学习日志四

    一.spring对JDBC的支持 JdbcTemplate 简介 为了使 JDBC 更加易于使用, Spring 在 JDBC API 上定义了一个抽象层, 以此建立一个 JDBC 存取框架. 作为 ...

  2. C# 计时器用法(DispatcherTimer、System.Timers.Timer、System.Threading.Timer)

    首先,我觉得三种计时器最大的区别是:DispatcherTimer触发的内容会直接转到主线程去执行(耗时操作会卡住主线程),另外两个则是在副线程执行,如果需要修改界面,则需要手动转到主线程. Disp ...

  3. 【ArcEngine】AE连接SDE_For_SQLServer参数设置

    SDE for sqlserver直连的ArcEngine访问 Ae中的数据的连接实质还是采用服务连接的方式.连接代码如下: 1 public IWorkspace Getworkspace() 2 ...

  4. 嵌入式Linux的启动过程

    1.了解 Linux 最初是由瑞典赫尔辛基大学的学生 Linus Torvalds在1991 年开发出来的,之后在 GNU的支持下,Linux 获得了巨大的发展.虽然 Linux 在桌面 PC 机上的 ...

  5. RapidSVN设置diff和edit工具

      菜单栏 -> View -> Preferences -> Programs选择相应的配置页即可   需要配置的路径,默认都在 /usr/bin目录下的 editor可以用ged ...

  6. 关闭 Scroll Lock

    通常,在电子表格中选择一个单元格并按箭头键时,所选内容会在各个单元格之间上下左右移动,具体取决于您按的箭头键.但是,如果在 Scroll Lock 处于开启状态时按箭头键,则向上或向下滚动一行.或者, ...

  7. 一、docker部署Jenkins

    1.部署启动脚本: [root@node10 docker-data]# cat start.sh docker run -d \ --restart=unless-stopped \ -v /opt ...

  8. Jenkins手动下载并安装插件

    最近遇到Jenkins插件无法自动安装的问题,在插件管理页面的[升级站点]使用镜像url也无法解决.于是决定手动下载并安装Jenkins插件,具体步骤如下. Step1:进入Jenkins官网的插件下 ...

  9. xxs攻击

    1 XSS是一种经常出现在web应用中的计算机安全漏洞,它允许恶意web用户将代码植入到提供给其它用户使用的页面中.比如这些代码包括HTML代码和客户端脚本.攻击者利用XSS漏洞旁路掉访问控制--例如 ...

  10. 从一个跨二十年的glibc bug说起

    1. 缘起 这几天调gcc 7.5.0 +glibc 2.23的交叉编译工具链,由于gcc 7.5.0的默认打开Werr,偶然发现了glibc一个隐藏了二十年的世纪大bug. 这个bug在glibc ...