对象的共享
3.1 可见性:
通常,我们无法确保执行读操作的线程能适时的看到其他线程写入的值,为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制;
重排序:
在没有同步的的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意向不到的调整,JVM可以保证在单线程内,程序表现为串行语义(并非没有重排序,只是不影响结果);因为重排序是一种优化手段,能提升程序执行效率,所以为此牺牲一些易用性是值得的;
3.1.1 失效数据:
- 在缺乏同步的程序中,读线程获得的变量可能是失效值,如读多个变量,则可能一部分失效,一部分是最新值;某些失效值可能关系不大,但链表中的引用失效,情况就会非常复杂;
3.1.2 非原子的64位操作:
-
非原子的64位操作:当线程没有同步的情况下读取变量,可能会得到一个失效值,但至少这个值是之前某个线程设置的值,而不是一个随机值,这种安全性保证被称为最低安全性;最低安全性适用于绝大多数变量,但是存在一个例外:
非volatile类型的64位数值变量(double和long);java内存模型要求,变量的读取和写入操作都是原子操作,但对于非volatile类型的double和long变量,JVM允许将64位的读操作和写操作分解为两个32位操作,这意味着,对他们的读取很可能读取到某个值的高32位和另一个值的低32位,因此,及时不考虑失效数据的我呢提,在并发程序中使用共享的可变的long和doulbe等类型变量也是不安全的,除非使用关键字volatile来声明他们或者用锁保护起来;HotSpot虚拟机的实现可能并未拆分为2个32位操作,但我们编程还是遵循规范而非针对实现比较好;
3.1.3加锁与可见性:
加锁的含义不仅仅体现在互斥行为,还包括内存可见性。为了确保所有的线程都能看见共享变量的最新值,所有的执行读操作和写操作的线程都必须在同一个锁上同步;
3.1.4 volatile变量:
volatile:
-
volatile的正确使用方式:
- 确保自身的可见性;
- 确保他们所引用对象的状态的可见性
- 标识一些重要的程序生命周期事件的发生(如,初始化或关闭,线程退出等);
加锁操作既能确保可见性,又能确保原子性,而volatile只能确保可见性;
调试tips:
对于服务器应用程序,无论在开发阶段还是在测试阶段,当启动JVM时,一定都要指定-server命令行选项,server模式比client模式的JVM进行更多的优化, 如将循环内部未被修改的变量提升到循环外部,因此在开发环境中正确运行的代码可能会在部署环境中运行失败;如下程序:
volatile boolean interruptted = false;
while(!interruptted){
//do something
}
如果疏忽写漏了volatile,client模式JVM可能会表现正常,但server模式下,程序很有可能死循环;在应用环境中解决死循环问题的代价要大得多;
3.2 发布与逸出
定义:
发布一个对象的意思是指,使对象能够在当前作用于之外的代码中使用,例如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中;许多情况下,我们需要发布某个对象,但如果发布时要确保线程安全,则可能需要同步;发布内部状态可能会破坏封装性。当某个不应该发布的对象被发布时,这种情况就叫逸出;
发布一个对象:
当发布某个对象时,可能会间接地发布其他对象;该对象的非私有域所引用的对象和非私有方法调用达到其他对象,那这些对象也都会被发布,如发布引用类型数组对象,那么该数组中所有的引用对象都被发布;
隐式的this引用逸出:向外发布非静态内部类同时会隐式地将this发布出去,如果该操作在构造函数中,这将会把未构造完成的对象发布出去,类似的情况还存在于构造函数中调用可以被重写的方法,该方法在子类中被重写,那么实例化子类对象时,父类的初始化操作将访问还未被初始化的子类对象;
假定有一个类C,外部方法指行为不完全由C来控制的方法,包括其他类中定义的方法及C中可以被改写的方法,当把一个对象传递给某个外部方法时,就相当于发布了这个对象;不管外部方法将如何使用该对象,一旦某个对象逸出,我们都必须假设有某个类或者线程可能会吴用该对象,这正是使用封装的原因;就如同账号密码在网上被人发布,不管别人是否会恶意使用个人信息,但我们的账户都已经不再安全;