Java多线程之内存可见性

1、什么是JAVA 内存模型

Java Memory Model (JAVA 内存模型)描述线程之间如何通过内存(memory)来进行交互。 具体说来,
JVM中存在一个主存区(Main Memory或Java Heap
Memory),对于所有线程进行共享,而每个线程又有自己的工作内存(Working
Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。

Java内存模型的抽象示意图如下:

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

1、线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2、线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:

如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。

2、什么是内存可见性?

从上图可知,如果线程A对共享变量X进行了修改,但是线程A没有及时把更新后的值刷入到主内存中,而此时线程B从主内存读取共享变量X的值,所以X的值是原始值,那么我们就说对于线程B来讲,共享变量X的更改对线程B是不可见的。如果共享的更新不可见,会导致什么问题呢? 请看下面的例子:

[java] view plain copy

  1. class MyThread implements Runnable {
  2. int num = 1000000;
  3. public void run() {
  4. if (Thread.currentThread().getName().equals("t1")) {
  5. increment();
  6. } else {
  7. decrement();
  8. }
  9. }
  10. public void increment() {
  11. for (int i = 0; i < 10000; i++) {
  12. num++;
  13. }
  14. }
  15. public void decrement() {
  16. for (int i = 0; i < 10000; i++) {
  17. num--;
  18. }
  19. }
  20. }
  21. public class Test {
  22. public static void main(String[] args) {
  23. MyThread thread = new MyThread();
  24. Thread a = new Thread(thread, "t1");
  25. Thread b = new Thread(thread, "t2");
  26. a.start();
  27. b.start();
  28. try {
  29. a.join();
  30. b.join();
  31. } catch (Exception e) {
  32. e.printStackTrace();
  33. }
  34. System.out.println(thread.num);
  35. }
  36. }

从上面代码可以看出,这里有两个线程,其中一个对num执行1000次加1操作,另一个线程执行1000次减1操作,按理说最后num的值是不变的,但是当你运行后,发现num的值可能并不是初始值。那么为什么会有这种问题呢?这是内存不可见引起的。

从上图中我们可以看到,当线程1对num值加一以后,还未把最新值写入主内存,CPU就停止了线程1的执行,并且执行线程2,线程2首先从主内存中获取num的值,然后减一,最后把值更新到主内存中,这个时候,CPU终止了线程2的执行,转而继续执行线程1,
这个时候线程1把最新值刷入主内存,所以主内存结果变为了1000001.

通过上面的分析,我们得知:内存不可见是由于共享变量的值没有及时在主内存中更新,为什么没有及时更新呢?是因为加一(或者减一)的操作不具备原子性(例子中最后一步被打断)。那么如何保证操作具有原子性呢?这里我们引入synchronized关键字。

3、Synchronized关键字

synchronized用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍然可以访问该object中的非加锁代码块。

JMM关于synchronized的两条规定:

1. 线程解锁前,必须把共享变量的最新值刷新到主内存中
2. 线程加锁时,讲清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。

这样,线程解锁前对共享变量的修改在下次加锁时对其他线程可见。

所以,为了保证num在某个时刻的修改具有原子性,我们可以在下面两个方法前加synchronized. 如果你zhi‘dui

[java] view plain copy

  1. public synchronized void increment() {
  2. for (int i = 0; i < 10000; i++) {
  3. num++;
  4. }
  5. }
  6. public synchronized void decrement() {
  7. for (int i = 0; i < 10000; i++) {
  8. num--;
  9. }
  10. }

因为synchronized的本质是一把锁,所以我们还可以通过真正意义上的加锁和开锁来实现内存可见性。代码如下:

[java] view plain copy

  1. class MyThread implements Runnable {
  2. int num = 1000000;
  3. Lock lock = new ReentrantLock();
  4. public void run() {
  5. if (Thread.currentThread().getName().equals("t1")) {
  6. increment();
  7. } else {
  8. decrement();
  9. }
  10. }
  11. public void increment() {
  12. for (int i = 0; i < 10000; i++) {
  13. lock.lock();
  14. num++;
  15. lock.unlock();
  16. }
  17. }
  18. public void decrement() {
  19. for (int i = 0; i < 10000; i++) {
  20. lock.lock();
  21. num--;
  22. lock.unlock();
  23. }
  24. }
  25. }

你可能会问:我们可否在num前面加volatile 达到内存可见性呢?
答案是否定的,volatile实现共享变量内存可见性有一个条件,就是对共享变量的操作必须具有原子性。比如 num = 10;
这个操作具有原子性,但是 num++ 或者num--由3步组成,并不具有原子性,所以是不行的。

参考:

http://www.infoq.com/cn/articles/java-memory-model-1  (本文第一部分主要来自这篇文章)

http://blog.csdn.net/xingjiarong/article/details/47603813 (本文第二部分主要来自这篇文章,代码做了修改)
http://baike.baidu.com/item/synchronized

http://www.imooc.com/learn/352

时间: 2024-10-07 12:01:14

Java多线程之内存可见性的相关文章

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

1.JMM:Java Memory Model(Java内存模型) 关于synchronized的两条规定: 1.线程解锁前,必须把共享变量的最新值刷新到主内存中 2.线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁和解锁需要是同一把锁) 注:线程解锁前对共享变量的修改在下次加锁时对其他线程可见 2.线程执行互斥代码的过程: 1.获得互斥锁 2.清空工作内存 3.从主内存拷贝变量的最新副本到工作内存 4.执行代码 5.将更改后的共享变量的值刷

java多线程与内存可见性

一.java多线程 JAVA多线程实现的三种方式: http://blog.csdn.net/aboy123/article/details/38307539 二.内存可见性 1.什么是JAVA 内存模型 共享变量 :如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量. Java Memory Model (JAVA 内存模型)描述线程之间如何通过内存(memory)来进行交互,描述了java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存

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

可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到. 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线 程的共享变量. Java内存模型: 线程对共享变量操作的规定: 共享变量可见性实现的原理: 可见性的必要条件: 可见性的实现方式:(final也可以) 线程执行互斥代码的过程: 重排序的概念: as-if-serial的概念: 导致共享变量在线程间不可见的原因: 可见性分析: 当一个线程访问object的一个sysnchronized(this)同步代

细说Java多线程支内存可见性

一.共享变量在线程间的可见性 可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到. 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是几个线程的共享变量. Java内存模型(Java Memory Model):描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节. Java内存模型(JVM):● 所有的变量都存储在主内存中.● 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主

java多线程03-----------------volatile内存语义

java多线程02-----------------volatile内存语义 volatile关键字是java虚拟机提供的最轻量级额的同步机制.由于volatile关键字与java内存模型相关,因此,我们在介绍volatile关键字之前,对java内存模型进行更多的补充(之前的博文也曾介绍过). 1. java内存模型(JMM) JMM是一种规范,主要用于定义共享变量的访问规则,目的是解决多个线程本地内存与共享内存的数据不一致.编译器处理器的指令重排序造成的各种线程安全问题,以保障多线程编程的原

1 Java线程的内存可见性

Java内存的可见性 可见性: 一个线程对共享变量的修改,能够及时被其它线程看到 共享变量: 如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量 Java内存模型(JMM): 描述了Java程序中各种线程共享变量的访问规则,以及在JVM中将线程共享变量存储到内存和从内存中读取出线程共享变量这样的底层细节 上面这些规则都是针对线程的共享变量的,JMM的细节会在以后的博客里面写. 本篇只需要知道 1 所有的变量都存储在主内存中 2 每个线程都有自己独立的工作内存,里面

JavaSE学习53:细说多线程之内存可见性

一共享变量在线程间的可见性 (1)有关可见性的一些概念介绍 可见性:一个线程对共享变量值的修改,能够及实地被其他线程看到. 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量.所 有的变量都存储在主内存中. 线程的工作内存:每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的 一份拷贝). (2)数据争用问题 多个线程对同一资源操作时,通常会产生进程,比如一个线程往消息队列插入数据,而另一个线程从消息队列取 出数据 当消息队

跟着实例学习java多线程4-内存可见性

前三篇我们主要说了多线程访问共享可变状态时需要进行正确的同步处理,保证同一时刻只有一个线程访问相同的数据,我们使用synchronized关键字来实现原子性操作. 今天我们在来认识一下同步的另一个重要方面:内存可见性,这个概念其实很好理解,就是保证在同一个时刻,共享可变状态对访问它的线程呈现出自己最新的状态变化. 我们经常遇到的情景是这样的,一个全局变量计数器,一个线程负责更新该数值,另一些线程获取这个值,那么可见性就是获取值的线程,可以获取到更新线程更新的最新的值. 让我们先来看一个例子,在没

java多线程12.内存模型

假设一个线程为变量赋值:variable = 3: 内存模型需要解决一个问题:"在什么条件下,读取variable的线程将看到这个值为3?" 这看上去理所当然,但是如果缺少内存同步,那么将会有许多因素使得线程无法立即甚至永远,看到另一个线程的操作结果. 如: 1.在编译器中生成的指令顺序,可以与源代码中的顺序不同,此外编译器还会将变量保存在寄存器而不是内存中: 2.处理器可以采用乱序或并行等方式来执行指令: 3.缓存可能会改变将写入变量提交到主内存的次序: 4.而且保存在处理器本地缓存