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 ...
随机推荐
- C#---OleDbHelper
/// <summary> /// OleDbServer数据访问帮助类 /// </summary> public sealed class OleDbHelper { pu ...
- C#多线程---Event类实现线程同步
一.简介 我们使用类(.net Framework中的类,如 AutoResetEvent, Semaphore类等)的方法来实现线程同步的时候,其实内部是调用操作系统的内核对象来实现的线程同步. S ...
- java8 lambda表达式和函数式编程
什么是函数式接口(Functional Interface) 其实之前在讲Lambda表达式的时候提到过,所谓的函数式接口,当然首先是一个接口,然后就是在这个接口里面只能有一个抽象方法 (可以有def ...
- jQuery中的子(后代)元素过滤选择器(四、六):nth-child()、first-child、last-child、only-child
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <hea ...
- .ssh/config 常用配置
不用每次都 -i 指定密钥,且避免连接自动断开 ControlMaster auto ControlPath ~/.ssh/connection-%r@%h:%p ControlPersist 4h ...
- NOIP模拟21:「Median·Game·Park」
T1:Median 线性筛+桶+随机化(??什么鬼?). 首先,题解一句话秀到了我: 考虑输入如此诡异,其实可以看作随机数据 随机数据?? 这就意味着分布均匀.. 又考虑到w< ...
- Linux制作根文件系统笔记
测试平台 宿主机平台:Ubuntu 12.04.4 LTS 目标机:Easy-ARM IMX283 目标机内核:Linux 2.6.35.3 交叉编译器:arm-linux-gcc 4.4.4 Bus ...
- three+pixi 将二维和三维结合
PIXI+THREE 使用 PIXI 和 THREE 将三维和二维渲染在同一个 canvas 下面 效果 思路 初始化 PIXI 的 Application, 作为 pixi 最重要的变量 const ...
- MyBatis的Mapper代理笔记
MaBatis--Mapper代理 目前使用SqlSession进行增删改查的缺点: 没有办法实现多参传值 书写的时候没有接口,后期的维护低 使用Mapper的动态代理方式来解决问题 具体实现 首先我 ...
- Linux上使用设置printf显示的颜色
我们经常看到别的屏幕五颜六色的很是羡慕,看着很炫是吧.其实我们也可以自己做一个简单的修改,是我们的显示结果也呈现出不同的颜色.shell脚本可能设置的比较多,但是我们平常使用C语言却很少设置它的颜色, ...