Java Concurrency in Practice——读书笔记

时间:2022-12-17 19:51:00

Thread Safety线程安全

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

  • 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必然是线程安全的,如下:

  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)

Java Concurrency in Practice——读书笔记

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?

  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