10-Java中共享内存可见性以及synchronized和volatile关键字
Java中共享变量的内存可见性
我们首先来看一下在多线程下处理共享变量时Java的内存模型,如图所示

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

图中显示的是一个双核CPU系统架构,每一个核都有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辑运算。每一个核都有自己的一级缓存。
当一个线程操作共享变量的时,它首先从主存复制共享变量到自己的工作内存(私有内存)中,然后对工作内存的变量进行处理,处理完之后将变量值更新到主存中。假如线程A和线程B同时处理一个共享变量,会出现什么情况呢?我们使用上图2-5所示的CPU架构,假设线程A和B使用不同的CPU执行,并且当前两级cache都为空,那么由于这个时候cache的存在,将会导致内存不可见问题:
- 线程A首先获取到共享变量X的值,由于两级cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0值缓存到两级cache中,线程A修改X=1,然后将其写入两级cache中,并且刷新到主存中。线程A操作完毕后,线程A所在的CPU的两级cache和主存中的X都为1。
- 线程B获取到X的值,首选一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回了一个X=1;到这里一切都是正常的,因为这时候主内存中X=1,然后线程B修改X=2,并将其放到线程B所在的一级cache和二级cache中,最后更新主存中X=2。
- 线程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关键字的更多相关文章
- java多线程之内存可见性-synchronized、volatile
1.JMM:Java Memory Model(Java内存模型) 关于synchronized的两条规定: 1.线程解锁前,必须把共享变量的最新值刷新到主内存中 2.线程加锁时,将清空工作内存中共享 ...
- Java多线程之内存可见性和原子性:Synchronized和Volatile的比较
Java多线程之内存可见性和原子性:Synchronized和Volatile的比较 [尊重原创,转载请注明出处]http://blog.csdn.net/guyuealian/article ...
- 细说Java多线程之内存可见性
编程这些实践的知识技能,每一次学习使用可能都会有新的认识 一.细说Java多线程之内存可见性(数据挣用) 1.共享变量在线程间的可见性 共享变量:如果一个 ...
- Java多线程之内存可见性
阅读本文约“3分钟” 共享变量在线程间的可见性 synchronized实现可见性 volatile实现可见性 —指令重排序 —as-if-serial语义 —volatile使用注意事项 synch ...
- java 语言多线程可见性(synchronized 和 volatile 学习)
共享变量可见性实现的原理 java 语言层面支持的可见性实现方式: synchronized volatile 1. synchronized 的两条规定: 1 线程解锁前,必须把共享变量的最新值刷新 ...
- Java中堆内存和栈内存详解2
Java中堆内存和栈内存详解 Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,ja ...
- java中的内存一般分成几部分?
java中的内存被分成以下四部分: ①.代码区 ②.栈区 ③.堆区 ④.静态区域 栈区:由编译器自动分配释放,存放函数的参数值.局部变量的值等:具体方法执行结束后,系统自动释放JVM内存资源 ...
- Java SE之Java中堆内存和栈内存[转/摘]
[转/摘]1-3Java中堆内存和栈内存 注解:内存(Memory)即 内存储器,主存,其作用是用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器(辅存)交换的数据. Java中把内存分为两种:栈 ...
- Java中的内存处理机制和final、static、final static总结
Java中的内存处理机制和final.static.final static总结 装载自:http://blog.csdn.net/wqthaha/article/details/20923579 ...
随机推荐
- wpf toggleSwitch 的只读属性
xml code --------------------------------------------- <Page x:Class="UWPDemo.MainPage" ...
- LeetCoded第739题题解--每日温度
每日温度 请根据每日 气温 列表,重新生成一个列表.对应位置的输出为:要想观测到更高的气温,至少需要等待的天数.如果气温在这之后都不会升高,请在该位置用 0 来代替. 例如,给定一个列表 temper ...
- 入门数据结构与算法,看这一个就够了,知识点+LeetCode实战演练
本笔记来自拉钩教育300分钟搞定算法面试 算法与数据结构 要掌握一种数据结构,就必须要懂得分析它的优点和缺点. 在考虑是否应当采用一种数据结构去辅助你的算法时,请务必考虑它的优缺点,看看它的缺点是否会 ...
- docker下gitlab(redis)安装配置使用(完整版)
ps:如果是云主机,需添加安全组开放相应端口(关联相应实例),防火墙开放端口或直接关闭 https://www.jianshu.com/p/080a962c35b6 将其中external_url换为 ...
- 关于servlet中要写初始化逻辑应该重载有参还是无参的init
关于开发者在写初始化逻辑的时候,应该选用的哪个init方法@author mzy 在查看servlet的源码的时候,因为servlet是一个接口使用较麻烦: 所以我们使用它的实现类:GenericSe ...
- Java HdAcm1174
空间一般直线的方程是:(x-x0)/a=(y-y0)/b=(z-z0)/c,这是一条过(x0,y0,z0),方向矢量为{a,b,c}的直线.假设已知点的坐标是A(e,f,g),过A点,且与{a,b,c ...
- 虚拟dom?diff算法?key?Vue原理的核心三问?打包教你搞定。
为什么需要虚拟DOM 先介绍浏览器加载一个HTML文件需要做哪些事,帮助我们理解为什么我们需要虚拟DOM.webkit引擎的处理流程,如下图所示: 所有浏览器的引擎工作流程都差不多,如上图大致分5步: ...
- 二.Go微服务--令牌桶
1. 令牌桶 1.1 原理 我们以 r/s 的速度向桶内放置令牌,桶的容量为 b , 如果桶满了令牌将会丢弃 当请求到达时,我们向桶内获取令牌,如果令牌足够,我们就通过转发请求 如果桶内的令牌数量不够 ...
- 剑指 Offer 36. 二叉搜索树与双向链表
剑指 Offer 36. 二叉搜索树与双向链表 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表.要求不能创建任何新的节点,只能调整树中节点指针的指向. 为了让您更好地理解问题,以下面的 ...
- docker run命令指定GPU多个显卡不生效的问题解决和代码示例
问题描述:我有一个程序(app),需要用到显卡来跑.原本的部署方式 是直接修改程序的配置文件来指定要用到的显卡. 这是我服务器的显卡信息:总共3卡 分别是 0卡 ,1卡和2卡. [root@k8s-r ...