`
aswang
  • 浏览: 837900 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

多线程中共享对象的可见性

    博客分类:
  • java
阅读更多

在阅读《java并发编程实战》的第三章的时候,看到书中的一个例子,随在Eclipse中执行看看效果。示例代码如下:

 

public class NoVisibility {
	private static boolean ready;
	private static int number;
	
	private static class ReaderThread extends Thread {
		public void run() {
			while(!ready) {
				Thread.yield();
			}
			
			System.out.println(number);
		}
	}
	
	public static void main(String[] args) {
		new ReaderThread().start();
		
		number = 42;
		ready = true;
	}
}

 

 

书中的解释是,这个程序的执行结果除了打印出42以外,还有两外另种情况:一时无限循环下去,二是打印出0来。其中打印42很好理解,因为,在启动ReaderThread线程以后,如果还没有设置ready为true,那么ReaderThread会一直循环,直到读取ready的值为true,然后打印出结果42来。

 

无限循环这个结果,通过代码来测试是很难观察到的,但是通过书中的分析,这种情况是存在的。这个示例中的number和ready,会被两个线程访问,其一是运行该程序的main线程,其二是ReaderThread线程,所以这两个变量可以称之为共享变量,由于java虚拟机自己的缓存机制,在缺少同步的情况下,会将number和ready的数值缓存在寄存器中,这就会导致ReaderThread线程在某些情况下读取不到最新的值,这样即使在main方法中将ready设置为true了,但是ReaderThread线程读取的ready值仍然为false,这样就会导致一直循环下去。

 

上面的这个示例很难观察到这种情况,所以我对其进行了改造,可以看到另外一种与我们预期不相符的情况:

 

public class NoVisibility {
	private static boolean ready;
	private static int number;
	
	private static class ReaderThread extends Thread {
		public void run() {
			while(!ready) {
				System.out.println(System.currentTimeMillis());
				Thread.yield();
			}
			
			System.out.println(number);
		}
	}
	
	public static void main(String[] args) {
		for(int i=0;i < 100;i++)
			new ReaderThread().start();
		
		number = 42;
		ready = true;
	}
}

  对示例代码做了两处改动,第一,在ReaderThread线程的while循环中打印当前时间,第二,在main方法中增加了一个循环,创建了100个ReaderThread线程并启动线程。然后我们看看运行结果:(这里只列出了部分结果)

 

 

...
1354960834108
42
1354960834108
1354960834108
42
1354960834108
42
1354960834108
1354960834108
1354960834108
1354960834108
42
42
...
 

 

从打印的结果来看,就会发现问题,为什么在某些线程打印了42以后,有些线程仍然在打印时间?

如果某个线程打印出了42,说明main方法已经执行完毕,即变量ready的值已经设置为true了,那么这以后其它的线程打印的结果应该都是42了,但这里的结果是有些线程读取的ready值仍然为false,这就说明了java虚拟机会对线程中使用到的变量进行缓存,所以就出问题了。

java虚拟机缓存变量,是出于性能的考虑,并且在单线程程序中,或者不存在共享变量的多线程程序中,这都不会出现问题。但是,在有共享变量的多线程程序中,就会发生问题,这里就涉及到共享对象的可见性了,也就是在没有使用同步机制的情况下,一个线程对某个共享对象的修改,并不会立即被其它的线程读取到。上面的代码之所以会出问题,就是因为ReaderThread线程,没有读取到main线程对ready变量修改后的值。要解决上述问题,可以通过在main方法和ReaderThread线程中的run方法中,给访问number和ready值的代码块中加锁来解决。

 

另外一种结果打印出0来,这个暂时还不是很明白,书中的解释是java虚拟机的内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中,这样可能在读取到ready修改后的值后,却仍然读取了number的旧值,从而打印出了int的默认值0来。

 

 

 

 

1
0
分享到:
评论
6 楼 60love5 2016-12-14  
60love5 写道
首先谢谢你的解析,但你这个验证可见性的小程序是存在问题的,你的那个输出说明不了任何问题。在这里,有一个事实就是,先打印出来的不一定就是先执行完的。如果你在打印 42 的同时,也打印当前时间,你就会发现:即使在打印 42 之后会打印出时间,那这个时间的生成时间也要早于 打印42的时间。下面这个程序说明了这一点:
public class NoVisibility {
	private static boolean ready;
	private static int number;
	private static final AtomicLong count = new AtomicLong(0);

	private static class ReaderThread extends Thread {
		public ReaderThread(String name) {
			super(name);
		}

		public void run() {
			while (!ready) {
				System.out.println(Thread.currentThread().getName() + " : "
						+ count.incrementAndGet() + " # "
						+ System.currentTimeMillis());
				Thread.yield();
			}
			System.out.println(Thread.currentThread().getName() + " : "
					+ count.incrementAndGet() + " # " + number + " # "
					+ System.currentTimeMillis());
		}
	}

	public static void main(String[] args) {
		Thread.currentThread().setPriority(10);
		for (int i = 0; i < 1000; i++) {
			Thread thread = new ReaderThread("Thread-" + i);
			thread.start();
		}

		number = 42;
		ready = true;

		System.out.println("--- main-Thread Over : "
				+ System.currentTimeMillis() + " ---");
	}
}/* Output: (输出片段,结果不唯一:)
        Thread-482 : 794 # 1481685379575
        Thread-485 : 2604 # 42 # 1481685379625
        Thread-482 : 2605 # 42 # 1481685379625
        Thread-487 : 793 # 1481685379575
        Thread-486 : 792 # 1481685379575
        Thread-480 : 791 # 1481685379575
        Thread-483 : 790 # 1481685379575
 *///:~

5 楼 60love5 2016-12-14  
首先谢谢你的解析,但你这个验证可见性的小程序是存在问题的,你的那个输出说明不了任何问题。在这里,有一个事实就是,先打印出来的不一定就是先执行完的。如果你在打印 42 的同时,也打印当前时间,你就会发现:即使在打印 42 之后会打印出时间,那这个时间的生成时间也要早于 打印42的时间。下面这个程序说明了这一点:
public class NoVisibility {
	private static boolean ready;
	private static int number;
	private static final AtomicLong count = new AtomicLong(0);

	private static class ReaderThread extends Thread {
		public ReaderThread(String name) {
			super(name);
		}

		public void run() {
			while (!ready) {
				System.out.println(Thread.currentThread().getName() + " : "
						+ count.incrementAndGet() + " # "
						+ System.currentTimeMillis());
				Thread.yield();
			}
			System.out.println(Thread.currentThread().getName() + " : "
					+ count.incrementAndGet() + " # " + number + " # "
					+ System.currentTimeMillis());
		}
	}

	public static void main(String[] args) {
		Thread.currentThread().setPriority(10);
		for (int i = 0; i < 1000; i++) {
			Thread thread = new ReaderThread("Thread-" + i);
			thread.start();
		}

		number = 42;
		ready = true;

		System.out.println("--- main-Thread Over : "
				+ System.currentTimeMillis() + " ---");
	}
}
4 楼 beyondfengyu 2016-04-19  
从打印的结果来看,就会发现问题,为什么在某些线程打印了42以后,有些线程仍然在打印时间?
这样也不能说明是JVM做了缓存才出现的问题,因为代码中跑了100条线程,有可能出现一种情况是,很多线程在执行System.out.println(System.currentTimeMillis());  时,刚好某个线程A打印了42,这时代表主线程执行完毕,此时ready=true,然后其它线程再执行while(!ready)时判断为false,执行打印语句。

我对你原来的做了修改
private static boolean ready;
private static int number;
private static AtomicInteger ss = new AtomicInteger();
private static class ReaderThread extends Thread {
public void run() {
while(!ready) {
System.out.println(System.currentTimeMillis());
Thread.yield();
}

System.out.println(ss.incrementAndGet()+"_"+number);
}
}

public static void main(String[] args) {
for(int i=0;i < 100;i++)
new ReaderThread().start();

number = 42;
ready = true;
}

加了一个ss变量显示打印的次数,刚好打印的是100次,如果按你的分析,它打印的次数有可能不是100次
3 楼 kililanxilu 2013-09-23  
aswang 写道
Scooler 写道
打印出0,现在的jdk没这个问题了。。

早期的jdk如1.2,1.3 允许无序写入。。
代码和实际执行顺序不一致,如下列代码,

number = 42; 
ready = true;

----------------------------------------
可能执行的顺序是:

ready = true;
number = 42; 

这样number还没初始化,就打印出0了



是的,早期的jdk中存在,新的版本貌似解决了,但是没找到明确解释。


貌似没有看到JDK有这方面的改动。重排序有个as-if-serial语义原则。就是如果交换顺序不会对下方的的语句产生影响就可以执行重排序,遵循了这种原则的情况下感觉任然是按照顺序执行。应该还是可能出现0这种情况的。我也是刚好看到这块,大家讨论下。http://www.infoq.com/cn/articles/java-memory-model-2
2 楼 aswang 2012-12-10  
Scooler 写道
打印出0,现在的jdk没这个问题了。。

早期的jdk如1.2,1.3 允许无序写入。。
代码和实际执行顺序不一致,如下列代码,

number = 42; 
ready = true;

----------------------------------------
可能执行的顺序是:

ready = true;
number = 42; 

这样number还没初始化,就打印出0了



是的,早期的jdk中存在,新的版本貌似解决了,但是没找到明确解释。
1 楼 Scooler 2012-12-10  
打印出0,现在的jdk没这个问题了。。

早期的jdk如1.2,1.3 允许无序写入。。
代码和实际执行顺序不一致,如下列代码,

number = 42; 
ready = true;

----------------------------------------
可能执行的顺序是:

ready = true;
number = 42; 

这样number还没初始化,就打印出0了

相关推荐

    java多线程安全性基础介绍.pptx

    java多线程安全性基础介绍 线程安全 正确性 什么是线程安全性 原子性 竞态条件 i++ 读i ++ 值写回i 可见性 JMM 由于cpu和内存加载速度的差距,在两者之间增加了多级缓存导致,内存并不能直接对cpu可见。 ...

    java并发编程理论基础精讲

    共享资源与竞态条件: 详解共享资源在多线程环境中的问题,引出竞态条件的概念。 对象锁和监视器: 介绍对象锁的概念,解释如何使用 synchronized 关键字来实现对象级别的同步。 线程间通信: 详细讲解多线程之间...

    java 并发编程实践 001

    第3章 共享对象 3.1 可见性 3.2 发布和逸出 3.3 线程封闭 3.4 不可变性 3.5 安全发布 . 第4章 组合对象 4.1 设计线程安全的类 4.2 实例限制 4.3 委托线程安全 4.4 向已有的线程安全类添加功能 4.5 同步策略的文档化 ...

    java7hashmap源码-Concurrency:这是用来学习java多线程的

    可见性-volatile 通过内存屏障和禁止重排序优化实现 1.对volatile变量写操时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存 2.对volatile变量读操时,会在读操作后加入一条load屏障指令,...

    Java并发编程实战

    3.5.6 安全地共享对象 第4章 对象的组合 4.1 设计线程安全的类 4.1.1 收集同步需求 4.1.2 依赖状态的操作 4.1.3 状态的所有权 4.2 实例封闭 4.2.1 Java监视器模式 4.2.2 示例:车辆追踪 4.3 线程安全性的...

    Java 并发编程实战

    3.5.6 安全地共享对象 第4章 对象的组合 4.1 设计线程安全的类 4.1.1 收集同步需求 4.1.2 依赖状态的操作 4.1.3 状态的所有权 4.2 实例封闭 4.2.1 Java监视器模式 4.2.2 示例:车辆追踪 4.3 线程安全性的...

    Java理论与实践:变还是不变?

    本文介绍了不变对象是在实例化后其外部可见状态无法更改的对象。Java类库中的String、Integer和BigDecimal类就是不变对象的示例-它们表示在对象的生命期内无法更改的单个值。并说明了在Java理论与实践中,不变性的...

    Java的六大问题你都懂了吗

    在多线程的操作中,一个对象会被多个线程共享或修改,一个线程对对象无意识的修改可能会导致另一个使用此对象的线程崩溃。一个错误的解决方法就是在此对象新建的时候把它声明为final,意图使得它"永远不变".其实那是...

    Java进阶教程解密JVM视频教程

    * 最后的加餐环节是带着你理解 Java 内存模型:见识多线程读写共享数据的原子性、可见性、有序性,以及很多人解释不清楚的 happens-before 规则。当然还不能少了 CAS 和 synchronized 优化。 主讲内容 第一章:...

    软件工程-理论与实践(许家珆)习题答案

    多视点方法也是管理需求变化的一种新方法,它可以用于管理不一致性, 并进行关于变化的推理。 2. M公司的软件产品以开发实验型的新软件为主。用瀑布模型进行软件开发已经有近十年了,并取得了一些成功。若你作为一...

    Thinking in Java简体中文(全)

    2.6.1 名字的可见性 2.6.2 使用其他组件 2.6.3 static关键字 2.7 我们的第一个Java程序 2.8 注释和嵌入文档 2.8.1 注释文档 2.8.2 具体语法 2.8.3 嵌入HTML 2.8.4 @see:引用其他类 2.8.5 类文档标记 2.8.6 变量文档...

    android 面试2

    service不包含可见的用户界面,而是在后台无限地运行可以连接到一个正在运行的服务中,连接后,可以通过服务中暴露出来的借口与其进行通信 broadcast receiver是一个接收广播消息并作出回应的component,broadcast ...

    java联想(中文)

    2.6.1 名字的可见性 2.6.2 使用其他组件 2.6.3 static关键字 2.7 我们的第一个Java程序 2.8 注释和嵌入文档 2.8.1 注释文档 2.8.2 具体语法 2.8.3 嵌入HTML 2.8.4 @see:引用其他类 2.8.5 类文档标记 2.8.6 变量文档...

    Thinking in Java 中文第四版+习题答案

    2.6.1 名字的可见性 2.6.2 使用其他组件 2.6.3 static关键字 2.7 我们的第一个Java程序 2.8 注释和嵌入文档 2.8.1 注释文档 2.8.2 具体语法 2.8.3 嵌入 2.8.4 @see:引用其他类 2.8.5 类文档标记 2.8.6 变量文档标记...

    Delphi5开发人员指南

    2.18.3 可见性表示符 62 2.18.4 友类 62 2.18.5 对象的秘密 63 2.18.6 TObject:所有对象的祖先 63 2.18.7 接口 63 2.19 结构化异常处理 66 2.19.1 异常类 68 2.19.2 执行的流程 70 2.19.3 重新触发异常 71 2.20 ...

    JAVA面试题最全集

    多线程,用什么关键字修饰同步方法?stop()和suspend()方法为何不推荐使用? 59.使用socket建立客户端与服务器的通信的过程 60.JAVA语言国际化应用,Locale类,Unicode 61.描述反射机制的作用 62.如何读写一个...

    Think in Java(中文版)chm格式

    2.6.1 名字的可见性 2.6.2 使用其他组件 2.6.3 static关键字 2.7 我们的第一个Java程序 2.8 注释和嵌入文档 2.8.1 注释文档 2.8.2 具体语法 2.8.3 嵌入HTML 2.8.4 @see:引用其他类 2.8.5 类文档标记 ...

    JAVA_Thinking in Java

    2.6.1 名字的可见性 2.6.2 使用其他组件 2.6.3 static关键字 2.7 我们的第一个Java程序 2.8 注释和嵌入文档 2.8.1 注释文档 2.8.2 具体语法 2.8.3 嵌入HTML 2.8.4 @see:引用其他类 2.8.5 类文档标记 2.8.6 变量文档...

Global site tag (gtag.js) - Google Analytics