Java Concurrency in Practice——读书笔记
Thread Safety线程安全
线程安全编码的核心,就是管理对状态(state)的访问,尤其是对(共享shared、可变mutable)状态的访问。
- shared:指可以被多个线程访问的变量
- mutable:指在其生命周期内,它的值可被改变
通常,一个对象Object的状态state就是他的数据data,存储于状态变量(state variables)如实例对象或者静态变量,以及他所依赖的其他对象。
Java中最常用的同步机制是使用Synchronized关键字,其他还有volatile变量, explicit locks(显式锁), 和atomic variables(原子变量)。
概念
- state:状态,怎么理解好呢,就是(在某一给定时刻,它所存储的信息,这里理解为数据data)
- invariant:不变性,就是用来限制state的constrains the state stored in the object.例如:
1 public class Date { 2 int /*@spec_public@*/ day; 3 int /*@spec_public@*/ hour; 4 5 /*@invariant 1 <= day && day <= 31; @*/ //class invariant 6 /*@invariant 0 <= hour && hour < 24; @*/ //class invariant 7 8 /*@ 9 @requires 1 <= d && d <= 31; 10 @requires 0 <= h && h < 24; 11 @*/ 12 public Date(int d, int h) { // constructor 13 day = d; 14 hour = h; 15 } 16 17 /*@ 18 @requires 1 <= d && d <= 31; 19 @ensures day == d; 20 @*/ 21 public void setDay(int d) { 22 day = d; 23 } 24 25 /*@ 26 @requires 0 <= h && h < 24; 27 @ensures hour == h; 28 @*/ 29 public void setHour(int h) { 30 hour = h; 31 } 32 }
如何做到线程安全?
- 不在线程间共享状态变量(state variable)—无状态的对象总是线程安全的。
- 在线程间共享不可变的状态变量(immutable state variable)
- 在访问状态变量时,使用同步机制
什么是线程安全?
线程安全的核心概念是:正确性。一个类是否正确,取决于它是否遵守他的规范(specification),一个好的规范,定义了如下两点内容:
- invariants不变性,或者叫约束条件,约束了他的状态state
- postconditions后置条件,描述了操作后的影响
atomic原子性
一个无状态的Servlet必然是线程安全的,如下:
1 @ThreadSafe 2 public class StatelessFactorizer implements Servlet { 3 public void service(ServletRequest req, ServletResponse resp) { 4 BigInteger i = extractFromRequest(req); 5 BigInteger[] factors = factor(i); 6 encodeIntoResponse(resp, factors); 7 } 8 }
加入一个状态后,就不再线程安全了。
1 @NotThreadSafe 2 public class UnsafeCountingFactorizer implements Servlet { 3 private long count = 0; 4 5 public long getCount() { 6 return count; 7 } 8 9 public void service(ServletRequest req, ServletResponse resp) { 10 BigInteger i = extractFromRequest(req); 11 BigInteger[] factors = factor(i); 12 ++count;// 非原子操作 13 encodeIntoResponse(resp, factors); 14 } 15 }
++ 操作符并非原子操作,它包含三步:读值,加一,写入(read-modify-write)
Race condition竞态条件
多线程中,有可能出现由于不恰当的执行时序而造成不正确结果的情况,称为竞态条件。
竞态条件一:read-modify-write(先读取再修改写入)
最后的结果依赖于它之前的状态值,如上++操作
竞态条件二:check-then-act(先检查后执行)
示例:lazy initialization
1 @NotThreadSafe 2 public class LazyInitRace { 3 private ExpensiveObject instance = null; 4 5 public ExpensiveObject getInstance() { 6 if (instance == null)// check then act 7 instance = new ExpensiveObject(); 8 return instance; 9 } 10 }
Compound actions复合操作
避免竞态条件的问题,就需要以“原子”方式执行上述操作,称之为“复合操作”。
解决read-modify-write这一类竞态条件问题时,通常使用已有的线程安全对象来管理类的状态,如下:
1 @ThreadSafe 2 public class CountingFactorizer implements Servlet { 3 private final AtomicLong count = new AtomicLong(0); 4 //使用线程安全类AtomicLong来管理count这个状态 5 6 public long getCount() { 7 return count.get(); 8 } 9 10 public void service(ServletRequest req, ServletResponse resp) { 11 BigInteger i = extractFromRequest(req); 12 BigInteger[] factors = factor(i); 13 count.incrementAndGet(); 14 encodeIntoResponse(resp, factors); 15 } 16 }
但这种方式无法满足check-then-act这一类竞态条件问题,如下:
1 @NotThreadSafe 2 public class UnsafeCachingFactorizer implements Servlet { 3 private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>(); 4 private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>(); 5 6 public void service(ServletRequest req, ServletResponse resp) { 7 BigInteger i = extractFromRequest(req); 8 if (i.equals(lastNumber.get())) 9 encodeIntoResponse(resp, lastFactors.get()); 10 else { 11 BigInteger[] factors = factor(i); 12 lastNumber.set(i); 13 lastFactors.set(factors); 14 encodeIntoResponse(resp, factors); 15 } 16 } 17 }
锁Locking可以更完美的解决复合操作的原子性问题。当然锁也可以解决变量的可见性问题。
Intrinsic locks内置锁
也称为monitor locks监视器锁,每一个Java对象都可以被当成一个锁,自动完成锁的获取和释放,使用方式如下:
1 synchronized (lock) { 2 // Access or modify shared state guarded by lock 3 }
同时,内置锁也是可重入的(Reentrancy),每个锁含有两个状态,一是获取计数器(acquisition count),一个是所有者线程(owning thread),当count=0,锁是可获取状态,当一个thread t1 获取了一个count=0的锁时,jvm设置这个锁的count=1,owning thread=t1,当t1再次要获取这个锁时,是被允许的(即可重入),此时count++,当t1退出该同步代码块时,count--,直到count=0后,即锁被t1彻底释放。
如何使用lock来保护state?
- 只是在复合操作(compound action)的整个执行过程中(entire duration)持有一把锁来维持state的原子性操作,是远远不够的;而是应该在所有这个状态可被获取的地方(everywhere that variable is accessed)都用同一把锁来协调对状态的获取(包括读、写)——可见性
- 所有(包含变量多于一个)的不定性,它所涉及的所有变量必须被同一把锁保护。(For every invariant that involves more than one variable, all the variables
involved in that invariant must be guarded by the same lock.)
活跃性与性能
- 避免在较长时间的操作中持有锁,例如网络IO,控制台IO等。
- 在实现同步操作时,避免为了性能而复杂化,可能会带来安全性问题。
可见性
可见性比较难发现问题,是因为总是与我们的直觉相违背。
重排序(reordering)的存在,易造成失效数据(Stale data),但这些数据多数都是之前某一个线程留下来的数据,而非随机值,我们称这种情况为最低安全性(out-of-thin-air safety);但非原子的64位操作(如long,double),涉及到高位和低位分解为2个32位操作的情况,而无法满足最低安全性,线程读到的数据,可能是线程A留下的高位和线程B留下的低位组合。除非用volatile关键字或锁保护起来。
volatile关键字修饰的变量会避免与其他内存操作重排序。慎用!
发布Publishing与逸出escaped
发布:使对象能够在当前作用域外被使用。
逸出:不应该发布的对象被发布时。
隐式this指针逸出问题:
1 public class ThisEscape { 2 private String name = null; 3 4 public ThisEscape(EventSource source) { 5 source.registerListener(new EventListener() { 6 public void onEvent(Event event) { 7 doSomething(event); 8 } 9 }); 10 name = "TEST"; 11 } 12 13 protected void doSomething(Event event) { 14 System.out.println(name.toString()); 15 } 16 } 17 // Interface 18 import java.awt.Event; 19 20 public interface EventListener { 21 public void onEvent(Event event); 22 } 23 // class 24 public class EventSource { 25 public void registerListener(EventListener listener) { 26 listener.onEvent(null); 27 } 28 } 29 // Main 30 public class Client { 31 public static void main(String[] args) throws InterruptedException { 32 EventSource es = new EventSource(); 33 new ThisEscape(es); 34 } 35 }
修改如下,避免This逸出:
1 public class SafePublish { 2 3 private final EventListener listener; 4 private String name = null; 5 6 private SafePublish() { 7 listener = new EventListener() { 8 public void onEvent(Event event) { 9 doSomething(); 10 } 11 }; 12 name = "TEST"; 13 } 14 15 public static SafePublish newInstance(EventSource eventSource) { 16 SafePublish safePublish = new SafePublish (); 17 eventSource.registerListener(safeListener.listener); 18 return safePublish; 19 } 20 21 protected void doSomething() { 22 System.out.println(name.toString()); 23 } 24 }
造成this指针逸出的情况:
- 在构造函数中启动了一个线程或注册事件监听;—私有构造器和共有工厂方法
- 在构造函数中调用一个可以被override的方法(非private或final方法)
Thread confinement线程封闭
如Swing 和 JDBC的实现,使用局部变量(local variables )和 ThreadLocal 类
ad-hoc线程封闭:不太懂,就是开发者自己去维护封闭性?
Stack confinement栈封闭
不可变immutable
并不是被final修饰的就是绝对的不可变!!
使用Volatile来发布不可变对象
1 @Immutable 2 class OneValueCache { 3 private final BigInteger lastNumber; 4 private final BigInteger[] lastFactors; 5 6 public OneValueCache(BigInteger i, BigInteger[] factors) { 7 lastNumber = i; 8 lastFactors = Arrays.copyOf(factors, factors.length); 9 } 10 11 public BigInteger[] getFactors(BigInteger i) { 12 if (lastNumber == null || !lastNumber.equals(i)) 13 return null; 14 else 15 return Arrays.copyOf(lastFactors, lastFactors.length); 16 } 17 } 18 19 // @ThreadSafe 20 public class VolatileCachedFactorizer implements Servlet { 21 private volatile OneValueCache cache = new OneValueCache(null, null); 22 23 public void service(ServletRequest req, ServletResponse resp) { 24 BigInteger i = extractFromRequest(req); 25 BigInteger[] factors = cache.getFactors(i); 26 if (factors == null) { 27 factors = factor(i); 28 cache = new OneValueCache(i, factors); 29 } 30 encodeIntoResponse(resp, factors); 31 } 32 } 33