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. AngularJs实现表单验证

    首先,我们应该知道,表单中,常用的验证操作有: $dirty 表单有填写记录 $valid 字段内容合法的 $invalid 字段内容是非法的 $pristine 表单没有填写记录 $error 表单 ...

  2. day 22 - 2 面向对象练习

    练习一 在终端输出如下信息 小明,10岁,男,上山去砍柴小明,10岁,男,开车去东北小明,10岁,男,最爱大保健老李,90岁,男,上山去砍柴老李,90岁,男,开车去东北老李,90岁,男,最爱大保健老张 ...

  3. kali下的webshell工具-Weevely

    Weevely ------------------------------------------------ 主要特点: ·         隐蔽的类终端的PHP webshell ·       ...

  4. LeetCode第十五题-找出数组中三数和为0的答案

    3Sum 问题简介: 给定n个整数的数组nums,是否有元素a,b,c在nums中,使a + b + c = 0? 找到数组中所有唯一的三元组,它们的总和为零 注:解决方案集不得包含重复的三元组 例如 ...

  5. RNN回归

    import torch from torch import nn import numpy as np import matplotlib.pyplot as plt # torch.manual_ ...

  6. anacoda报错No module named 'sklearn.cross_validation'

    在目前的snacoda里集成的sklearn已经不存在cross_validation模块了 使用以下模块 from  sklearn.model_selection  import train_te ...

  7. git自动部署到服务器

    1.现在服务器配置空仓库 mkdir -p test/project.git chmod 777 test cd test/project.git/ git init --bare . cd .. c ...

  8. ESP8266莫名重启或者死机问题

    多半是内存使用不当 1. 如果你要用很大长度的数组,那么可以换用更小的数据类型.比如,int值要占用两个字节,你可以用byte(只占用一个字节)代替:    2. esp8266有时会莫明重启,大部分 ...

  9. Mac 小功能

    Safari  安装扩展    https://safari-extensions.apple.com/?category=translation 关闭第三方验证  有时候打开自己下载的安装包会提示 ...

  10. WOW.js – 让页面滚动更有趣

    官网:http://mynameismatthieu.com/WOW/ 建议去官网一看 下载地址:https://github.com/matthieua/WOW 浏览器兼容 IE10+  Chrom ...