Tag Archive for JMM

深入理解JVM(新)

1   Java技术与Java虚拟机

说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成: Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。它们的关系如下图所示:

图1   Java四个方面的关系

运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件)。最后字节码被装入内存, 一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。从上图也可以看出Java平台由Java虚拟机和 Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。这个平台的结构如下图所示:

Read more

Java 虚拟机体系结构

众所周知,Java源代码被编译器编译成class文件。而并不是底层操作系统可以直接执行的二进制指令(比如Windows OS的.exe文件)。因此,我们需要有一种平台可以解释class文件并运行它。而做到这一点的正是Java 虚拟机(JVM)。

实际上,JVM是一种解释执行class文件的规范技术。各 个提 供商都可以根据规范,在不同的底层平台上实现不同的JVM

下面是JVM实现的基本结构框图。其中类装载子系统、运行时数据区、执行引擎等 是JVM的必须要解决的几大问题。

★ 类装载器子系统

Read more

Class文件内容及常量池

JVM运行Java程序的时候,它会加载对应的class文件,并提取class文件中的信息存放在JVM开辟出来的方法区 内存中。那么这个class文件里面到底有些什么内容呢?

一、class文件内容概述

class文件是由8bits的字节流组成,全部字节构成了15个有意义的项目。这些项目之间没有任何无意义的字节,因此class文件非常紧凑。占据多字节空间的项目按照高位在前的顺序存放。下面我们详细讨论这些项目:

★ magic(魔数) 每个class文件的前4个字节称为魔数,值为0xCAFEBABE。作用在于轻松的辨别class文件与非class文件。

★ minor_version、major_version(次、主版本号) 各占2个字节。随着Java技术的发展,class文件的格式会发生变化。版本号的作用在于使得虚拟机能够认识当前加载class的文件格式。从而准确的提取class文件信息。

★ constant_pool_count 、constance_pool(常量池) 从这里开始的字节组成了常量池 。 存储了诸如符号常量、final常量值、基本数据类型的字面值等内容。JVM会将每一个常量构成一个常量表,每个常量表都有自己的入口地址。而实际上在 JVM会将这些常量表存储在方法区中一块连续的内存空间中,因此class文件会根据常量表在常量池中的位置对其进行索引。比如常量池中的第一个常量表的 索引值就是1,第二个就是2。有的时候常量表A需要常量表B的内容,则在常量表A中会存储常量表B的索引值x。而constant_pool_count 就记录了有多少个常量表,或则所有多少个索引值。实际上,常量池中没有索引值为0的常量表,但这缺失的索引值也被记录在 constant_pool_count中,因此 constant_pool_count等于常量表的数量加1。关于常量池的具体内容,我们会在下面详细讲述,并用一个例子来显示整个class文件的内容。

Read more

和小云聊堆栈

音冢迪光 23:27:24
问个问题
小云 23:27:24

音冢迪光 23:27:25

音冢迪光 23:27:57
堆栈 是在 工作内存中的是吧?
堆是在 主存中的 吼?
小云 23:28:26
什么叫工作内存和主存,有什么区别?
音冢迪光 23:28:26

音冢迪光 23:28:37
JMM中的概念额
小云 23:28:48
堆、栈,一个在下,一个在上
音冢迪光 23:28:49

Read more

深入理解JMM

深入理解JMM  more understanding about jmm  java memory model(本站开始做英文SEO了)

深入理解JMM的重点

JMM具体规定要JLS的 “Thread and lock”一章中,可以说这是一章非常晦涩的一个规范,要想完全把
它理解清楚,一般的辛苦是不行的.那是要”相当的~~~”的辛苦.而要把它向别人再解释清楚,那简直就
是恶梦.

作者自知无力能全面清楚地向大家说明这一章的内容,但以作者的经验,主要从以下两个方面去理解
可以改快地抓住本质.而不至于陷入”Thread and lock”的泥潭.

一.理解主存储区和线程工作存储区.

二.理解同步的两个功能.

首先要明白的问题:
1.多个线程共有的字段应该用synchronizedvolatile来保护.
2.synchronized负责线程间的互斥.即同一时候只有一个线程可以执行synchronized中的代码.
3.volatile负责线程中的变量与主存储区同步.但不负责每个线程之间的同步.

[Main Memory]与[Working Memory]

java内存模型

Main Memory是实例所在的存储区,所有实例和实例的字段都在此区域,为所有线程所共有.
Working Memory是绺个线程独自所拥有的存储区.其中有Main Memory中部分COPY.

Read more

JMM & synchronized概述

根据Java语言规范中的说明,JVM系统中存在一个

主内存(Main Memory),Java中所有的变量存储在主内存中,对于所有的线程是共享的(相当于黑板,其他人都可以看到的)。

每个线程都有自己的工作内存 (Working Memory),工作内存中保存的是主存中变量的拷贝,(相当于自己笔记本,只能自己看到),工作内存由【缓存】和【堆栈】组成,其中缓存【所谓的stack  栈?不明白啊】保存的是主存中的变量的 copy,堆栈保存的是线程局部变量。线程对所有变量的操作都是在工作内存中进行的,线程之间无法直接互相访问工作内存,变量的值得变化的传递需要主存来 完成。在JMM中通过并发线程修改的变量值,必须通过线程变量同步到主存后,其他线程才能访问到。
大体上来讲,线程对某个变量的操作可以简化成下面的步骤:
1.从主内存中复制数据到工作内存
2.执行代码,对数据进行各种操作和计算
3.把操作后的变量值重新写回主内存中

当然这样的运行顺序也是我们所期望的!但是, JVM并不保证第1步和第3步会严格按照上述次序立即执行。因为根据java语言规范的规定,线程的工作内存和主存间的数据交换是松耦合的,什么时候需要 刷新工作内存或者什么时候更新主存的内容,可以由具体的虚拟机实现自行决定【悲了个剧】。由于JVM可以对特征代码进行调优,也就改变了某些运行步骤的次序的颠倒,那 么每次线程调用变量时是直接取自己的工作存储器中的值还是先从主存储器copy再取是没有保证的,任何一种情况都可能发生。同样的,线程改变变量的值之 后,是否马上写回到主存储器上也是不可保证的,也许马上写,也许过一段时间再写。

那么,在多线程的应用场景下就会出现问题了,多个线程同时访问同一个代码 块,很有可能某个线程已经改变了某变量的值,当然现在的改变仅仅是局限于工作内存中的改变,此时JVM并不能保证将改变后的值立马写到主内存中去,也就意 味着有可能其他线程不能立马得到改变后的值,依然在旧的变量上进行各种操作和运算,最终导致不可预料的结果。
这样的情况是不是就不能避免了呢?
Java程序员都知道synchronized关键字强制实施一个互斥锁,使得被保护的代码块在同一时间只能有一个线程进入并执行。当然 synchronized还有另外一个方面的作用:在线程进入synchronized块之前,会把工作存内存中的所有内容映射到主内存上,然后把工作内 存清空再从主存储器上拷贝最新的值。而在线程退出synchronized块时,同样会把工作内存中的值映射到主内存,但此时并不会清空工作内存。这样一 来就可以强制其按照上面的顺序运行,以保证线程在执行完代码块后,工作内存中的值和主内存中的值是一致的,保证了数据的一致性!

参考:http://www.javaeye.com/topic/438068

java多线程编程-正确使用 Volatile 变量

Java™ 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。其中 Volatile 变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错。在这期的 Java 理论与实践中,Brian Goetz 将介绍几种正确使用 volatile 变量的模式,并针对其适用性限制提出一些建议。

Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。本文介绍了几种有效使用 volatile 变量的模式,并强调了几种不适合使用 volatile 变量的情形。

锁提供了两种主要特性:互斥(mutual exclusion)可见性(visibility

互斥 即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。

可见性要更加复杂 一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的【就是修改必须同步到主存的意思?】 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

Volatile 变量

Read more

java内存模型详解

内存模型 (memory model)

内存模型描述的是程序中各变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的低层细节.

不同平台间的处理器架构将直接影响内存模型的结构.

在C或C++中, 可以利用不同操作平台下的内存模型来编写并发程序. 但是, 这带给开发人员的是, 更高的学习成本.
相比之下, java利用了自身虚拟机的优势, 使内存模型不束缚于具体的处理器架构, 真正实现了跨平台.
(针对hotspot jvm, jrockit等不同的jvm, 内存模型也会不相同)

内存模型的特征:
a, Visibility 可视性 (多核,多线程间数据的共享)
b, Ordering 有序性 (对内存进行的操作应该是有序的)

  • java 内存模型 ( java memory model )

All instance fields, static fields and array elements are stored in heap memory . Local variables ( §14.4) , formal method parameters ( §8.4.1) or exception handler parameters are never shared between threads and are unaffected by the memory model.

Read more

高性能JAVA开发之内存管理

一、JVM中的对象生命周期

对象的生命周期一般分为7个阶段:创建阶段,应用阶段,不可视阶段,不可到达阶段,可收集阶段,终结阶段,释放阶段。

创建阶段,首先大家看一下,如下两段代码:

test1:

for( int i=0; i<10000; i++)

Object obj=new Object();

test2:

Object obj=null;

for( int i=0; i<10000; i++)

obj=new Object();

这两段代码都是相同的功能,但是显然test2的性能要比test1性能要好,内存使用率要高, 这是为什么呢?原因很简单,test1每次执行for循环都要创建一个Object的临时对象,但是这些临时对象由于JVM的GC不能马上销毁,所以他们 还要存在很长时间,而test2则只是在内存中保存一份对象的引用,而不必创建大量新临时变量.从而降低了内存的使用.

另外不要对同一个对象初始化多次.例如:

public class A{

private Hashtable table = new Hashtable();

public A(){

table = new Hashtable(); // 这里应该去掉,因为table已经被初始化.

}

}

这样就new了两个Hashtable,但是却只使用了一个.另外一个则没有被引用.而被忽略掉.浪费了内存.并且由于进行了两次new操作.也影响了代码的执行速度。

应用阶段:即该对象至少有一个引用在维护他。

不可视阶段:即超出该变量的作用域.这里有一个很好的做法,因为JVM在GC的时候并不是马上进行回收,而是要判断对象是否被其他引用在维护.所 以,这个时候如果我们在使用完一个对象以后对其obj=null或者obj.doSomething()操作,将其标记为空,可以帮助JVM及时发现这个 垃圾对象。

不可到达阶段:就是在JVM中找不到对该对象的直接或者间接的引用。

可收集阶段,终结阶段,释放阶段:此为回收器发现该对象不可到达,finalize方法已经被执行,或者对象空间已被重用的时候。

二、java内存管理特点

Java一个最大的优点就是取消了指针,由垃圾收集器来自动管理内存的回收。程序员不需要通过调用函数来释放内存。

(1)Java的内存管理就是对象的分配和释放问题。

在Java中,内存的分配是由程序完成的,而内存的释放是有GC完成的,这种收支两条线的方法简化了程序员的工作。但也加重了JVM的工作。这也是Java程序运行速度较慢的原因之一。【还有几种其他原因呢?】

在Java中,程序员需要通过关键字new为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间,对象的释放是由GC决定和执行的。

GC释放空间方法:监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等。当该对象不再被引用时,释放对象,但程序中对GC的操作并不一定能 达到管理内存的效果,GC对于程序员来说基本是透明的,不可见的。我们只有几个函数可以访问GC,例如运行GC的函数System.gc()。但是根据 Java语言规范定义,System.gc()函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通 常,GC的线程的优先级别较低【是否可以调节】,而且强制内存回收对于系统自动的内存回收机制会产生负面影响,会加大系统自动回收的处理时间,所以应该尽量避免显式使用 System.gc(),

JVM调用GC的策略有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来 说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应 用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的 HotSpot JVM就支持这一特性。

(2)内存管理结构

Java使用有向图的方式进行内存管理,对于程序运行的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。

将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个 对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。

(3)使用有向图方式管理内存的优缺点

Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。这 种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精 度行低(很难处理循环引用的问题),但执行效率很高。

三、Java的内存泄露

Java虽然由GC来回收内存,但也是存在泄露问题的,只是比C++小一点。

(1)与C++的比较

C++所有对象的分配和回收都需要由用户来管理。即需要管理点,也需要管理边。若存在不可达的点,无法在回收分配给那个点的内存,导致内存泄露。存在无用的对象引用,自然也会导致内存泄露。

Java由GC来管理内存回收,GC将回收不可达的对象占用的内存空间。所以,Java需要考虑 的内存泄露问题主要是那些被引用但无用的对象——即指要管理边就可以。被引用但无用的对象,程序引用了该对象,但后续不会再使用它。它占用的内存空间就浪 费了,如果存在对象的引用,这个对象就被定义为“活动的”,同时不会被释放。

(2)Java内存泄露处理

处理Java的内存泄露问题:确认该对象不再会被使用,接着典型的做法——把对象数据成员设为null

注意,当局部变量不需要时,不需明显的设为null,因为一个方法执行完毕时,这些引用会自动被清理。

例子:

List myList=new ArrayList();

for (int i=1;i<100; i++)

{

Object o=new Object();

myList.add(o);

o=null;

}

此时,所有的Object对象都没有被释放,因为变量myList引用这些对象。当myList后来不再用到,将之设为null,释放所有它引用的对象。之后GC便会回收这些对象占用的内存。

(3)内存泄露检测

市场上已有几种专业检查Java内存泄漏的工具,它们的基本工作原理大同小异,都是通过监测Java程序运行时,所有对象的申请、释放等动作,将内存管理 的所有信息进行统计、分析、可视化。开发人员将根据这些信息判断程序是否有内存泄漏问题。这些工具包括Optimizeit Profiler,JProbe Profiler,JinSight, Rational公司的Purify等。

在运行过程中,我们可以随时观察内存的使用情况,通过这种方式,我们可以很快找到那些长期不被释放,并且不再使用的对象。我们通过检查这些对象的生存周期,确认其是否为内存泄露。

四、java程序设计中有关内存管理的经验

1. 最基本的建议是尽早释放无用对象的引用。如:…

A a = new A();

//应用a对象

a = null; //当使用对象a之后主动将其设置为空

….

注:如果a 是方法的返回值,不要做这样的处理,否则你从该方法中得到的返回值永远为空,而且这种错误不易被发现、排除

2. 尽量少用finalize函数。它会加大GC的工作量。

3. 注意集合数据类型,包括数组、树、图、链表等数据结构,这些数据结构对GC来说,回收更为复杂。

4. 尽量避免在类的默认构造器中创建、初始化大量的对象,防止在调用其自类的构造器时造成不必要的内

存资源浪费。由于对象的创建是递归式的,也就是先调用超级类的构造,然后依次向下递归调用构造函数,

所以应该避免在类的构造函数中初始化变量,这样可以避免不必要的创建对象造成不必要的内存消耗.当

然这里也就看出来接口的优势。

5. 尽量避免强制系统做垃圾内存的回收,增长系统做垃圾回收的最终时间

6. 尽量避免显式申请数组空间

7. 别用new Boolean()

在很多场景中Boolean类型是必须的,比如JDBC中boolean类型的set与get都是通过Boolean封装传递的,大部分ORM也是用Boolean来封装boolean类型的,比如:

ps.setBoolean(“isClosed”,new Boolean(true));

ps.setBoolean(“isClosed”,new Boolean(isClosed));

ps.setBoolean(“isClosed”,new Boolean(i==3));

通常这些系统中构造的Boolean实例的个数是相当多的,所以系统中充满了大量Boolean实例小对象,这是相当消耗内存的。Boolean类实际上只要两个实例就够了,一个true的实例,一个false的实例。

Boolean类提供两了个静态变量:

public static final Boolean TRUE = new Boolean(true);

public static final Boolean FALSE = new Boolean(false);

需要的时候只要取这两个变量就可以了,

比如:ps.setBoolean(“isClosed”,Boolean.TRUE);

那么象2、3句那样要根据一个boolean变量来创建一个Boolean怎么办呢?可以使用Boolean提供的静态方法:Boolean.valueOf()

比如:

ps.setBoolean(“isClosed”,Boolean.valueOf(isClosed));

ps.setBoolean(“isClosed”,Boolean.valueOf(i==3));

因为valueOf的内部实现是:return (b ? TRUE : FALSE);

所以可以节省大量内存。相信如果Java规范直接把Boolean的构造函数规定成private,就再也不会出现这种情况了。

8. 别用new Integer

和Boolean类似,java开发中使用Integer封装int的场合也非常多,并且通常用int表示的数值通常都非常小。SUN SDK中对Integer的实例化进行了优化,Integer类缓存了-128到127这256个状态的Integer,如果使用 Integer.valueOf(int i),传入的int范围正好在此内,就返回静态实例。这样如果我们使用Integer.valueOf代替new Integer的话也将大大降低内存的占用。如果您的系统要在不同的SDK(比如IBM SDK)中使用的话,那么可以自己做了工具类封装一下,比如IntegerUtils.valueOf(),这样就可以在任何SDK中都可以使用这种特 性。

9. 用StringBuffer代替字符串相加

这个我就不多讲了,因为已经被人讲过N次了。我只想将一个不是笑话的笑话,我在看国内某“著名”java开发的WEB系统的源码中,竟然发现其中大量的使用字符串相加,一个拼装SQL语句的方法中竟然最多构造了将近100个string实例。无语中!

10. 不要过滥使用哈希表

有一定开发经验的开发人员经常会使用hash表(hash表在JDK中的一个实现就是HashMap)来缓存一些数据,从而提高系统的运行速度。比如使用HashMap缓 存一些物料信息、人员信息等基础资料,这在提高系统速度的同时也加大了系统的内存占用,特别是当缓存的资料比较多的时候。其实我们可以使用操作系统中的缓 存的概念来解决这个问题,也就是给被缓存的分配一个一定大小的缓存容器,按照一定的算法淘汰不需要继续缓存的对象,这样一方面会因为进行了对象缓存而提高 了系统的运行效率,同时由于缓存容器不是无限制扩大,从而也减少了系统的内存占用。现在有很多开源的缓存实现项目,比如ehcache、oscache等,这些项目都实现了FIFO、MRU等常见的缓存算法。

11. 避免过深的类层次结构和过深的方法调用。因为这两者都是非常占用内存的(特别是方法调用更是堆栈空间的消耗大户)。

12. 变量只有在用到它的时候才定义和实例化。

13. 共享静态存储空间

我们都知道静态变量在程序运行期间其内存是共享的,因此有时候为了节约内存工件,将一些变量声明为静态变量确实可以起到节约内存空间的作用。但是由于静态变量生命周期很长,不易被系统回收,所以使用静态变量要合理,不能盲目的使用.以免适得其反。

因此建议在下面情况下使用:变量所包含的对象体积较大,占用内存过多;变量所包含对象生命周期较长;变量所包含数据稳定;该类的对象实例有对该变量所包含的对象的共享需求.(也就是说是否需要作为全局变量)。

并发编程探索-JMM和共享对象

很多程序员对一个共享变量初始化要注意可见性和安全发布(安全地构建一个对象,并其他线程能正确访问)等问题不是很理解,认为Java是一个屏蔽内存细节的平台,连对象回收都不需要关心,因此谈到可见性和安全发布大多不知所云。其实关键在于对Java存储模型,可见性和安全发布的问题是起源于 Java的存储结构。

Java存储模型(JMM)原理

有很多书和文章都讲解过Java存储模型,其中一个图很清晰地说明了其存储结构:

由上图可知, jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。 每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。

这个存储模型很像我们常用的缓存与数据库的关系,因此由此可以推断JVM如此设计应该是为了提升性能,提高多线程的并发能力,并减少线程之间的影响。

Java存储模型潜在的问题

一谈到缓存 我们立马想到会有缓存不一致性问题,就是说当有缓存与数据库不一致的时候,就需要有相应的机制去同步数据。同理,Java存储模型也有这个问题,当一个线程在自己工作内存里初始化一个变量,当还没来得及同步到主存里时,如果有其他线程来访问它,就会出现不可预知的问题。另外,JVM在底层设计上,对与那些没有同步到主存里的变量,可能会以不一样的操作顺序来执行指令,举个实际的例子:

public class PossibleReordering {
static int x = 0, y = 0;
static int a = 0, b = 0;
public static void main(String[] args)
throws InterruptedException {
        Thread one = new Thread(new Runnable() {
public void run() {
                a = 1;
                x = b;
            }
        });
        Thread other = new Thread(new Runnable() {
public void run() {
                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存储模型后,再来看共享对象可见性和安全发布的问题就较为容易了

共享对象的可见性

当对象在从工作内存同步到主内存之前,那么它就是不可见的。若有其他线程在存取不可见对象就会引发可见性问题,看下面一个例子:

 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,但其实际结果会非常奇怪,可能会永远没有输出(因为ready为false),可能会输出0(因为重排序问题导致ready=true先执行)。再举一个更为常见的例子,大家都喜欢用只有set 和get方法的pojo来设计领域模型,如下所示:

@NotThreadSafe
public class MutableInteger {
private int value;
public int  get() { return value; }
public void set(int value) { this.value = value; }
}

但是,当有多个线程同时来存取某一个对象时,可能就会有类似的可见性问题。

为了保证变量的可见性,一般可以用锁、 synchronized关键字、 volatile关键字或直接设置为final

共享变量发布

共享变量发布和我们常说的发布程序类似,就是说让本属于内部的一个变量变为一个可以被外部访问的变量。发布方式分为以下几种:

  • 将对象引用存储到公共静态域
  • 初始化一个可以被外部访问的对象
  • 将对象引用存储到一个集合里

安全发布和保证可见性的方法类似,就是要同步发布动作,并使发布后的对象可见。

线程安全

其实当我们把这些变量封闭在本线程内访问,就可以从根本上避免以上问题,现实中存在很多例子通过线程封闭来安全使用本不是线程安全的对象,比如:

  • swing的可视化组件和数据模型对象并不是线程安全的,它通过将它们限制到swing的事件分发线程中,实现线程安全
  • JDBC Connection对象没有要求为线程安全,但JDBC的存取模式决定了一个Connection只会同时被一个线程使用
  • ThreadLocal把变量限制在本线程中共享

reference:http://blog.csdn.net/cutesource/archive/2010/08/01/5780486.aspx

无觅相关文章插件,快速提升流量