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. springBoot和Mybatis输出sql日志

    利用slf4j来输出日志. 首先需要版本兼容的slf4j-log4j12.log4j.slf4j-api.slf4j-nop.slf4j-simple这5个包. 可以去maven知识库中找到这5个包的 ...

  2. java基于redis事务的秒杀实现

    package com.vian.user.service; import org.junit.Test; import org.springframework.util.CollectionUtil ...

  3. JavaScript中的alert()与console.log()的区别

    1.alert() [1.1]有阻塞作用,不点击确定,后续代码无法继续执行 [1.2]alert()只能输出string,如果alert输出的是对象会自动调用toString()方法 e.g. ale ...

  4. SpringMVC-简单总结

    要学习一项技术,首先要知道, 它是什么, 为什么要用它 , 它由哪些东西组成, 每个东西是干什么的, 它们怎么综合在一起的 参考博客: 平凡希: https://www.cnblogs.com/xia ...

  5. 【转】子类会调用父类的@PostConstruct方法

    如果一个类用@Service或@Component,那么只需要用@PostConstruct修饰某个方法,该方法能在类实例化的过程中自动执行,相当于类的构造函数.同时,具备了构造函数不具备的功能. @ ...

  6. 前端笔记知识点整合之JavaScript(四)关于函数、作用域、闭包那点事

    一.自定义函数function 函数就是功能.方法的封装.函数能够帮我们封装一段程序代码,这一段代码会具备某一项功能,函数在执行时,封装的这一段代码都会执行一次,实现某种功能.而且,函数可以多次调用. ...

  7. 关于VC预定义常量_WIN32,WIN32,_WIN64等预定义宏的介绍(整理、转载)

    参考帖子: (1)MSDN上专门讲预定义宏:https://msdn.microsoft.com/en-us/library/b0084kay(v=vs.80).aspx (2)VS中属性页的配置介绍 ...

  8. Mac本地搭建kubernetes环境

    前言:之前在windows上面的虚拟机上面手工搭建了kubernetes集群,但是环境被破坏了,最近想要继续学习k8s,手工搭建太费事,所以选择了minikube,完全能够满足个人的需求,其实在Win ...

  9. windows环境安装phantomjs和pyspider遇到的问题

    1. 安装phantomjs 下载地址:http://phantomjs.org/download.html 解压后将phantomjs.exe文件放到python根目录 2.安装pyspider p ...

  10. 打造vim IDE

    pathogen.vim:vim插件目录自动识别.加载(注意:能用pathogen.vim安装插件,就不要用Vundle.因为Vundle下载插件速度非常慢.) https://github.com/ ...