满足同步需求的两种方法。
3.3 线程封闭(Thread Confinement)
可以避免多个线程共享的(线程封闭)
当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方法就是不共享数据。如果仅在单线程内访问数据,就不需要同步,这个技术称为线程封闭(Thread Confinement),它是实现线程安全性的最简单方法之一。当某个对象封闭在一个线程中,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。
线程封闭技术的常用应用是Swing和JDBC(Java Database Connectivity)的Connection对象。
3.3.1 Ad-hoc线程封闭
Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。事实上,对线程封闭对象的引用通常保存在公有变量中。
在volatile变量上存在一种特殊的线程封闭。只要你能确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在共享的volatile变量上执行“读入-修改-写入”的操作。在这种情况下,相当于将修改操作封闭在单个操作中以防止发生竞态条件,并且volation变量的可见性保证还确保了其他线程能看到最新的值。
由于Ad-hoc线程封闭技术的脆弱性,在程序中应尽量少用它。
3.3.2 栈封闭(Stack Confinement)
栈封闭(也称为线程内部使用或者线程局部使用)是线程封闭的一种特例。只能通过局部变量才能访问对象。封装能耐使代码更容易维持不变形条件,而同步变量也能使对象更易于封闭在线程中。局部变量的固有属性之一就是封闭在执行线程中,它们位于执行线程的栈中,其他线程无法访问这个栈。
比Ad-hoc线程封闭更易于维护,也更加健壮。
对于基本类型的局部变量,如下面的numPairs无论如何都不会破坏栈封闭性。由于任何方法都无法获得对基本类型的引用,因此Java的这种语义就确保了基本类型的局部变量始终封闭在线程内。
// 3-9 基本类型的局部变量与引用变量的线程封闭性
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;
}
在loadTheArk中实例化一个TreeSet对象,并将指向对象的一个引用保存到animals中。此时,只有一个引用指向结合animals,这个引用被封装在局部变量中,因此也被封闭在执行线程中。(局部变量的固有属性之一就是封闭在执行线程中,它们位于执行线程的栈中,其他线程无法访问这个栈。)然而,如果发布了对集合animals(或者对该对象中的任何内部数据)的引用,那么封闭性将被破坏,导致对象animals的逸出。
3.3.3 ThreadLocal类
维护线程封闭性更规范的方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。
ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。
以connectionHolder方法来讲解:在单线程应该程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个Connection对象。由于JDBC的连接对象不一定是线程安全的,因此,当多线程应该用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将JDBC的连接保存到ThreadLocal对象中,每个对象都会用于属于自己的连接。
// 3-10 使用ThreadLocal来维持线程封闭性
private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() { //将JDBC的连接保存到ThreadLocal对象中
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,都可以使用ThreadLocal来维持线程封闭性。
3.4 不变性(Immutability)
满足同步需求的另一个种方法就是使用不可变对象(Imutable Object)。
如果某个对象在被创建后其状态就不能被改变,那么这个对象就称为不可变对象。线程安全性是不可变对象的固有属性之一,它们的不变性条件是由构造函数床架的,只要它们的状态不改变,这些不变性条件就能维持。
不可变对象一定是线程安全的。.
即使对象中所有的域都是final类型,这个对象也依然是可变的,因为在final类型的域中可以保存对可变对象的引用。
当满足以下条件时,对象才是不可变的:
①对象创建以后其状态就不能修改
②对象的所有域都是final类型
③对象都是证券创建的(在对象的创建期间,this引用没有逸出)
在不可变对象的内部仍可以使用可变对象来管理它们的状态。
在ThreeStooges,尽管保存姓名的Set对象是可变的,但Set对象构造完成后无法对其进行修改(final)。stooges是一个final类型的引用变量,所有的对象状态都通过一个final域来访问
// 3-11 在可变对象基础上构建的不可变类
@Immutable
public final class ThreeStooges {
private final Set<String> stooges = new HashSet<String>();//被final修饰,stooges是一个final类型的引用变量,所有的对象状态都通过一个final域来访问
public ThreeStooges() {
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge(String name) {
return stooges.contains(name);
}
}
3.4.1 Final域(Final Fields)
关键字final用于构造不可变对象。final类型的域是不能修改的(但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的)。final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象的时候无需同步。
除非需要更高的可见性,否则应该将所有的域都声明为私有域,除非需要某个域是可变的,否则应该将其声明为final域,这是良好的编程习惯。
3.4.2 利用Volatile类型来发布不可变对象
因数分解Servlet将执行两个原子动作:更新缓存的结果,以及判断缓存中的数值是否等于请求的数值来决定是否可以直接读取缓存中的因数分解结果。每当需要对一组相关数据以原子执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据。
// 3-12 对数值及其因数分解结果进行缓存的不可变容器类
@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); //Arrays的copyOf()方法传回的数组是新的数组对象
}
public BigInteger[] getFactors(BigInteger i) { //缓存因数分解的结果
if (lastNumber == null || !lastNumber.equals(i))
return null;
else
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
如果是一个可变的对象,就必须使用锁来确保原子性。如果是一个不可变对象,那么当线程获得了对该对象的引用后,就不用担心另一个线程会修改对象的状态。如果要更新这些变量,可以创建一个新的容器对象,但其他使用原有对象的线程仍会看到对象处于一致的状态。
当一个线程将volatile类型的cache设置为引用一个新的OneValueCache时,其他线程可以立即看到新缓存的数据。
// 3-13 使用指向不可变容器对象的volatile类型引用以缓存最新的结果
@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); //判断是否有该数缓存,有就返回结果,这都在不可变容器类OneValueCache中完成
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相关的操作不会互相干扰,因为OneVauleCache是不可变的,并且在每条相应的代码路径中只会访问它一次。
3.5 安全发布(Safe Publication)
// 3-14 在没有足够同步的情况下发布对象(不要这么做)
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
由于可见性问题,其他线程看到的Holder对象将处于不一致的状态,即使在该对象的构造函数中已经正确地构建了不变性条件。
3.5.1 不正确的发布:正确的对象被破坏
某个观察该对象的线程将看到对象处于不一致的状态,然后看到对象的状态发生变化,即使线程在对象发布后还没有修改过它。使用下面不安全发布方式,另一个线程会调用assertSanity时会抛出AsserionError(使用错误,声明错误),如果用final修饰n则不会出现这种问题。
// 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.");
}
}
由于没有使用同步来确保Holder来确保Holder对象对其他线程可见,所以称为“未被正确发布”。
3.5.2 不可变对象与初始化安全性(Immutable Objects and Initialization Safety)
任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。为了维持这种初始化安全性的保证,必须满足不变性的所有需求:状态不可修改,所有域都是final类型,以及正确的构造过程。
在没有额外同步额情况下,也可以安全地访问final类型的域。然而,如果final类型的域指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。
3.5.3 安全发布的常用模式(Safe Publication Idioms)
可变对象必须通过安全的方式来发布。要安全地发布一个对象,对象的引用以及对象 的状态必须同时对其他线程可见。一个正确构造的对象可以通过一下方式来安全地发布:
①在静态初始化函数中初始化一个对象引用
②将对象的引用保存到volatile类型的域或者AtomicReferace对象中。
③将对象的引用保存到某个正确构造对象的final类型域中。
④将对象的引用保存到一个由锁保护的域中。(例如Vector或synchronizedList )
线程安全库中的容器类提供了以下的发布安全保证:
· 通过将一个键或值放入Hashtable,synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是迭代器访问)
· 通过将某个元素放入Vector,CopyOnWriteArrayList,CopyOnWriteArraySet,synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程
· 通过将某个元素放入BlockingQueue或ConcurrentLinkedQueue,可以将该元素安全地发布到任何从这些容器中访问该元素的线程
通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器:
public static Holder holder = new Holder(42);
3.5.4 事实不可变对象(Effectively Immutable Objects)
如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么这种对象称为事实不可变对象(Effectively Immutable Objects)。这些对象不需满足不可变性的严格定义。
在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。
例如,Data本身是可变的,但如果将它作为不可变对象来使用,那么在多个线程之间共享Data对象时,就可以省去对锁的使用。假设需要维护一个Map对象,其中保存了每位用户的最近登录时间:
public Map<String, Date> lastLogin =
Collections.synchronizedMap(new HashMap<String, Date>());
这些Data对象的值在被放入Map后就不会被改变,那么synchronizedMap中的同步机制就足够Data值被安全发布,并且在访问这些Data值时不需要额外的同步。
3.5.5 可变对象(Mutable Objects)
如果对象在构造后可以修改,那么安全发布只能确保“安全发布”状态的可见性。要安全地共享可变对象,这些对象就必须被安全地发布,并且必须是线程安全的或者某个锁保护起来。
对象的发布需求取决与它的可见性:
①不可变对象可以通过任意机制来发布
②事实不可变对象必须通过安全方式来发布
③可变对象必须被安全地发布,并且必须是线程安全的或者某个锁保护起来。
3.5.6 安全地共享对象(Sharing Objects Safely)
当发布一个对象时,必须明确说明对象的访问方式。
在并发程序中使用和共享对象时,可以使用一些实用的策略,例如:
线程封闭
线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改(栈封闭和ThreadLocal)
只读共享
在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程不能修改它,共享的只读对象包括不可变对象和事实不可变对象。
线程安全共享
线程安全的对象在其内部实现同步,因此多个线程可以通过对象的共有接口来访问而不需要进一步的同步。
保护对象
被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。