首页 > 代码库 > 读书笔记-----Java并发编程实战(二)对象的共享
读书笔记-----Java并发编程实战(二)对象的共享
1 public class NoVisibility{ 2 private static boolean ready; 3 private static int number; 4 private static class ReaderThread extends Thread{ 5 public void run(){ 6 while(!ready) 7 Thread.yield(); 8 System.out.println(number); 9 }10 }11 12 public static void main(String[] args){13 new ReaderThread().start();14 number = 42;15 ready = true;16 }17 }
在两个线程之间共享访问变量是不安全的,执行结果顺序没法预料。
我们常用的get和set方法有时是非线程安全的
1 @NotThreadSafe2 public class MutableInteger{3 private int value;4 public int get(){return value;}5 public void set(int value){this.value =http://www.mamicode.com/ value;} 6 }
如果某个线程调用了set,那么另一个正在调用get的线程可能会看到更新后的value值,也可能看不到
将其变成线程安全的类
@ThreadSafe public class SynchronizedInteger{ @GuardBy("this") private int value; public synchronized int get(){return value;} public synchronized void set(int value){this.value =http://www.mamicode.com/ value;}}
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
Volatile变量
用来确保将变量的更新操作通知到其他线程。volatile变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。
Volatile变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile变量的最新值。Volatile变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。
但如果在代码中依赖Volatile变量来控制状态的可见性,通常比使用锁的代码更加脆弱,也更难以理解。
仅当volatile变量能简化代码实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,就不要使用volatile变量。volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标示一些重要的程序生命周期事件的发生(例如,初始化或关闭)
典型用法:检查某个状态标记以判断是否退出循环。
volatile boolean asleep;... while(!asleep) countSomeSheep();
volatile不足以确保递增操作的原子性。
加锁机制既可以确保可见性又可以确保原子性。而volatile变量只能确保可见性。
当且仅当满足以下条件时,才应该使用volatile变量
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
- 该变量不会与其他状态的变量一起纳入不变性条件中
- 访问变量时不需要加锁
1 class UnSafeStates{ 2 private String[] states={"AK","AL"}; 3 public String[] getStates(){ return states; } 4 public static void main(String[] args) { 5 UnSafeStates safe = new UnSafeStates(); 6 System.out.println(Arrays.toString(safe.getStates())); 7 safe.getStates()[1] = "c"; 8 System.out.println(Arrays.toString(safe.getStates())); 9 } 10 }
public class Escape{ private int thisCanBeEscape = 0; public Escape(){ new InnerClass(); } private class InnerClass { public InnerClass() { //这里可以在Escape对象完成构造前提前引用到Escape的private变量 System.out.println(Escape.this.thisCanBeEscape); } } public static void main(String[] args) { new Escape(); } }
1 public class SafeListener{ 2 private final EventListener listener; 3 4 private SafeListener(){ 5 listener = new EventListener(){ 6 public void onEvent(Event e) { 7 doSomething(e); 8 } 9 }10 }11 12 public static SafeListener newInstance(EventSource source){13 SafeListener safe = new SafeListener();14 source.registerListener(safe.listener);15 return safe;16 }17 }
1 private static ThreadLocal<Connection> connectionHolder2 = new ThreadLocal<Connection>(){3 public Connection initialValue(){4 return DriverManager.getConnection(DB_URL);5 }6 };7 public static Connection getConnection(){8 return connectionHolder.get();9 }
将JDBC的连接保存到ThreadLocal对象中,调用get方法使得每个线程都拥有属于自己的连接副本。
可以将ThreadLocal<T>看做Map<Thread,T>对象。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾回收。
@Immutablepublic final class ThreeStooges{ private final Set<String> stooges = new HashSet<String>(); public ThreeStooges(){ stooges.add("Moe"); stooges.add("Larry"); stooges.add("Ted"); } public boolean isStooge(String name){ return stooges.contains(name); }}
不可变性并不等于将对象所有的域都声明为final类型,即使对象中所有的域都是final类型,这个对象仍然也可以是可变的,因为在final域中可以保持对可变对象的引用。
对象创建出来后(通过构造方法)无论如何对此对象进行非暴力操作(不用反射),此对象的状态(成员变量的值)都不会发生变化,那么此对象就是不可变的,相应类就是不可变类,跟是否用 final 修饰没关系,final 修饰类是防止此类被继承。
final域
final域不能修改的(尽管如果final域指向的对象是不可变的,这个对象仍然可被修改),然而它在Java内存模式中还有着特殊语义。final域使得确保被始化安全性成为可能,初始化安全性让不可变性对象不需要同步就能自由地被访问和共享。
正如“将所有的域声明为私有的,除非它们需要更高的可见性”一样“将所有的域声明为final型,除非它们是可变的”,也是一条良好的实践。
示例:使用volatile发布不可变的对象
下面是一个不可变对象,进(构造时传进的参数)出(使用时)都对状态进行了拷贝。因为BigInteger是不可变的,所以直接使用了Arrays.copyOf来进行拷贝了,如果状态所指引的对象不是不可变对象时,就不能使用这项技术了,因为外界可以对这些状态所指引的对象进行修改,如果这样只能使用new或深度克隆技术来进行拷贝了。
@Immutableclass 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); }}
下面开始发布上面不可变对象,其中volatile起关键作用,如果没有它,即使OneValueCache是不可变类,其最新的状态也无法被其他线程可见。
@ThreadSafepublic 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为volatile,所以最新值立即能让其它线程可见 cache = new OneValueCache(i, factors); } encodeIntoResponse(resp, factors); }}
不可变对象与初始化安全
Java内存模型为共享不可变对象提供了特殊的初始化安全性的保证,即对象在完全初始化之后才能被外界引用。
即使发布对象引用进没有使用同步,不可变对象仍然可以被安全地访问。为了获得这种初始化安全性的保证上,应该满足所有不可变性的条件:不可修改的状态、所有域都是final类型的以及正确的构造。
不可变对象呆以在没有额外同步的情况下,安全地用于任意线程;甚至发布它们时亦不需要同步。
可变对象与安全发布
如果一个对象不是不可变的,它就必须要被安全的发布,通常发布线程与消费线程都必须同步化。我们要确保消费线程能够看到处于发布当时的对象状态。
为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确创建的对象可以通过下列条件安全地发布:
1、 通过静态初始化器初始化对象引用;
2、 将它的引用存储到volatile域或AtomicReference;
3、 将它的引用存储到正确创建的对象的Final域中;
4、 或者将它的引用存储到由锁正确保护的域中。
线程安全中的容器提供了线程安全保证,正是遵守了上述最后一条要求。
通常,以最简单和最安全的方式发布一个被静态创建的对象,就是使用静态初始化器:
public static Holder holder = new Holder(42);
静态初始化器由JVM在类的初始阶段执行,由于JVM内在的同步,该机制确保了以这种方式初始化的对象可以被安全地发布。
对象可变性与安全发布
发布对象的必要条件依赖于对象的可变性:
1、 不可变对象可以通过任意机制发布;
2、 高效不可变对象(指对象本身是可变的,但只要发布后状态不再做修改)必须要安全发布;
3、 可变对象必须要安全发布,同时必须要线程安全或者是被锁保护;
读书笔记-----Java并发编程实战(二)对象的共享