很多程序员对一个共享变量初始化要注意可见性和安全发布(安全地构建一个对象,并其他线程能正确访问)等问题不是很理解,认为Java是一个屏蔽内存细节的平台,连对象回收都不需要关心,因此谈到可见性和安全发布大多不知所云。其实关键在于对Java存储模型,可见性和安全发布的问题是起源于Java的存储结构。
Java存储模型原理
有很多书和文章都讲解过Java存储模型,其中一个图很清晰地说明了其存储结构:
由上图可知,jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
这个存储模型很像我们常用的缓存与数据库的关系,因此由此可以推断JVM如此设计应该是为了提升性能,提高多线程的并发能力,并减少线程之间的影响。
Java存储模型潜在的问题
一谈到缓存,我们立马想到会有缓存不一致性问题,就是说当有缓存与数据库不一致的时候,就需要有相应的机制去同步数据。同理,Java存储模型也有这个问题,当一个线程在自己工作内存里初始化一个变量,当还没来得及同步到主存里时,如果有其他线程来访问它,就会出现不可预知的问题。另外,JVM在底层设计上,对与那些没有同步到主存里的变量,可能会以不一样的操作顺序来执行指令,举个实际的例子:
-
publicclassPossibleReordering{
-
staticintx=0,y=0;
-
staticinta=0,b=0;
-
publicstaticvoidmain(String[]args)
-
throwsInterruptedException{
-
Threadone=newThread(newRunnable(){
-
publicvoidrun(){
-
a=1;
-
x=b;
-
}
-
});
-
Threadother=newThread(newRunnable(){
-
publicvoidrun(){
-
b=1;
-
y=a;
-
}
-
});
-
one.start();other.start();
-
one.join();other.join();
-
System.out.println("("+x+","+y+")");
-
}
-
}
由于,变量x,y,a,b没有安全发布,导致会不以规定的操作顺序来执行这次四次赋值操作,有可能出现以下顺序:
出现这个问题也可以理解,因为既然这些对象不可见,也就是说本应该隔离在各个线程的工作区内,那么对于有些无关顺序的指令,打乱顺序执行在JVM看来也是可行的。
因此,总结起来,会有以下两种潜在问题:
解决Java存储模型潜在的问题
为了能让开发人员安全正确地在Java存储模型上编程,JVM提供了一个happens-before原则,有人整理得非常好,我摘抄如下:
- 在程序顺序中, 线程中的每一个操作, 发生在当前操作后面将要出现的每一个操作之前.
- 对象监视器的解锁发生在等待获取对象锁的线程之前.
- 对volitile关键字修饰的变量写入操作, 发生在对该变量的读取之前.
- 对一个线程的 Thread.start() 调用 发生在启动的线程中的所有操作之前.
- 线程中的所有操作 发生在从这个线程的 Thread.join()成功返回的所有其他线程之前.
有了原则还不够,Java提供了以下工具和方法来保证变量的可见性和安全发布:
- 使用synchronized来同步变量初始化。此方式会立马把工作内存中的变量同步到主内存中
- 使用volatile关键字来标示变量。此方式会直接把变量存在主存中而不是工作内存中
- final变量。常量内也是存于主存中
另外,一定要明确只有共享变量才会有以上那些问题,如果变量只是这个线程自己使用,就不用担心那么多问题了
搞清楚Java存储模型后,再来看共享对象可见性和安全发布的问题就较为容易了
共享对象的可见性
当对象在从工作内存同步到主内存之前,那么它就是不可见的。若有其他线程在存取不可见对象就会引发可见性问题,看下面一个例子:
-
publicclassNoVisibility{
-
privatestaticbooleanready;
-
privatestaticintnumber;
-
privatestaticclassReaderThreadextendsThread{
-
publicvoidrun(){
-
while(!ready)
-
Thread.yield();
-
System.out.println(number);
-
}
-
}
-
publicstaticvoidmain(String[]args){
-
newReaderThread().start();
-
number=42;
-
ready=true;
-
}
-
}
按照正常逻辑,应该会输出42,但其实际结果会非常奇怪,可能会永远没有输出(因为ready为false),可能会输出0(因为重排序问题导致ready=true先执行)。再举一个更为常见的例子,大家都喜欢用只有set和get方法的pojo来设计领域模型,如下所示:
-
@NotThreadSafe
-
publicclassMutableInteger{
-
privateintvalue;
-
publicintget(){returnvalue;}
-
publicvoidset(intvalue){this.value=value;}
-
}
但是,当有多个线程同时来存取某一个对象时,可能就会有类似的可见性问题。
为了保证变量的可见性,一般可以用锁、synchronized关键字、volatile关键字或直接设置为final
共享变量发布
共享变量发布和我们常说的发布程序类似,就是说让本属于内部的一个变量变为一个可以被外部访问的变量。发布方式分为以下几种:
- 将对象引用存储到公共静态域
- 初始化一个可以被外部访问的对象
- 将对象引用存储到一个集合里
安全发布和保证可见性的方法类似,就是要同步发布动作,并使发布后的对象可见。
线程安全
其实当我们把这些变量封闭在本线程内访问,就可以从根本上避免以上问题,现实中存在很多例子通过线程封闭来安全使用本不是线程安全的对象,比如:
- swing的可视化组件和数据模型对象并不是线程安全的,它通过将它们限制到swing的事件分发线程中,实现线程安全
- JDBC Connection对象没有要求为线程安全,但JDBC的存取模式决定了一个Connection只会同时被一个线程使用
- ThreadLocal把变量限制在本线程中共享
分享到:
相关推荐
本书的读者是那些具有一定Java编程经验的程序员、希望了解Java SE 5,6在线程技术上的改进和新特性的程序员,以及Java和并发编程的爱好者。 目录 代码清单 序 第1章 介绍 1.1 并发的(非常)简短历史 1.2 线程的...
真正的Addison-Wesley 出品的Java Concurrency in Practice 中文版 目录回到顶部↑ 代码清单 序 第1章 介绍 1.1 并发的(非常)简短历史 ...第16章 java存储模型 附录a 同步annotation 参考文献 索引
中文完整版的Java并发编程实践PDF电子书 作者:Brian Gogetz Tim Peierls Joshua Bloch Joseph Bowbeer David Holmes Doug Lea 译者:韩锴 方秒 目录 第1章 介绍 1.1 并发的(非常)简短历史 1.2 线程的优点 1.3 ...
详细介绍java并发编程相关知识: 基础知识 并发与并行 Java并发演进历史 Java并发模型 线程模型 存储模型 JVM同步原语 volatile CAS 线程安全 保护“共享数据” 低级并发工具 原子变量 锁...
中文完整版的Java并发编程实践PDF电子书 作者:Brian Gogetz Tim Peierls Joshua Bloch Joseph Bowbeer David Holmes Doug Lea 译者:韩锴 方秒 目录 第1章 介绍 1.1 并发的(非常)简短历史 1.2 线程的优点 1.3 ...
线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程中的其它线程共享一个存储空间,这使得线程间的通信远较进程简单。 具体到java内存...
Java存储与读取对象 1个目标文件 如题 Java调色板面板源代码 1个目标文件 摘要:Java源码,窗体界面,调色板 使用Java语言编写的一款用于反映颜色变化的面板,也就是大家熟悉的颜色调色板演示程序。原理是初始化颜色...
在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步,通信指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。 Java语言的并发采用的是共享...
Java存储与读取对象 1个目标文件 如题 Java调色板面板源代码 1个目标文件 摘要:Java源码,窗体界面,调色板 使用Java语言编写的一款用于反映颜色变化的面板,也就是大家熟悉的颜色调色板演示程序。原理是初始化颜色...
Java存储与读取对象 1个目标文件 如题 Java调色板面板源代码 1个目标文件 摘要:Java源码,窗体界面,调色板 使用Java语言编写的一款用于反映颜色变化的面板,也就是大家熟悉的颜色调色板演示程序。原理是初始化颜色...
Java从网络取得文件 1个目标文件 简单 Java从压缩包中提取文件 1个目标文件 简单 Java存储与读取对象 1个目标文件 如题 Java调色板面板源代码 1个目标文件 摘要:Java源码,窗体界面,调色板 使用Java语言编写的一款...
Java存储与读取对象 1个目标文件 如题 Java调色板面板源代码 1个目标文件 摘要:Java源码,窗体界面,调色板 使用Java语言编写的一款用于反映颜色变化的面板,也就是大家熟悉的颜色调色板演示程序。原理是初始化...
Java存储与读取对象 1个目标文件 如题 Java调色板面板源代码 1个目标文件 摘要:Java源码,窗体界面,调色板 使用Java语言编写的一款用于反映颜色变化的面板,也就是大家熟悉的颜色调色板演示程序。原理是初始化...
Java存储与读取对象 1个目标文件 如题 Java调色板面板源代码 1个目标文件 摘要:Java源码,窗体界面,调色板 使用Java语言编写的一款用于反映颜色变化的面板,也就是大家熟悉的颜色调色板演示程序。原理是初始化...
Java从网络取得文件 1个目标文件 简单 Java从压缩包中提取文件 1个目标文件 简单 Java存储与读取对象 1个目标文件 如题 Java调色板面板源代码 1个目标文件 摘要:Java源码,窗体界面,调色板 使用Java语言编写的一款...
"类的初始化过程 当创建一个对象时,对象的各个变量根据其类型被设置为相应的默认初始值,然后调用构造方法,而每次调用构造方法都是要执行三个阶段: 1.调用超类的构造方法; 2.由初始化语句对给变量进行初始化...
设计目的:通过研究Linux 的进程机制和信号量实现生产者消费者问题的并发控制。说明:有界缓冲区内设有20 个存储单元,放入/取出的数据项设定为1‐20 这20 个整型数。设计要求:1)每个生产者和消费者对有界缓冲区...
4.3 掌握C程序设计语言,以及C++、Java、Visual、Basic、Visual C++中任一种程序设计语言,以便能指导程序员进行编程和测试,并进行必要的优化 4.4 系统测试 • 指导程序员进行模块测试,并进行验收 • 准备...