第三章 共享对象
在有的业务场景,不仅需要线程安全读写对象,还要让其他线程得知对象状态的更改,这必须由线程同步机制来完成。
3.1 可见性
当某个变量改变时,如果不运用线程同步机制,将无法确保另外的读线程能马上看到修改的结果。
例如下面的代码:
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; } }
尽管主线程很快的更改了number和ready,但是执行结果却是:读线程将挂起很久(因为无法立即看到ready变成true),甚至打印出来0(没有看到number的变化)。这其中的机制较为复杂。
3.1.1 陈旧的数据(Stale value)
更为复杂的是,由于Java内部的优化机制非常复杂,导致变量的可见性甚至是没有顺序的:可能后赋值的变量已经被读取了,但是先更新的变量却还是原来的数值。
因此进行变量控制是非常有必要的:
@ThreadSafe public class SynchronizedInteger { @GuardedBy("this") private int value; public synchronized int get() { return value; } public synchronized void set(int value) { this.value = value; } }
必须对getter和setter都添加上synchronized。
3.1.2 64比特的特例
对于64比特的变量,例如double和long,JVM允许将他们编程两次32bit的操作来读取!这会进一步引发问题。因此对于64bit的double和long,必须声明volatile或者锁机制。
一点从网上摘得介绍:http://netcome.javaeye.com/blog/648487
Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
3.1.3 锁定和可见性
Everything A did in or prior to a synchronized block is visible to B when it executes a synchronized block guarded by the same lock. Without synchronization, there is no such guarantee.
恩,就是这意思,进入锁->修改变量->释放锁,这之后,所有使用同样锁的代码块中,都可以立即看到变量的修改更新结果。
3.1.4 Volatile变量
被声明为Volatile的变量(使用volatile关键字)不会被Cache,任意线程读的话都会得到最新的结果。volatile不是万能的,不能代替synchronized,例如count++在volatile的情况下仍然不是原子操作。一般情况下只是为了确保变量的更新能马上被看到。
Volatile的用法多用于判断块/循环的终结:interruption, status flag等等,例如下面的代码:
volatile boolean asleep; ... while (!asleep) countSomeSheep();
Volatile的用法规则:
1、对变量的写操作,不依赖于当前的值。或者只有一个线程更新这个变量。
2、该变量不与其他变量一同参与类状态的改变。
3、由于某些原因,不需要锁。
3.2 变量发布和外泄(Publication and Escape)
变量发布:让变量能被代码作用域之外的区域访问到。如通过Referance、return等。变量的发布无所谓对错,单需要同步控制。
变量外泄:当变量不应该被别人看到的时候,却被发布了。
当变量外泄时,必须假定外部的函数可能威胁线程安全。
变量外泄有多种情况:容易被忽略的是匿名类。
public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener( new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } }
匿名类的new将使this外泄。特别是在外泄之时,ThisEscape还没有被构造完毕。
常见的错误时:在构造函数中启动线程,这回导致this变量外泄。此时新开启的线程与原线程共享同一个this,它具备了在为构造好this之前就访问Object的能力。如果同时start,则可能会导致比较大的问题。
因此最好将线程的启动等需要共享this的操作,放在除构造函数之外的函数中。如果一定要放在构造函数中,将构造函数设置为private,然后单独添加函数来访问新对象,如下图所示:
如下所示:
public class SafeListener { private final EventListener listener; private SafeListener() { listener = new EventListener() { public void onEvent(Event e) { doSomething(e); } }; } public static SafeListener newInstance(EventSource source) { SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe; } }
3.3 线程囚禁(Thread Confinement)
各种麻烦的线程同步问题有一个最简单的办法:不把数据共享给其他线程,每个线程独立使用自己的变量,这就是线程囚禁。
在JDBC、Swing等之中,都广泛的采用了这种技术解决线程安全问题。
线程囚禁分为几种:
1、自我线程囚禁:由线程自己负责,用的比较少因为限制较多。
2、栈囚禁:是指通过返回本地变量(或副本)等方法,由于永久类型(如int、long)无法用引用的方式表示,所以在函数中返回永久类型一定满足栈囚禁的条件,例如:
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; }
对于引用类型则更麻烦一下,在这个例子中,我们在函数内new了TreeSet,它只被唯一的animal所引用,而且animal是local变量。因此,仅在一个线程内使用非线程安全对象是安全的。
3、使用ThreadLocal
一种更通用的方法是使用ThreadLocal,它存储了每一个线程和对应独立的一份对象拷贝!
因此某个线程在ThreadLocal上get到的,是该线程最近一次set上去的值。
ThreadLocal常用来保护容易变动的单件类或全局变量。
典型的应用,也是我们基本都会写错的是JDBC的getConnection。
一个应用程序的不通线程可通过Singleton获取Connection,显然每个线程应该获取独立的Connection。这种情况应该使用ThreadLocal。
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { return DriverManager.getConnection(DB_URL); } }; public static Connection getConnection() { return connectionHolder.get(); }
通过重载initialValue( )方法,为尚未set过的Thread在get的时候提供初始值。父类ThreadLocal默认返回null。
实际上它也是线程Cache的一种好方法(用ThreadLocal封装个Buffer之类的)。
ThreadLocal在各种框架的多线程Context切换的时候非常有用。
3.4 不变对象
截止目前为止,几乎所有的线程安全隐患都是由多个线程尝试改变某状态所导致的。如果对象的状态是恒定不变的,则可以彻底的解决线程安全问题。
不变对象是指:构造函数之后,状态就不再改变的对象。不变对象只能由构造函数控制状态的改变,因此他们具有固有的线程安全性。
不变对象并非指field都为final的对象,因为字段可能是引用类型,引用不能改变,但引用的对象是可变的。
一个对象被称为可变对象当且仅当下述条件同时成立:
1、状态在构造函数之后便不能改变
2、所有字段都是final,并且已经被初始化完毕。
final对象如果无法被外界获取和改变即可。
不变对象具有较弱的原子特性。
例如前面存储被分解数和因子们的Cache的例子,由于无法将更新待操作数、分解因子放到同一个原子操作内,因此它不是线程安全的。
现在使用final字段来形成不变对象可以在一定程度上解决这个问题,构造一个不变对象同时存储数值和因子:
@Immutable 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); } }
此时,在Servlet中,每次的更新都将构造一个新的OneValueCache的不变对象,它总是原子的从一个状态更新到下一个状态。
此时无需锁机制也能实现线程安全!
3.5 安全发布
正如前面几节讨论的那样,有时候我们需要将类内的数值发布出去,但又要保证线程安全。
例如,使用上面提到的不变对象,将它应用在Servlet中:
@ThreadSafe public class VolatileCachedFactorizer 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); } }
注意,要使用volatale修饰变量,以便让某一线程触发的改动马上可见于其他线程。
而未经任何同步措施,就发布对象会导致线程不安全:
// Unsafe publication public Holder holder; public void initialize() { holder = new Holder(42); }
这种发布方式会导致非常诡异的结果:有的线程看到了完整构造的holder,有的却看到了没有构造的holder(null)。通常,这是灾难性的。
对可变对象而言,必须使用同步机制。
不可变对象则可以自由的发布(当满足了那些条件后)。
3.5.3 对象的安全发布规则
1、通过static的构造器初始化
2、将field变成volatile或者AtomicReferance
3、filed编程final并合理的构造函数
4、使用锁同步field
JDK中提供了一些安全发布的类的规则:
1、在下列容器put key/value的时候,可以安全的发布给其他线程:Hashtable, synchronizedMap, or Concurrent-Map
2、Vector, CopyOnWriteArrayList, CopyOnWrite-ArraySet, synchronizedList, or synchronizedSet之中put是线程安全的。
3、BlockingQueue和ConcurrentLinkedQueue是线程安全的。
而使用static来初始化是最好的保护方式:
public static Holder holder = new Holder(42);
JVM可以确保这个是线程安全的。
精华在这里,共享对象的几种形式:
1、线程内独享:只在线程内使用,不给其他线程用。
2、共享-只读:可以并发的被多个线程所用,不用添加同步机制,但是任何线程都不能修改它。主要指不变对象。
3、共享-线程安全:内部看守,因此各个线程可以线程安全的获取它。指ThreadLocal。
4、锁:由线程锁同步机制来完成。