Java虚拟机在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域,这些区域包括方法区,堆,虚拟机栈,本地方法栈,程序计数器。方法区存储类信息,常量,字节码等数据,堆内存存储所有生成的对象,方法区和堆内存为所有线程共享,而虚拟机栈是每个线程独有的,也就是说每个线程有自己的虚拟机栈,线程执行时,每调用一个方法,就会在虚拟机栈上创建一个栈帧,栈帧信息包括方法的局部变量表,操作数栈。
Java内存模型将内存分为主存储器和工作内存,主存储器也叫主内存,对应着Java内存区域划分的堆内存,工作内存对应着虚拟机栈和本地方法栈,所有线程共享主存储器,但是每个线程有各自的工作内存。所有对象在主内存分配,线程不能直接使用主内存的内容,必须先将主内存的内容加载到工作内存后,才能使用,修改工作内存里的内容后,也只有同步到主内存后,别的线程才可以看到。Java 内存模型如下图所示:
线程无法直接操作主存储器(堆内存),因此它无法直接引用字段的值。当线程需要引用字段时,需要经过3个操作:
当线程再次引用同一字段时,线程可能直接引用刚才拷贝得到的工作拷贝,这时候执行的操作是use, 也可能再次从堆内存拷贝到工作内存区(虚拟机栈),然后拷贝到变量副本,最后才引用,这时候执行的操作是(read->load->use)。这两种方式的选择是由Java虚拟机执行子系统决定的。
线程无法直接对主存储器进行操作,因此它也无法将值直接指定给主存储器的字段。当线程需要赋值给主存储器的字段时,需要经过以下三个步骤:
当同一线程反复给同一字段赋值时,可能只会对工作拷贝赋值,即只执行assign操作,只有指定的最后结果才会拷贝到主存储器,即执行store->write,也可能每次赋值,都会先赋值给工作拷贝,然后拷贝到主存储器,即执行assign->store->write。这两种方式的选择由Java执行处理系统决定。
Java内存模型定义了8种操作来完成关于主内存和工作内存之间具体的交互,这些操作都是原子的,不可分割(long double类型除外)。这8种操作如下所示:
如果要把一个变量从主内存复制到工作内存,那就要按顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,那就要顺序地执行store和write操作。注意,Java内存模型只要求上述两个操作必须按顺序地执行,而没有保证必须是连续执行,也就是说read和load之间,store和write之间是可以插入其它指令的,如对内存中的变量a,b进行访问时,一种可能出现的顺序是read a, read b, load b, load a。除此之外,Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
synchronized有两项功能,线程同步和内存同步。
线程同步是指使用synchronized来制作临界区时,该区间仅让一个线程进行操作。在一个线程进入临界区后,其它线程必须在临界区的入口处等待,直到先前在临界区的线程执行完临界区代码后退出临界区,其它线程再竞争入口,谁胜利了,谁就能进入临界区执行操作。synchronized所规定的范围控制了线程的操作,这就是线程的同步。
内存的同步是指工作内存和主内存的同步问题。以下所陈述的内容可适用于synchronized方法与synchronized块两者。
进入synchronized时,如果工作内存有未映射到主内存的工作拷贝,则工作拷贝的值会被强制写入主内存,成为其它线程可以看得见的状态。然后工作内存上的所有工作拷贝都会被丢弃。之后,欲引入主内存上的值的线程,必定会从主内存将值拷贝到工作内存。
总而言之,工作内存的内容会和主内存的内容予以同步。
如果工作内存有未映像到主内存的工作拷贝,则工作拷贝的值会被强制写入主内存。但是退出synchronized时,不会清空工作内存的工作拷贝,也就是说后续会直接使用工作内存上的工作拷贝。
注意synchronized关键字与内存交互操作的lock、unlock并无直接关系,进入synchronized块和退出synchronized块时怎么执行lock和unlock操作,要看虚拟机如何实现。
synchronized方法编译后对应的方法字节码里有属性ACC_SYNCHRONIZED,而synchronized代码块编译生成的字节码里有monitorenter和monitorexit指令,而关于这两条指令以及带有ACC_SYNCHRONIZED属性的方法如何执行要看虚拟机如何实现,可参考《Java虚拟机规范》以及《Java语言规范》
volatile仅进行内存同步,并不进行线程同步。当线程引用volatile字段时,通常都会发生从主内存器到工工作的拷贝,当线程给volatile字段赋值时,会发生从工作内存到主内存的拷贝。
Java内存模型要求lock,unlock,read,load,assign,use,store和write这八个操作都具有原子性,但是对于64位的数据类型(long和double),在模型中定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32的操作来进行,即允许虚拟机可以不保证64位数据类型的load,store,read和write操作的原子性,这点就是long和double的非原子协定。如果有多个线程共享一个未声明为volatile的long或double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读到一个既非原值,也不是其他线程修改值的代表了“半个变量”的数值。
实际开发中,目前各种平台下的商用虚拟机几乎都把64位数据的读写操作为原子操作来对待,因此我们在编写代码时一般不需要将用到的long和double变量专门声明为volatile。
早期内置锁synchronized的性能比较低,所以在实现懒汉式单例模式时采取Double Checked Locking Pattern模式,它通过尽量少执行内置锁的锁定以提高性能,如下面的代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class MySystem { private static MySystem instance = null; private Date date = new Date(); private MySystem() { } public static MySystem getInstance() { if (instance == null) { //(a) 第一次test synchronized (MySystem.class) { //(b) 进入synchronized块 if (instance == null) //(c) 第二次test instance = new MySystem(); //(d) set } //(e) //退出synchronized块 } return instance; //(f) } public Date getDate() { return date; } } |
在(a)if语句的条件检查下(第一次test),当instance等于null时,便会进入(b)的synchronized块,取得锁定的对象为MySystem.class,亦即MySystem Class对象。
(a)的条件检查位于临界区之外,在(c)重新执行条件检查时(第二次test),进行了同步,synchronized可以保证别的线程如果确实创建好了instance,本线程可以看到,当instance确实等于null时,便会在(d)处生成MySystem的实例。(d)的实例实在(b)~(c)的临界区中产生,因此不会产生两个以上的MySystem实例。
只有在(a)的条件测试下instance等于null时,才会进入(b)的synchronized块。因此,第二次及以后调用getInstance方法时,几乎都不会进入synchronized块。故此不用担心内置锁synchronized带来的性能问题。
表面上看Double Checked Locking模式完美地解决了synchronized引入的性能问题,只要创建好实例,就不会再进入synchronized块。但是Double Checked Locking Pattern引入了一个新的问题,就是当单例对象还没有完全构造好时,别的线程调用getInstance可能返回单例对象,此时单例对象的某些字段可能为空。以上述程序为例,某个线程调用MySystem.getInstance().getDate()方法可能返回null,这看起来有点不可思议,我们来分析一下,假设有如下的线程执行顺序(只是一种可能):
线程A | 线程B |
---|---|
(A-1)在(a)处判断instance == null | |
(A-2)在(b)处进入synchronized块 | |
(A-3)在(c)判断instance==null | |
(A-4)在(d)制作MySystem的实例,指定给instance字段 | |
线程在此处更新,线程A将instance字段从工作内存拷贝到主内存, 线程B就可以看到instance不为null | |
(B-1)在(a)判断instance=null | |
(B-2)在(f)将instance的值设为getInstance的返回值 | |
(B-3)调用getInstance返回值之getDate方法 |
在制作MySystem的实例时,New Date()的值会指定给实例字段date,但这只是线程A对工作内存上的date工作拷贝进行赋值。若线程A退出synchronized块,date字段的值肯定会写入到主内存,但在退出前,date字段的值不保证会写入到主内存。instance=MySystem()也是一样,退出synchronized前instance字段的值可能也没有写入到主内存,但是也可能写入到主内存了。这里假设的线程执行顺序里,退出synchronized前,instance字段的值已经被写入到主内存,但是date字段的值并未写入到主内存,这是符合Java虚拟机规范的。
此时,线程B在(B-1)判断instance!=null,如此线程便不会进入synchronized块,而会立刻将getInstance的值当作返回值予以返回(return)。然后,线程B会在(B-3)调用getInstance的返回值的getDate方法,GetDate的返回值为date字段的值,线程B会引用date字段的值,这时候线程B会引用自己工作内存的date字段的值,结果date字段不在工作内存里,需要从主内存拷贝到工作内存,然而主内存里date字段的值为空,故此线程B调用MySystem.getInstance().getDate()方法返回null。
安全实现懒汉式单例模式的示范代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class MySystem { private Date date = new Date(); private MySystem() { } private static class MySystemHolder { private static MySystem instance = new MySystem(); } public static MySystem getInstance() { return MySystemHolder.instance; } public Date getDate() { return date; } } |
实现原理:Java虚拟机在加载类的时候会在使用某个类时才会加载该类,调用MySystem.getInstance()方法会执行return MySystemHodler.instance,此时才会加载MySystemHolder类,另外Java虚拟机会保证加载类是线程安全的,故此上述代码是线程安全的。
转载 http://www.cloudchou.com/softdesign/post-631.html