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)

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

Java Concurrency in Practice——读书笔记的更多相关文章

  1. Java Concurrency in Practice 读书笔记 第十章

    粗略看完<Java Concurrency in Practice>这部书,确实是多线程/并发编程的一本好书.里面对各种并发的技术解释得比较透彻,虽然是面向Java的,但很多概念在其他语言 ...

  2. Java Concurrency in Practice 读书笔记 第二章

    第二章的思维导图(代码迟点补上):

  3. java concurrency in practice读书笔记---ThreadLocal原理

    ThreadLocal这个类很强大,用处十分广泛,可以解决多线程之间共享变量问题,那么ThreadLocal的原理是什么样呢?源代码最能说明问题! public class ThreadLocal&l ...

  4. 《Java编程思想》读书笔记(五)

    前言:本文是<Java编程思想>读书笔记系列的最后一章,本章的内容很多,需要细读慢慢去理解,文中的示例最好在自己电脑上多运行几次,相关示例完整代码放在码云上了,码云地址:https://g ...

  5. 《Java 8实战》读书笔记系列——第三部分:高效Java 8编程(四):使用新的日期时间API

    https://www.lilu.org.cn/https://www.lilu.org.cn/ 第十二章:新的日期时间API 在Java 8之前,我们常用的日期时间API是java.util.Dat ...

  6. 《Java编程思想》读书笔记(二)

    三年之前就买了<Java编程思想>这本书,但是到现在为止都还没有好好看过这本书,这次希望能够坚持通读完整本书并整理好自己的读书笔记,上一篇文章是记录的第一章到第十章的内容,这一次记录的是第 ...

  7. 《Java编程思想》读书笔记(四)

    前言:三年之前就买了<Java编程思想>这本书,但是到现在为止都还没有好好看过这本书,这次希望能够坚持通读完整本书并整理好自己的读书笔记,上一篇文章是记录的第十七章到第十八章的内容,这一次 ...

  8. 《Java编程思想》读书笔记

    前言 这个月一直没更新,就是一直在读这本<Java编程思想>,这本书可以在Java业界被传神的一本书,无论谁谈起这本书都说好,不管这个人是否真的读过这本书,都说啊,这本书很好.然后再看这边 ...

  9. 《神经网络算法与实现-基于Java语言》的读书笔记

    文章提纲 全书总评 读书笔记 C1.初识神经网络 C2.神经网络是如何学习的 C3.有监督学习(运用感知机) C4.无监督学习(自组织映射) Rreferences(参考文献) 全书总评 书本印刷质量 ...

随机推荐

  1. python3 练手实例7 斐波那契数列

    '''a,b=0,1 x=int(input('请指定需要多少项:')) while x>0: print(b) a,b=b,a+b x-=1''' #递归 def fibo(n): if n& ...

  2. Activiti工作流学习笔记

    先从工作流的启动开始讲,Activiti提供了四种工作流的启动方式 1.空启动事件 2.定时启动事件 3.异常启动事件 4.消息启动事件 空启动事件中标签内没有任何其他元素的定义 <startE ...

  3. Arduino常用的数据类型以及转换

    常用的数据类型有布尔类型.字符型.字节型.整型.无符号整型.长整型.无符号长整型.浮点型.双精度浮点型等 布尔类型bollean: 布尔值是一种逻辑值,其结果只能为真(true)或者假(false). ...

  4. 简单迷宫算法(递归与非递归C++实现)

    假定迷宫如下:1代表墙,0代表道路,起点在(1,1),终点(11,9)(PS:下标从0开始计算). 现在寻求一条路径能从起点到达终点(非最短). 有两种解法:递归与非递归. 递归算法思路: 要用递归, ...

  5. 第4章学习小结_串(BF&KMP算法)、数组(三元组)

    这一章学习之后,我想对串这个部分写一下我的总结体会. 串也有顺序和链式两种存储结构,但大多采用顺序存储结构比较方便.字符串定义可以用字符数组比如:char c[10];也可以用C++中定义一个字符串s ...

  6. [Kubernetes]资源模型与资源管理

    作为 Kubernetes 的资源管理与调度部分的基础,需要从它的资源模型说起. 资源管理模型的设计 我们知道,在 Kubernetes 里面, Pod 是最小的原子调度单位,这就意味着,所有和调度和 ...

  7. sql 常见错误总结

    1.根据一张表更新另一张表的数据. . 写法轻松,更新效率高: update table1 set field1=table2.field1, field2=table2.field2 from ta ...

  8. 【原创】大数据基础之Kudu(3)primary key

    关于kudu的primary key The primary key may not be changed after the table is created. You must drop and ...

  9. margin合并和浮动

    1.父子元素margin塌陷问题子元素设置margin-top作用于父元素时, 会产生margin合并问题. 解决方法是: 给父元素的::before伪元素设置为display:table属性, 其中 ...

  10. computed计算属性

    在computed中,可以定义一些属性,这些属性 叫做计算属性.计算属性的本质是一个方法,只不过我们在使用的时候,把他们的名称当做属性来使用,并不会吧计算属性当做方法去调用.与methods平级. / ...