第三章
3.3.2 栈限制
栈限制是线程限制的一种特例,只能通过本地变量才可以触及对象。本地变量使对象更容易被限制在线程本地中,本地变量本身就限制在执行线程中,存在于执行线程。其他线程无法访问这个栈。
- 如下面代码所示,我们的numPairs是基本类型的本地变量,无法利用栈限制,由于无法获得基本类似的引用,所以语言语义确保了基本类型本地变量总是线程封闭的。
- 维护对象引用的栈限制,我们就需要确定引用的对象没有溢出。在下面代码中我们实例化的一个TreeSet的animals对象并且保存了一个到animals集合中的一个元素的引用。此时只有一个引用指向集合animals,因此它被限制在保存本地变量的执行线程中,但是如果我们发布了到集合animals(或者其他任何内部数据)的引用,那么将会破坏限制性,也将导致animals对象的溢出。
public int loadTheArk (Collection<Animal> candidates ){
SortedSet<Animal> animals ;
int numPairs = 0;
Animal candidate = null ;
//animals被限制在方法中,不要让它溢出
animals = new TreeSet<Animal>(new SpecialsGenderComparator());
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.3.3 Threadlocal
- threadlocal允许我们将每个线程与特有数值的对象关联在一起。
- threadlocal提供了get-set访问器,为每个使用它的线程维护一份单独的拷贝。所以get总是返回由当前执行线程设置的最新值
- threadlocal变量通常用于防止在基于可变的单体或全局变量的设计中,出现不正确的共享。
- 使用threadlocal确保线程的封闭性
private static ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<>(){
public Connection initialValue(){
try {
return DriverManager.getConnection(DB_URL);
} catch (SQLException e) {
e.printStackTrace();
}
}
};
public static Connection getConnection(){
return connectionThreadLocal.get();
}
在将一个单线程的应用迁移到多线程环境中,我们可以将共享的全局变量全都转换成ThreadLocal类型,这样可以确保线程安全。前提是全局共享变量的语义允许这样。
3.4 不可变性
多线程并发工作的难点在于:多个线程各种难以预料的行为协同工作,多个线程总是试图同时访问相同的可变状态。如果对象的状态不可修改,那么这些风险自然就没有了。
创建后,状态不能被修改的对象叫做不可变对象。不可变对象天生就是线程安全的 。
不可变对象是简单的,只有一种状态,由构造函数谨慎的控制着这个状态。
- 不可变对象不等同于将对象所有属性声明为final类型。因为final类型的对象任然是可以改变的。因为final可以获得一个到可变对象的引用。记住:对象不可变和到对象的引用不可变是不同的,如下代码所示:
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();
}
}
public class FinalDemo {
private static final int[] numbers =new int[]{1,2,3,4};
private int[] changeInt(){
numbers[0]=55;
numbers[1]=55;
return numbers;
}
private int[] getInt(){
return numbers;
}
public static void main(String[] args) {
FinalDemo demo = new FinalDemo();
demo.changeInt();
System.out.println(demo.getInt()[0]);
}
}
- 不可变对象的约束条件:
- 1 状态在创建后不可修改
- 2 所有状态都是final类型,并且,
- 3 它被正确的创建(创建期间没有发生this溢出)
3.4.1 Final域
- 将所有的域声明为私有的,除非它们需要更高的可见性
- 将所有的域声明为final类型,除非它们是可变的
3.4.2 使用volatile发布不可变对象
- 通过使用不可变对象来持有所有的变量,可以消除在访问和更新这些变量时的竞争条件。
- 不可变的容器对象持有不变约束相关的多个状态变量,并利用volatile引用确保及时的可见性,这两个前提 条件保证了即使VolatileCachedFactorizer 没有显式的用到锁,但任然线程是安全的。
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);
}
}
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};
}
}
3.5 安全发布
public class UnSafeHolder {
private Holder holder;
public void initialize(){
holder = new Holder(42);
}
}
这个居然是不安全发布,你敢信!!!
分析一下:由于可见性的问题,容器是可能会在其他线程中被设置为一个不一致的状态,即使它的不变约束已经在构造函数中得以正确的创建,这种不正确的发布导致其他线程可以观察到“局部创建对象”
3.5.1 不正确发布:当好对象变坏时
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域的过期值,因而看到可能是null或者一个旧值。
- 其他线程看到的Holder引用是最新的,但是Holder的状态确实过期的。
3.5.2 不可变对象和初始化安全性
- 为了保证对象状态有一个一致性视图,我们需要同步
- 即使发布对象的时候没有同步,不可变对象仍然是可以被安全访问的。
- 为了保证正确初始化,应该满足所有不可变对象的条件:
- 不可修改的状态
- 所有域都是final类型的
- 正确的构造
- 如果final域指向的对象是可变对象,那么访问这些对象的状态时仍然需要同步
3.5.3 安全发布的模式
- 为了安全发布对象,对象的引用和对象的状态必须同时对其他线程可见。
一个正确创建的对象应该通过以下条件安全的发布
1 通过静态初始化器初始化对象的引用
private static Holder holder = new Holder(42);
2 将它的引用存储到volatile域或者AtomicReference
3 将它引用存储到正确创建的对象的final域中.
4 或者将它的引用存储到由锁正在保护的域中
线程安全库提供了如下的线程安全保证:
- 1 置于HashTable synchronized ConcurrentMap中的主键以及键值,会安全的发布到可以从map获得它们的任意线程中,无论是直接获取还是通过迭代器获得
- 2 置于Vector CopyOnWriterArrayList CopyOnWriterArraySet synchronizedList synchronizedSet 会安全的发布到可以从容器中获得它的任意线程中
- 3 置于BlockingQueue或者ConcurrentLinkedQueue的元素,会安全的发布到可以从队列中获得它的任意线程
3.5.4 高效不可变对象
一个对象在技术上不是不可变的,但是它的状态不会在发布后被修改,这样的对象称为高效不可变对象。
任何线程都可以在没有额外的同步的情况下使用一个安全发布的高效不可变对象
3.5.5 可变对象
发布对象的必要条件依赖于对象的可变性:
- 不可变对象可以通过任意机制发布
- 高效不可变对象必须要安全发布
- 可变对象必须要安全发布,同时必须要线程安全或者被锁保护起来
3.5.6 安全的共享对象
在并发编程中,使用和共享对象的一些最有效的策略如下:
- 1 线程限制:一个线程限制的对象,通过限制在线程中,而被线程占用,且只能被占用它的线程修改
- 2 共享只读 :一个共享的只读对象,在没有额外的同步的情况下,可以被多个线程并发访问,但是任何线程都不可以修改它。共享只读对象包括可变对象与高效不可变对象
- 3 共享线程安全:一个线程安全的对象在内部进行同步,所以其他线程无需额外的同步,就可以通过公共接口访问它
- 4 被守护的: 一个被守护的对象只能通过特定的锁来访问,被守护的对象包括那些被线程安全访问封装起来的对象。和已知被特定的锁保护起来的已发布的对象