2.共享对象
//这一段代码是不安全的,结果不唯一//可能会一直保持循环,可能会输出0public class NoVisibility{private static boolean ready;private static int number;private static class ReaderThread extends Thread{public void ruun(){while(!ready)Thread.yield();System.out.println(number);}}public static void main(String[] args){new ReaderThread().start();number = 42;ready = true;}}
2.1可见性
在一个单线程的环境里,向一个变量先写入值,然后再没有写干涉的情况下读取这个变量,可以得到相同的返回值。但是,当读和写发生在不同的线程中时,情况却根本不是这样的。为了确保跨线程写入的内存可见性,必须使用同步机制。
**重排序**:在没有同步的情况下,java存储模型允许编译器重排序操作,在寄存器中缓存数值,还允许CPU重排序,并在处理器特有的缓存中缓存数值。
在没有同步的情况下,编译器,处理器,运行时安排操作的执行顺序可能完全出人意料。在没有进行适当同步的多线程程序中,尝试推断那些"必然"发生在内存中的动作时,你总是会判断错误。
2.1.1过期数据
一个线程可能会得到一个变量最新的值,也可能得到另一个变量先前写入的过期值。(过期既不会发生在全部变量上,也不会完全不出现)
2.1.2非原子的64位操作
当一个线程在没有同步的情况下读取变量,他可能会得到一个过期值。但至少它可以保证得到的是一个真实数值。(这被称为 **最低限的安全性**)
最低限的安全性适用于所有变量。除了:**没有声明为volatile的64位数值变量( double 和 long )**。
**因为**:JVM允许将**64位**的读或写**划分为两个32位**的操作。如果读和写发生在不同的线程,这种情况读取一个非volatile类型的long,就**可能会得到一个值的高32位和另一个值的低32位**。
2.1.3锁和可见性
当访问一个共享的可变变量时,要求所有线程由同一个锁进行同步。为了保证一个线程对数值进行的写入,其它线程也都可见。
锁不仅仅是关于同步与互斥的,也是关于内存可见的,为了保证所有线程都能够看到共享的、可变的最新值,读取和写入线程必须使用公共的锁进行同步。
2.1.4Volatile变量
volatile变量:它确保对一个变量的更新以可预见的方式告知其它线程,
当一个域被声明为volatile类型后,编译器与运行时会监视这个变量:**它是共享的,而且对它的操作不会与其它的内存操作一起被重排序**。
volatile变量不会缓存在寄存器或者缓存在对其他处理器隐藏的地方。
访问volatile变量的操作不会加锁。(轻量级的同步机制)
volatile变量通常被**当作标识完成、中断、状态的标记使用**。
加锁可以保证可见性和原子性,volatile变量只能保证可见性。
使用volatile注意点:
写入变量时并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
变量不需要与其它的状态变量共同参与不变约束
而且,访问变量时。没有其它的原因需要加锁。
2.2发布和逸出
发布:使一个对象能够**被当前范围之外的代码所使用**。
逸出:一个对象在尚未准备好时,就将它发布。
最常见的发布方式:
//将对象放入静态公共域中
//将一个Secret对象加入集合konwnSecrets中,就完成了发布
public static Set<Secret> knownSecrets;
一种隐式的逸出方式
public class ThisEscape{
public ThisEscape(EventSource source){
source.registerListener(
new EventListener(){
public void onEvent(Event e){
doSomething(e);
}
});
}
}
/**个人理解
new是分为三步的:
1.分配内存空间
2.初始化对象
3.将对象指向刚分配的内存空间变成了
由于jvm优化发生指令重排:
1.分配内存空间
2.将对象指向刚分配的内存空间
3.初始化对象
当我们执行到第二步的时候刚好有其他的线程在访问这个EventSource对象(我们传入的),这时候我们已经能够取到刚注册的监听器对象(逸出),但是由于他并不处于稳定状态,因为没有初始化完毕,那么该线程就会抛出异常,这个过程和双重检验锁的过程其实是类似的。
**/
//优化方式
//在创建类时仅仅只创建了监听事件,在另一个方法里进行使用,保证了执行顺序
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;
}
}
//当对象在构造函数中创建了一个线程时,无论是显示地还是隐式地,this引用几乎总是被新线程共享,于是新的线程在所属对象完成构造前就能看到它。
2.2.1安全构建的实践
如果this引用在构造过程中逸出,这样的对象被认为是“没有正确构建的”。
在构造函数中创建线程并没有错误。但是最好不要立即启动它。
可以使用一个私有的构造函数和一个公共的工厂方法,这样避免了不正确的创建。(实例见上一节的改进代码)
2.3线程封闭
线程封闭:将对象封闭在一个线程中。
这时候,就不需要做任何的同步,这种做法会自动成为线程安全的。
例子:
应用池化的JDBC Connection 对象。JDBC并没有要求 Connection对象是线程安全的。
然而,在典型的服务器应用中,线程总是从池中获得一个Connection对象,并用它处理一个单一的请求,最后把它归还。而且在Connection对象被归还前,池不会将它再分配给其它线程。
2.3.1Ad-hoc线程限制
Ad-hoc线程限制是指维护线程限制性的任务全部落在实现上的这种情况。(未经过设计而得到的线程封闭行为)
线程限制的一种特例:
将它用户volatile变量。只要确保只通过单一线程写入共享的volatile变量,那么在这些volatile变量上执行“读-写-改”操作就是安全的。在这种情况下,你就**将修改操作限制在单一的线程中**,阻止了竞争条件。
鉴于ad-hoc线程限制固有的易损性。(可以考虑用栈限制或ThreadLocal取代它)
2.3.2栈限制
栈限制是线程限制的一种特例。
只能通过本地变量才可以触及对象。本地变量本身就被限制在执行线程中,它们存在于执行线程栈,其它线程无法访问这个栈。
个人理解:就是在方法内部创建对象,并且不要将对象的引用发布出去。
2.3.3ThreadLocal
ThreadLocal:使维护线程限制的方式更加规范。
它可以将每个线程与持有数值的对象关联在一起。并且提供了get和set访问器,为每个使用它的线程维护一份单独的拷贝,所以get总是返回由**当前执行线程**通过set设置的最新值。
例子:
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
public Connection initialValue(){
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection(){
return connectionHolder.get();
}
//这样,每个线程都会拥有属于自己的Connection。(Connection本身并不是线程安全的)
与线程相关的值存储在线程对象自身中,线程终止后,这些值会被垃圾回收。
2.4不可变性
使用不可变对象就不会由线程安全问题了。
创建后状态不能被修改的对象叫做不可变对象(final)。不可变对象天生是线程安全的。
只有满足如下状态,一个对象才是不可变的:
它的状态不能再创建后再被修改。
所有域都是final类型
它被正确创建(创建期间没有发生this引用的逸出)
“对象是不可变的” 与 “到对象的引用是不可变的” 之间并不等同。
程序存储再不可变对象中的状态仍然可以通过替换一个带新状态的不可变对象的实例得到更新。
例子:
String会惰性地计算哈希值:当第一次调用hashcode时,String计算哈希值,并将它缓存在一个非final域中,之所以可以这样做,仅是因为这个域所表现的非默认的值,在每次计算后都得到相同的结果,因为该结果来自一个已经确定的不可变的状态。
2.4.1Final域
final域是不能修改的(尽管如果final域指向的对象是可变的,这个对象仍然可被修改)。
即使对象是可变的,将一些域声明为final类型仍然有助于简化对其状态的判断。
正如“将所有的域声明为私有的,除非它们需要更高的可见性”一样,“将所有的域声明为final型,除非它们是可变的”,也是一条良好的实践。
2.4.2使用volatile发布不可变对象
@Immutable
class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache(BigInteger i, BigInteger[] factors) {
this.lastNumber = i;
this.lastFactors = Arrays.copyOf(factors, factors.length);
}
public BigInteger[] getFactors(BigInteger i) {
if (lastFactors == null || !lastNumber.equals(i)) {
return null;
} else {
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
}
//当一个线程设置volatile类型的cache域引用到一个新的OneValueCache后,新数据会立即对其它线程可见。与cache域相关的操作不会相互干扰,因为OneValueCache是不可变的。
//不可变的容器对象持有与不变约束相关的多个状态变量,并利用volatile引用确保即时的可见性,这两个前提保证了即使VolatileCachedFactor没有显示地用到锁,但仍然是线程安全的。
2.5安全发布
安全地共享一个对象。
例如:
@ThreadSafe
public class VolatileCachedFactorizer implements Servlet{
//使用不可变容器对象的volatile类型引用
private volatile OneValueCache cache = new OneValueCache(null,null);
......
}
2.5.1不正确发布:当好对象变坏时
例子:
public class Holder{
private int n;
public Holder(int n){this.n = n;}
public void assertSanity(){
if(n!=n){
throw new AssertionError("");
}
}
}
//不安全发布
public Holder holder;
public void initialize(){
holder = new Holder(42);
}
//对象本身没有问题,是一个“好对象”
//但是除了发布线程,其它线程调用assertSanity时都可能抛出AssertionError。
//问题不在Holder类,而是因为没有正确发布,如果将域n声明为final,就可以避免不正确发布的问题。
2.5.2不可变对象与初始化安全性
Java存储模型为共享不可变对象提供了特殊的初始化安全性的保证。
不可变对象可以在没有额外同步的情况下,安全地用于任意线程,甚至发布它们时也不需要同步。
如果final域指向可变对象,那么访问这些对象的状态时仍然需要同步。
2.5.3安全发布的模式
为了安全的发布对象,对象的引用以及对象的状态必须同时对其它线程可见。
一个正确创建的对象可以通过下列条件安全地发布:
通过静态初始化器初始化对象的引用。
将它的引用存储到volatile域或AtomicReference;
将它的引用存储到正确创建的对象的final域中。
或者将它的引用存储到由锁正确保护的域中。
线程安全库中的容器提供了如下的线程安全保证
- 置入Hashtable、synchronizedMap、ConcurrentMap中的主键以及键值,会安全地发布到可以从Map获取到它们的任意线程中,无论是直接获得还是通过迭代器获得。
- 置入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList、synchronizedSet中的元素,会安全地发布到可以从容器中获得它的任意线程。
- 置入BlockingQueue或者ConcurrentLinkedQueue的元素,会安全地发布到可以从队列中获得它的任意线程中。
通常,以最简单和最安全的方式发布一个被静态创建的对象,就是使用静态初始化器(static)。
2.5.4高效不可变对象
一个对象在技术上是可变的,但是它的状态不会在发布后被修改,这样的对象称为**有效不可变对象**。
任何线程都可以在没有额外的同步下安全地使用一个安全发布的高效不可变对象。
例子:
//Date自身是可变的,但如果你把它当作不可变对象来使用就可以忽略锁。否则,每当Date被跨线程共享时,都要用锁确保安全。
public Map<String,Date> lastLogin = Collections.synchronizedMap(new HashMap<String,Date>());
//如果Date值在置入Map中后就不会改变,那么,synchronizedMap中同步的实现,对于安全发布Date值,是至关重要的。而访问这些Date值时,就不再需要额外的同步。
2.5.5可变对象
为了保证安全地共享可变对象,可变对象必须被安全发布,同时必须是线程安全的或者是被锁保护的。
发布对象的必要条件依赖于对象的可变性:
- 不可变对象可以通过任意机制发布;(final)
- 高效不可变对象必须要安全发布;(假装是final)
- 可变对象必须要安全发布,同时必须要线程安全或者被锁保护。
