(一):同步的另一个重要方面:内存可见性
//我们不仅希望防止某个线程在使用对象状态时,另一个线程在同时修改对象。//也希望当一个线程修改了对象的状态后,其他线程都能够看到发生的状态变化。
多线程环境中,通常,我们无法确保执行读操作的线程 能够适时地看到其他线程写入的值,有时候甚至根本不可能。
//在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整(重排序)。//在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确结论。
简单的避免方法:只要有数据在多个线程之间共享,就使用正确的同步。
失效数据:
在缺乏同步的程序中可能产生错误的一种情况:读线程查看共享变量时,可能会得到一个已经失效的值。更糟糕的是,失效值可可能不会同时出现:可能会获得某个变量的最新值,又获得另一个变量的失效值。
失效值可能会导致一些严重的安全问题或活跃性问题。
//加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,//所有执行读操作或者写操作的线程都必须在同一个锁上同步。
(二):Volatile变量 (仅保持可见性,不同步)
java语言提供了一种削弱的同步机制:volatile变量,用来确保将变量的更新操作通知到其他线程。
java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值得低32位。
把变量声明为volatile类型后,编译器和运行时都会注意到这个变量是共享的,因此不会将变量上的操作与其他内存操作一起重排序。
volatile变量不会被缓存在寄存器或对其他处理器不可见的地方,因此读取volatile变量时总是会返回最新的值。
volatile变量不会执行加锁操作。
一种典型的用法:检查某个状态标记以判断是否退出循环。
volatile boolean asleep;...while(!asleep){ .....}
Volatile总结:加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
(三): 发布与逸出
发布:发布一个对象:使对象能够在当前作用域之外的代码中使用。
逸出:当某个不应该发布的对象被发布时,就被称之为逸出。
发布内部状态可能会破坏封装性,并使得程序难以维持不变性条件。
class States{ private String[] states = new String[]{"aa","bb","cc","dd"}; //private被发布,使其可以被修改。 //该数组已经逸出了它所在的作用域。 public String[] getStates(){ return states; }}
当把一个对象传递给某个外部方法时,就相当于发布了这个对象。
(四): 线程封闭
把对象封闭到一个线程里,只有这个线程能看到此对象。
1:Ad-hoc线程封闭(直接忽略)
维护线程封闭性的职责完全由程序实现来承担(非常脆弱)。
2:栈封闭(使用局部变量)
局部变量的固有属性之一就是封闭在执行的线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。
局部变量都是无状态的。
3:ThreadLocal类
(内部维护一个Map, key是线程的名称,value是要封闭的对象)
ThreadLocal类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get、set等访问接口或方法。这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回当前执行线程使用set时设置的最新值。
ThreadLocal变量类似于全局变量,他会降低代码可重用性,并在类之间引入隐含的耦合性,因此使用时要小心。
(五): 不变性
满足同步需求的另一种方法:使用不可变对象
不可变对象一定是线程安全的。
(不可变性并不等与对象中所有域都声明为final类型,因为final类型的域中可以保存对可变对象的引用。)