`
aswang
  • 浏览: 846306 次
  • 性别: 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可见。 ...

    ASP技术常遇问题解答-如何防止Application对象在多线程访问中出现错误?.zip

    1. **理解Application对象的特性**:Application对象是ASP.NET中的一个全局性对象,它存储的数据对所有用户和请求都是可见的。这意味着多个线程可能同时尝试读取或写入Application对象,如果不加以控制,就可能导致...

    java多线程Demo

    Java多线程是Java编程中的一个重要概念,它允许程序同时执行多个任务,提高了程序的效率和响应速度。在Java中,实现多线程有两种主要方式:继承Thread类和实现Runnable接口。 1. 继承Thread类: 当我们创建一个新...

    java 多线程并发实例

    - volatile:修饰变量,确保多线程环境下的可见性和有序性,但不保证原子性。在实例中,可能用于共享标志的设置与读取。 - wait()、notify()和notifyAll():这些方法存在于Object类中,用于线程间的通信。在线程A...

    Android开发中的多线程编程技术

    在Android应用开发中,多线程技术是必不可少的,它能帮助开发者实现高效的代码执行,提升用户体验,并确保应用程序的响应性。本资源包主要聚焦于Android平台上的多线程编程,包括理论概念、最佳实践以及实际应用案例...

    Java多线程练习题

    2. volatile关键字:保证了变量在多线程环境下的可见性和有序性,但不保证原子性。 3. Lock接口与ReentrantLock类:提供了比synchronized更细粒度的锁控制,具有可重入性、公平性等特点,可以配合Condition进行条件...

    多线程面试题

    volatile确保了多线程环境下的变量可见性和有序性,但不保证原子性。 5. **死锁**:当两个或更多线程互相等待对方释放资源时,就会发生死锁。避免死锁的方法包括避免循环等待、设置超时、使用死锁检测算法等。 6. ...

    java多线程面试题

    Java内存模型定义了一套多线程访问内存和CPU的规则,以确保多线程环境下变量的可见性和顺序性。它规定了操作的先行发生规则,包括程序次序规则、管程锁定规则、volatile变量规则、线程启动规则、线程终止规则、对象...

    csdn_多线程

    3. **volatile关键字**:用于确保多个线程之间对共享变量的可见性,但不保证原子性。 4. **Lock接口与ReentrantLock**:提供比`synchronized`更细粒度的锁控制,支持公平锁和非公平锁,以及可中断的锁等待等。 **...

    Linux多线程编程手册

    Linux多线程编程是计算机编程中一个高级主题,涉及到同时执行多个任务的能力,这些任务共享公共地址空间。它允许程序更有效地利用多核处理器的能力,提高性能和响应速度,优化资源利用,并改善程序的结构。Linux多...

    java多线程基础资料

    Java多线程是Java编程中的一个核心概念,它允许程序同时执行多个任务,极大地提高了程序的效率和响应性。在Java中,多线程的实现主要有两种方式:继承Thread类和实现Runnable接口。 1. 继承Thread类:创建一个新的...

    Java多线程详解(超详细)_狂神说笔记完整版_项目代码_适合小白随课程学习

    - `volatile`关键字确保线程间变量的可见性,但不保证原子性。 - `wait()`, `notify()`和`notifyAll()`方法用于线程间通信,需在同步环境中使用。 5. **线程通信问题** - 生产者-消费者问题:一个线程生产数据,...

    深入解析volatile关键字:保障多线程下的数据一致性与可见性.pdf

    通过对`volatile`关键字的工作原理及其在多线程环境中的应用进行深入探讨,我们可以更好地理解如何利用`volatile`来解决可见性和有序性问题。同时,我们也应该注意到`volatile`并不能保证操作的原子性,因此在需要...

    易语言模块多线程支持模块.rar

    易语言的多线程模块可能提供了互斥量、信号量、事件对象等同步机制,确保线程安全地访问共享数据。 3. **线程通信**:线程间通信是为了让线程之间能够交换信息。易语言可能提供了消息队列、共享内存或者基于事件的...

    多线程案例,多线程教程

    本文将深入探讨多线程的概念、重要性以及如何在实际项目中应用多线程,特别关注Java语言中的多线程实现。 多线程的基本概念: 1. 线程是操作系统调度的基本单位,一个进程中可以有多个线程并行执行。相比于单线程,...

    C++11 多线程

    随着**C++11** 的发布,标准库中包含了针对多线程编程的新组件,使得编写多线程应用程序变得更加简单。 #### 二、线程管理 - **`std::thread` 类**:这是**C++11** 中管理线程的主要方式。通过`std::thread`可以...

    多线程编程指南

    本指南详细介绍了多线程编程在iOS开发中的应用,希望读者能够从中受益,并在实际项目中有效地运用多线程技术。 #### 推荐资源 最后,推荐了一些学习多线程编程的资源,包括书籍、在线教程等,供有兴趣进一步深入...

Global site tag (gtag.js) - Google Analytics