2.对象的共享
一.可见性
在下面代码中,主线程和读线程都将访问共享变量ready和number。主线程启动读线程,然后将number 设为42,并将ready设为true。读线程一直循环直到发现ready的值变为true,然后输出number 的值。虽然NoVisibility 看起来会输出42,但事实上很可能输出0,或者根本无法终止。这是因为在代码中没有使用足够的同步机制,因此无法保证主线程写入的ready值和number值对于读线程来说是可见的。 在程序清单3-1中的Noviability说明了当多个线程在没有同步的情况下共享数据时出现的错误.在代码中,主线程和读线程都将访问共享变量就绪和号码。主线程启动读线程,然后将编号设为42,并将就绪设为true。读线程一直循环直到发现Ready的值变为true,然后输出Number的值.虽然Noviability看起来会输出42,但事实上很可能输出0,或者根本无法终止。这是因为在代码中没有使用足够的同步机制,因此无法保证主线程写入的就绪值和号码值对于读线程来说是可见的。
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; } }
NoVisibility可能会持续循环下去,因为读线程可能永远都看不到ready的值。一种更奇怪的现象是,NoVisibility可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值,这种现象被称为“重排序(Reordering)”。只要在某个线程中无法检测到重排序情况(即使在其他线程中可以很明显地看到该线程中的重排序),那么就无法确保线程中的操作将按照程序中指定的顺序来执行。当主线程首先写入number,然后在没有同步的情况下写入ready,那么读线程看到的顺序可能与写入的顺序完全相反。 Noviability可能会持续循环下去,因为读线程可能永远都看不到Ready的值。一种更奇怪的现象是,Noviability可能会输出0,因为读线程可能看到了写入Ready的值,但却没有看到之后写入Number的值,这种现象被称为“重排序(重新排序)”。只要在某个线程中无法检测到重排序情况(即使在其他线程中可以很明显地看到该线程中的重排序),那么就无法确保线程中的操作将按照程序中指定的顺序来执行.当主线程首先写入号码,然后在没有同步的情况下写入就绪,那么读线程看到的顺序可能与写入的顺序完全相反。
1.失效数据
NoVisibility展示了在缺乏同步的程序中可能产生错误结果的一种情况:失效数据。当读线程查看ready变量时,可能会得到一个已经失效的值。除非在每次访问变量时都使用同步,否则很可能获得该变量的-一个失效值。更糟糕的是,失效值可能不会同时出现:一个线程可能获得某个变量的最新值,而获得另一个变量的失效值。 Noviability展示了在缺乏同步的程序中可能产生错误结果的一种情况:失效数据.当读线程查看就绪变量时,可能会得到一个已经失效的值。除非在每次访问变量时都使用同步,否则很可能获得该变量的-一个失效值.更糟糕的是,失效值可能不会同时出现:一个线程可能获得某个变量的最新值,而获得另一个变量的失效值。
下面MutableInteger 不是线程安全的,因为get和set都是在没有同步的情况下访问value的。与其他问题相比,失效值问题更容易出现:如果某个线程调用了set,那么另一个正在调用get的线程可能会看到更新后的value值,也可能看不到。 因为Get和Set都是在没有同步的情况下访问Value的.与其他问题相比,失效值问题更容易出现:如果某个线程调用了集,那么另一个正在调用获取的线程可能会看到更新后的值值,也可能看不到。
@NotThreadSafe public class MutableInteger { private int value; public int get() { return value; } public void set(int value) { this.value = value; } }
下面SynchronizedInteger中,.通过对get和 set等方法进行同步,可以使MutableInteger成为一个线程安全的类。仅对set方法进行同步是不够的,调用get的线程仍然会看见失效值。 在程序清单3-3的同步集成中.通过对Get和Set等方法进行同步,可以使MutableInteger成为一个线程安全的类。仅对Set方法进行同步是不够的,调用Get的线程仍然会看见失效值.
@ThreadSafe public class SynchronizedInteger { @GuardedBy("this") private int value; public synchronized int get() { return value; } public synchronized void set(int value) { this.value = value; } }
2.非原子的64位操作
非类型的64位数值变量(Double和Long)。内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非易失性类型的Long和Double变量,jvm允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非用关键字volatile来声明它们,或者用锁保护起来。
3.加锁与可见性
在访问某个共享且可变的变量时要求所有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其他线程来说都是可见的。否则,如果一个线程在未持有正确锁的情况下读取某个变量,那么读到的可能是-一个失效值。
4.Volatile变量
当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
然而,我们并不建议过度依赖volatile变量提供的可见性。如果在代码中依赖volatile变量来控制状态的可见性,通常比使用锁的代码更脆弱,也更难以理解。
二.发布与逸出
当某个不应该发布的对象被发布时,这种情况就被称为逸出( Escape)。
class Secrets { public static Set<Secret> knownSecrets; public void initialize() { knownSecrets = new HashSet<Secret>(); } } class Secret { }
当发布某个对象时,可能会间接地发布其他对象。如果将一个Secret对象添加到集合knownSecrets中,那么同样会发布这个对象,因为任何代码都可以遍历这个集合,并获得对这个新Secret对象的引用。同样,如果从非私有方法中返回一个引用,那么同样会发布返回的对象。下面UnsafeStates 发布了本应为私有的状态数组。
class UnsafeStates { private String[] states = new String[]{ "AK", "AL" /*...*/ }; public String[] getStates() { return states; } }
三.线程封闭
当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭。
1.Ad-hoc线程封闭
Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。
2.栈封闭
栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。正如封装能使得代码更容易维持不变性条件那样,同步变量也能使对象更易于封闭在线程中。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。栈封闭(也被称为线程内部使用或者线程局部使用,不要与核心类库中的ThreadLocal混淆)比Ad-hoc线程封闭更易于维护,也更加健壮。 对于基本类型的局部变量,下面 loadTheArk方法的numPairs,无论如何都不会破坏栈封闭性。由于任何方法都无法获得对基本类型的引用,因此Java语言的这种语义就确保了基本类型的局部变量始终封闭在线程内。
public int loadTheArk(Collection<Animal> candidates) { SortedSet<Animal> animals; int numPairs = 0; Animal candidate = null; // animals confined to method, don't let them escape! 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; }
3.ThreadLocal类
维持线程封闭性的-种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享例如,在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个接对象,从而避免在调用每个方法时都要传递一个Connection对象。由于JDBC的连接对象一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是:程安全的。通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的;接,如下面ConnectionHolder所示。
public class ConnectionDispenser { static String DB_URL = "jdbc:mysql://localhost/mydatabase"; private ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { try { return DriverManager.getConnection(DB_URL); } catch (SQLException e) { throw new RuntimeException("Unable to acquire Connection, e"); } }; }; public Connection getConnection() { return connectionHolder.get(); } }
四.不变性
在不可变对象的内部仍可以使用可变对象来管理它们的状态,下面的ThreeStooges 所示。尽管保存姓名的Set对象是可变的,但从 ThreeStooges的设计中可以看到,在Set对象构造完成后无法对其进行修改。stooges是一个final类型的引用变量,因此所有的对象状态都通过一个final域来访问。最后一个要求是“正确地构造对象”,这个要求很容易满足,因为构造函数能使该引用由除了构造函数及其调用者之外的代码来访问。
@Immutable public final class ThreeStooges { private final Set<String> stooges = new HashSet<String>(); public ThreeStooges() { stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); } public boolean isStooge(String name) { return stooges.contains(name); } public String getStoogeNames() { List<String> stooges = new Vector<String>(); stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); return stooges.toString(); } }
1.Final域
2.示例:使用Volatile类型来发布不可变对象
用volatile 类型的变量来保存这些值也不是线程安全的。 时读取或更新这两个相关的值.同样,用挥发性类型的变量来保存这些值也不是线程安全的. 然而,在某些情况下,不可变对象能提供- -种弱形式的原子性。
@Immutable public class OneValueCache { private final BigInteger lastNumber; private final BigInteger[] lastFactors; public OneValueCache(BigInteger i, BigInteger[] factors) { lastNumber = i; lastFactors = Arrays.copyOf(factors, factors.length); } public BigInteger[] getFactors(BigInteger i) { if (lastNumber == null || !lastNumber.equals(i)) return null; else return Arrays.copyOf(lastFactors, lastFactors.length); } }
@ThreadSafe public class VolatileCachedFactorizer extends GenericServlet implements Servlet { private volatile OneValueCache cache = new OneValueCache(null, null); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = cache.getFactors(i); if (factors == null) { factors = factor(i); cache = new OneValueCache(i, factors); } encodeIntoResponse(resp, factors); } void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) { } BigInteger extractFromRequest(ServletRequest req) { return new BigInteger("7"); } BigInteger[] factor(BigInteger i) { // Doesn't really factor return new BigInteger[]{i}; } }
与cache相关的操作不会相互干扰,因为OneValueCache是不可变的,并且在每条相应的 代码路径中只会访问它一一次。通过使用包含多个状态变量的容器对象来维持不变性条件,并使 用一个volatile类型的引用来确保可见性,使得Volatile Cached Factorizer 在没有显式地使用锁的情况下仍然是线程安全的。 的情况下仍然是线程安全的.
五.安全发布
如下面将对象引用保存到公有域中,还不足以安全地发布这个对象。
public class StuffIntoPublic { public Holder holder; public void initialize() { holder = new Holder(42); } }
1.不正确的发布:正确的对象被破坏
你不能指望一个尚未被完全创建的对象拥有完整性。某个观察该对象的线程将看到对象 处于不一致的状态,然后看到对象的状态突然发生变化,即使线程在对象发布后还没有修改过 处于不一致的状态,然后看到对象的状态突然发生变化,即使线程在对象发布后还没有修改过 它。事实上,下面的Holder使用上面中的不安全发布方式,那么另 一个线程在调用assertSanity时将抛出AssertionError.
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."); } }
2.不可变对象与初始化安全性
任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。
3.安全发布的常用模式
4.事实不可变对象
5.可变对象
6.安全地共享对象