java内存模型要求变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的64位数值变量(double,long),jvm允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非使用volatile来声明他们或者用锁保护起来。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
把变量声明为volatile类型后,编译与运行时都会注意到这个变量是共享的,因此不会讲该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存到寄存器或者对其他处理器不可见的地方,因此在读取volatile变量时总会返回最新写入的值。
比如,while循环的条件变量,如果监视是否改变,需要设置为volatile,否则访问while的线程会将asleep放到工作区,一直循环直到某刻去内存读取。
volatile boolean asleep; ... while(!asleep) countSomeSheep();
发布(Publish)一个对象是指:使对象能够在当前作用域之外的代码中使用。
在许多情况,我们要确保对象及其内部状态不被发布。而在某些情况,我又需要发布该对象,但如果在发布时要确保线程安全性,则可能需要同步。发布内部状态可能会破坏封装性,使线程难以维持不变的状态。例如,如果在对象构造完成之前就发布该对象,就会破坏线程安全性。
发布对象的最简单的方法是将对象的引用保存到一个共有的静态变量中。
逸出(Escape):当某个不应该发布的对象呗发布时。
class UnsafeStates{ private String[] states = new String[]{"as","dsf",...}; public String[] getStates(){return states;} }
如果按上述的方式发布states,则任何调用者都可以修改数组,这是不安全的。
上述构造函数好像没啥问题,至少我看本书之前看不出。构造函数中的对象是this,其他譬如引用类属性的拥有者为this,如果将这哥匿名类传递给source,source如果对其进行了引用,而这时候构造函数还没结束即没有创建ThisEscape的对象,这个匿名类也还没构造,即空。当且仅当对象的构造函数返回时,对象才处于一个可预测和一致的状态。
参考http://bruce008.iteye.com/blog/1461345
文章指出,匿名内部类编译的结果表明ThisEscape这个类会作为一个final成员变量放到匿名内部类中,即这个ThisEscape被逃逸出去了。因此不要再构造函数中发布匿名类和起线程。
当某个对象封闭在一个线程中时,这种方法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。
public int loadTheArk(Collection<Animal> candidates){ SortedSet<Animal> animals; int numPairs = 0; Animal candidate = null; //animals被封闭在方法中,不要使他们逸出 animals = new TreeSet<Animal>(new SpeciesGenderComparator()); animals.addAll(candidates); for (Animal a:animals) { if(candidate==null || !candidate.isPotentialMate(a)) candidate = a; else{ ark.load(new AnimalPair(candidate,a)); ++numPairs; candidate = null; } } return numPairs; }
如果在线程内部(Within-Thread)上下文中使用非线程安全的对象,那么该对象仍然是线程安全的。但要注意后期维护的时候对象逸出。
维持线程封闭性的一种更规范方法是使用ThreadLocal。ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。比如,在单线程应用中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个connection对象。由于jdbc的连接对象不一定是线程安全的,因此当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将jdbc的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接:
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){ public Connection initialValue(){ try { return DriverManager.getConnection("url","username","password"); } catch (SQLException e) { e.printStackTrace(); } return null; } }; public static Connection getConnection(){ return connectionHolder.get(); }
当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。
当某个线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值。
ThreadLocal变量类似于全局比阿娘,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。
满足同步需求的另一种方法是使用不可变对象(Immutable Object)。如果某个对象在被创建后其状态就不能修改,那么这个对象就成为不可变对象。
正如除非需要更高的可见性,否则应将所有的域都声明为私有域是一个良好的习惯,除非需要某个域是可变的,否则应将其声明为final域也是一个良好的编程习惯。
2.8 安全的发布
在某些情况下,我们希望在多个线程间共享对象,此时必须确保安全地进行共享。以下是错误的:
//不安全的发布 public Holder holder; public void initialize(){ holder = new Holder(22); }
看起来应该没什么问题才对,实际上多线程访问的时候可能得到一个尚未创建的对象。
再详细些:
public class Holder{ private int n; public Holder(int n){this.n = n} public void assertSanity(){ if(n!=n){ throw new AssertionError("This statement is false."); } } }