发布与逸出
-
本章介绍如何共享和发布对象,从而使它们能够安全的由多个线程同时访问。
-
定义 。发布:发布一个对象是指使对象能够在当前作用域之外的代码中使用(将一个指向对象的引用保存到其他代码可以访问的地方;在某一个非私有方法中返回该引用;将引用传递到其他类的方法中)。逸出:不该发布的对象被发布时(不想被发布的内部状态(私有,发布会破坏封装性);对象在构造完成前就被发布)。
-
发布对象的方式 :
1.将对象的引用保存到一个公有的静态变量中public static Set<Secret> knownSecrets; public void initialize() { knownSecrets = new HashSet<Secret>(); }
2,从非私有方法中返回一个引用
class UnsafeStates { private String[] states = new String[]{ "AK", "AL" /*...*/ };//私有可变状态 public String[] getStates() { return states; } }
当发布一个对象时,在该对象非私有域中引用的所有对象同样会被发布(逸出)。
3,构造过程中发布一个内部类实例
public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener(new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } }
当ThisEscape发布EventListener(或者启动一个线程,那么this引用在未构造完成之前就可以被新线程看见)时,也隐含的发布了ThisEscape本身,因为在这个内部类的实例中包含了对ThisEscape实例的隐含引用。因此当对象的构造函数中发布对象时,发布了一个尚未构造完成的对象(逸出)。
- 如何避免this引用在构造过程中逸出:
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; }
可见性
- synchronize同步代码块和同步方法除了确保原子的执行方式之外,还确保了另一个重要的方面:内存可见性(Memory Visibility).
- 定义:我们不仅希望某个线程正在使用对象状态而另一个线程在同时修改状态,而且希望确保当一个线程修改了对象状态后。其他线程能够看到状态的变化。
- Volatile关键字
- 每次针对Volatile修饰的变量的操作都激发一次load和save操作,本质上volatile不缓存,而是直接在内存中取值操操作(个人理解).因此在读取volatile类型的变量时总是会返回最新写入的值。
- volatile只确保可见性,不像加锁机制一样还确保原子性,所以volatile变量通常用作某个操作完成、发生中断或者状态的标志。
3 线程安全的具体方法
- 在上一章——线程安全性的学习中,了解到解决多线程并发访问操作状态变量的方法有三种:
不在线程内共享该状态变量。
将状态变量修改为不可变的变量。
在访问状态变量时使用同步。
3.1 线程封闭(不在线程内共享数据)
- 线程封闭(Thread Confinement):在单线程内访问数据就不需要同步。当某一个对象封闭在一个线程中时,这种用法自动实现线程安全性,即使被封闭的对象本身不是线程安全的。
- 线程封闭式在程序设计中的一个考虑因素,Java语言提供一些机制来维持线程封闭性,例如局部变量和ThreadLocal类,但是程序员仍然需要负责确保封闭在线程内的对象不会从现场中逸出。
3.1.1 Ad-hoc线程封闭
- Ad-hoc指维护线程封闭性的职责完全有程序实现来承担。(大概就是完全由自己实现吧,问就脆弱,问就别用)
3.1.2 栈封闭
- 栈封闭:只能通过局部变量才能访问对象。
- 例如loadTheArk方法中的numPairs,由于任何方法都无法获得对基本类型的引用,因此java语言的这种语义就确保了基本类型的局部变量时钟封闭在线程内。
public class Animals {
Ark ark;
Species species;
Gender gender;
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.1.3 ThreadLocal类
- ThreadLocal:将线程中的某个值与保存值得对象关联起来。ThreadLocal提供了get和set等访问接口或者方法,这些方法为每个使用该变量的线程都存有一份独立的副本( /可以将ThreadLocal视为包含了Map<Thread,T>,并不是这样实现的),因此get总是返回有当前线程在调用set时设置的最新值。
- ThreadLocal对象通常用于防止可变的单实例变量或全局变量进行共享。e.g
public class ConnectionDispenser {
static String DB_URL = "jdbc:mysql://localhost/mydatabase";
private ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
//当某个线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值
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();
}
}
通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。
- 当频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免每次执行时都重新分配该临时对象,可以使用ThreadLocal。例如,在java 5.0之前,Integer。toString()使用ThreadLocal对象来保存一个12字节大小的缓冲区,用于对结果进行格式化。
3.2 不变性(将状态变量修改为不可变的变量)
- 不可变对象一定是线程安全的。
- 不可变对线的满足条件:
1.对象创建后其状态不能修改。
2.对象的所有域都是final类型(1.final域中可以保存对可变对象的引用,所以只是条件之一;2.从技术上来说,不可变对象不需要将其所有的域声明为final类型,String就是如此,但需要深入理解java内存模型,对类的良性数据竞争做精确分析(不懂));
3.对象时正确创建的(在对象创建期间,没有this引用逸出)
3.2.1 使用volatile类型来发布不可变对象
- 不可变对象提供一种弱形式的原子性。
- 对数值及其因数分解结构进行缓存的不可变容器类。
public class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache(BigInteger i,
BigInteger[] factors) {
lastNumber = i;
//必须用copyof或者clone来初始化lastFactors(不懂)
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);
}
}
- 使用指向不可变容器的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);
if (factors == null) {
factors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
}
}
4 安全发布
- 第三节讨论的是如何确保对象不被发布(如线程封闭),如果希望在多个线程内共享对象,如何确保安全性?
- e.g
public class StuffIntoPublic {
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
}
由于可见性问题,其他线程看到的Holder对象将处于不一致的状态,即便在该对象的构造函数中已经正确的构建了不变性条件。这种不正确的发布导致其他线程看到尚未创建完成的对象。
4.1 不正确的发布:正确的对象被破坏
public class StuffIntoPublic {
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
public void assertSanity(){
if(n!=n) throw new AssertionError("this statment is false");
}
由于没有使用同步来确保Holderd对象对其他线程可见,因此这是不正确的发布。一个线程在调用assertSanity方法时可能会抛出异常。
4.2 不可变对象与初始化安全性
- 任何线程都可以再不需要额外同步的情况下安全的访问不可变对象,即使在发布这些对象时没有同步。
4.3安全发布的常用模式
1.在静态初始化函数中初始化一个对象引用
2.将对象的引用保存到volatile类型的域。
3.将对象的引用保存到某个正确构造的final类型域中。
4.将对象的引用保存到一个由锁保护的域中。
- 将对象放入某个线程安全库中的容器里(例如synchronizeMap,vector),将满足第4个模式。