3.1 可见性
程序清单3-1 在没有同步的情况下共享变量(不要这么做)
/** * 主线程和读线程都将访问共享变量:ready 和 number * 结果可能 * 1、 主线程先运行完,读线程后运行:读线程在控制台打印输出42, * 2、 读线程先执行完,主线程后执行:读线程在控制台打印输出0 * 3、 NoVisibility根本无法终止,读线程可能永远都看不到ready的值 * 4、 重排序(Reordering):读线程看到了主线程中顺序靠后写入的ready的值,但没看到主线程先写入的number的值 */ @NotThreadSafe public class NoVisibility { private static boolean ready; private static int number; //读线程 private static class ReaderThread extends Thread { @Override public void run() { while(!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }
程序清单3-2 非线程安全的可变整数类
//mutable 英[ˈmju:təbl] adj. 易变的,性情不定的; @NotThreadSafe public class MutableInteger { private int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } }
程序清单3-3 线程安全的可变整数类
@ThreadSafe public class SynchronizedInteger { @GuardedBy("this")private int value; public synchronized int getValue() { return value; } public synchronized void setValue(int value) { this.value = value; } }
程序清单3-4 数绵羊
/** * volatile通常用作某个操作完成、发生中断或者状态的标志 * volatile语义不足以确保递增操作(count++)操作的原子性 * asleep 状态量一旦在某个线程中被更改了,其它线程也应该看到 * */ @ThreadSafe public class CountSheep { volatile boolean asleep; public void countSomeSheep() { while(!asleep) { for (int i = 0; i < 100; i++) { System.out.println("sheep"+i); } } } }
3.2 发布与逸出
程序清单3-5 发布一个对象
/** * “发布”一个对象的意思是:是对象能够在当前作用域之外的代码中使用。 * 1、将一个指向该对象的引用保存到其他代码可以访问的地方 * 2、在某一个非私有的方法中返回该引用 * 3、将引用传递到其他类的方法中 * @author guchunchao * */ public class PublicObject { private static Set<Object> knownSecret; public void initialize() { knownSecret = new HashSet<>(); //将方法体中new的对象发布到了方法体外的变量中 } }
程序清单3-6 使内部的可变状态逸出(不要这么做)
/** * 按如下方式发布states,任何修改者都能修改这个数组的内容,不安全 * @author gcc */ public class UnsafeStates { private String[] states = {"aaa","bbb","ccc"}; public String[] getStates() { return states; } }
程序清单3-7 隐式地使this引用逸出(不要这么做)
public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener( new EventListener() { //发布EventListener时,也隐含的将ThisEscape实例本身:this发布了 public void onEvent(Event e) { doSomething(e); } } ); } }
程序清单3-8 使用工厂方法来防止this引用在构造过程中逸出
public class SafeListener { private final EventListener listener; private ThisEscape(EventSource source) { listener = new EventListener( public void onEvent(Event e) { doSomething(e); } ); } public static SafeLisener newInstance(EventSource source) { SafeLisener safe = new SafeLisener(); soure.registerListener(safe.listener); } }
3.3 线程封闭
程序清单3-9 基本类型的局部变量与引用变量的线程封闭性
/** * 栈封闭 * 由于任何方法都无法获得对基本类型的引用,因此基本类型的局部变量始终封闭在线程内 * @author guchunchao * */ public class BasicTypeStackClose { //candidate [ˈkændɪdət] 报考者; 申请求职者; 攻读学位者; 最后命运或结局如何已显然可见者; public int loadTheArk(Collection<Object> candidates) { SortedSet<Object> animals; int numPairs = 0; //方法局部变量,始终在栈中 Object candidate = null; animals = new TreeSet<Object>(); animals.addAll(candidates); ++numPairs; return numPairs; //即使被return了外界也无法修改。 } }
程序清单3-10 使用ThreadLocal来维持线程封闭性
/** * ThreadLocal对象通常用于防止对可变的、非线程安全的单实例变量(Singleon)或全局变量进行共享。 * ThreadLocal的get与set等访问接口或方法为每个使用该变量的线程都存有一份独立的副本,因此get总是能返回当前线程在调用set时设置的新值。 * * 单线程应用程序中维持一个全局的数据库连接,并在程序启动时初始化,从而避免在调用每个方法时都要传递一个Connection对象。 * * 而JDBC的连接对象不一定是线程安全的,因此当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。 * 将JDBC连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的JDBC连接。 * * 更多ThreadLocal内容见:http://www.cnblogs.com/dolphin0520/p/3920407.html */ public class ThreadLocalTemplate { private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { return DriverManager.getConnection("DB_URL"); //JDBC的Connection不一定是线程安全的,封装到ThreadLocal中保证每个线程都会拥有自己的JDBC连接对象副本 } }; public static Connection getConnection() { return connectionHolder.get(); } }
3.4 不变性
不可变对象一定是线程安全的! Why?
程序清单3-11 在不可变对象基础上构建不可变类
/** * final修饰的类: * 从字面意思来理解就是不会发生变化的类,那么是什么不会发生变化呢,其实就是类的状态,也就是不变类的实例一旦被创建,其状态就不会发生变化。 * 举个例子: * 如果人是一个class,那么我们中的每一个都是人这个类的具体的instance, * 如果人这个类只有一个状态就是生身父母,那么它就是一个不变类,因为每一个人在出生的那一刹那,生身父母就已经被设置了值,而且终生都不会发生变化。 * 不变类有什么好处呢? * 1) 不变类是线程安全的,由于不变类的状态在创建以后不再发生变化,所以它可以在线程之间共享,而不需要同步。 * 。。。。*/ @Immutable //[ɪˈmju:təbl]不可改变的; public final class ThreeStooges { private final Set<String> stooges = new HashSet<>();//[stu:dʒ]喜剧里的配角或丑角,助手,伙伴; public ThreeStooges() { stooges.add("hello"); stooges.add("world"); stooges.add("hello world"); } public boolean isStooge(String name) { return stooges.contains(name); } }
程序清单3-12 对数值及因数分解结果进行缓存的不可变容器类
/** * 对于在访问和更新多个相关变量是出现的竞态条件问题,可以通过将这些变量全部保存在一个不可变对象中来消除。 * 如果是一个可变的对象,那么就必须使用锁来确保原子性。 * 如果是一个不可变对象,那么当线程获得了对该对象的饮用后,就不必担心另一个线程会修改对象的状态。 * 如果要更新这些变量,可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。 * @author guchunchao * */ @Immutable public class OneValueCache {//存缓存的容器类 private final BigInteger lastNumber; private final BigInteger[] lastFactors; public OneValueCache(BigInteger lastNumber, BigInteger[] lastFactors) { this.lastNumber = lastNumber; this.lastFactors = lastFactors; } public BigInteger[] getLastFactors(BigInteger i) { if(lastNumber == null || lastNumber.equals(i)) return null; else return Arrays.copyOf(lastFactors, lastFactors.length); } } //程序清单3-13 使用指向不可变容器对象的 volatile类型引用以缓存最新结果 @ThreadSafe public class VolatileCacheFactorizer implements Servlet { private volatile OneValueCache cache = new OneValueCache(null, null); @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { BigInteger i = extractFromReqest(req); BigInteger[] factors = cache.getLastFactors(i); if(factors == null) { factors = factor(i); cache = new OneValueCache(i, factors); } encodeIntoResponse(res,factors); } }
3.5 安全发布
程序清单3-14 在没有足够同步的情况下发布对象(不要这么做)
//不安全的发布 public Holder holder; public void initialize() { holder = new Holder(42); }
程序清单3-15 由于未被正确发布,因此这个类可能出现故障
//不安全的发布 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."); } }