Java Concurrency in Practice——读书笔记

Thread Safety线程安全

    线程安全编码的核心,就是管理对状态(state)的访问,尤为是对(共享shared、可变mutable)状态的访问。java

  • shared:指能够被多个线程访问的变量
  • mutable:指在其生命周期内,它的值可被改变

    一般,一个对象Object的状态state就是他的数据data,存储于状态变量(state variables)如实例对象或者静态变量,以及他所依赖的其余对象。安全

    Java中最经常使用的同步机制是使用Synchronized关键字,其余还有volatile变量, explicit locks(显式锁), 和atomic variables(原子变量)。网络

概念

  1. state:状态,怎么理解好呢,就是(在某一给定时刻,它所存储的信息,这里理解为数据data)
  2. 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 }

如何作到线程安全?

  1. 不在线程间共享状态变量(state variable)—无状态的对象老是线程安全的。
  2. 在线程间共享不可变的状态变量(immutable state variable)
  3. 在访问状态变量时,使用同步机制

什么是线程安全?

线程安全的核心概念是:正确性。一个类是否正确,取决于它是否遵照他的规范(specification),一个好的规范,定义了以下两点内容:多线程

  1. invariants不变性,或者叫约束条件,约束了他的状态state
  2. postconditions后置条件,描述了操做后的影响

atomic原子性

一个无状态的Servlet必然是线程安全的,以下:less

  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 }

加入一个状态后,就再也不线程安全了。jvm

  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)ide

image

Race condition竞态条件

多线程中,有可能出现因为不恰当的执行时序而形成不正确结果的状况,称为竞态条件。函数

竞态条件一:read-modify-write(先读取再修改写入)post

       最后的结果依赖于它以前的状态值,如上++操做性能

竞态条件二: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?

  1. 只是在复合操做(compound action)的整个执行过程当中(entire duration)持有一把锁来维持state的原子性操做,是远远不够的;而是应该在全部这个状态可被获取的地方(everywhere that variable is accessed)都用同一把锁来协调对状态的获取(包括读、写)——可见性
  2. 全部(包含变量多于一个)的不定性,它所涉及的全部变量必须被同一把锁保护。(For every invariant that involves more than one variable, all the variables
    involved in that invariant must be guarded by the same lock.)

                    活跃性与性能

                    1. 避免在较长时间的操做中持有锁,例如网络IO,控制台IO等。
                    2. 在实现同步操做时,避免为了性能而复杂化,可能会带来安全性问题。

                    可见性

                    可见性比较难发现问题,是由于老是与咱们的直觉相违背。

                    重排序(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 }
                    运行上述代码会报空指针错误,是由于在name 初始化以前,就使用了ThisEscape实例(this指针逸出),而此时实例还没有完成初始化。

                    修改以下,避免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 
                    相关文章
                    相关标签/搜索