:::info 💡 根据 遗忘曲线:如果没有记录和回顾,6天后便会忘记75%的内容
读书笔记正是帮助你记录和回顾的工具,不必拘泥于形式,其核心是:记录、翻看、思考 :::

书名 Java 并发编程实战
作者 Doug Lea
状态 阅读中
简介 要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。

思维导图

用思维导图,结构化记录本书的核心观点。

第二章线程安全性 - 图1

第二章 线程安全性

书摘

  • 要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享(Shared)和可变的(Mutable)状态的访问。
  • 从非正式的意义上来说,对象的状态是指存储在状态变量(例如实例或静态域)中的数据。
  • “共享”意味着变量可以由多个线程同时访问,而“可变”则意味着变量的值在其生命周期内可以发生变化。
  • 要使得对象是线程安全的,需要采用 同步机制 来协同对对象可变状态的访问。如果无法实现协同,那么可能会导致数据破坏以及其他不该出现的结果。
  • 如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:

    • 不在线程之间共享该状态变量。
    • 将状态变量修改为不可变的变量。
    • 在访问状态变量时使用同步机制。

      2.1 什么是线程安全性

      我们如何区分线程安全的类以及非线程安全的类?进一步说,“安全”的含义是什么?
      在线程安全性的定义中,最核心的概念就是正确性。如果对线程安全性的定义是模糊的,那么就是因为缺乏对正确性的清晰定义。
      正确性的含义是,某个类的行为与其规范安全完全一致。在良好的规范中通常会定义各种不变性条件(Invariant)来约束对象的状态,以及定义各种后验条件(Postcondition)来描述对象操作的结果。

      2.2 原子性

      你可能会认为,在基于Web的服务中,命中计数器值的少量偏差或许是可以接受的,在某些情况下也确实如此。但如果该计数器被用来生成数值序列或者唯一的对象标识符,那么在多次调用中返回相同的值将导致严重的数据完整性问题。在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,他有一个正式的名字:竞态条件(Race Condition,注:区别于数据竞争)。
      方法:在实际情况中,应尽可能地使用现有的线程安全对象(例如AcomicLong)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性。

      2.3 加锁机制

      当在Servlet中添加一个状态变量时,可以通过线程安全的对象来管理Servlet的状态以维护Servlet的线程安全性。但如果想在Servlet中添加更多的状态,那么是否只需添加更多的线程安全状态变量就够了?
      答:否。尽管原子变量本身是安全的,但在示例代码中存在着竞态条件,这可能产生错误的结果。
      在线程安全性的定义中要求,多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。 当不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此,当更新一个变量时,需要在同一个原子操作中对其他变量同时进行更新。
      要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
    1. 内置锁:synchronized
    2. 重入:在第二章讲线程安全性中关于锁重入有这样一个示例(2.3.2):

      子类改写了父类的synchronized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将产生死锁。由于Widget和LoggingWidget中doSomething方法都是synchronized方法,因此每个doSomething方法在执行前都会获取Widget上的锁。因为这个锁已经被持有,从而线程将永远停顿下去,等待一个永远无法获取的锁。重入则避免了这种情况的发生。

  1. public class Widget {
  2. public synchronized void doSomthing() {
  3. ...
  4. }
  5. }
  6. public class LoggingWidget extends Widget {
  7. public synchronized void doSomthing {
  8. System.out.println(toString + ": calling doSomthing");
  9. super.doSomthing();
  10. }
  11. }

这里作者的表述或者翻译不够严禁,正确的表述如下:

子类改写了父类的synchronized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将产生死锁。由于Widget和LoggingWidget中doSomething方法都是synchronized方法,因此每个doSomething方法在执行前都会获得调用该方法当前实例上的锁。

  • 当线程执行LoggingWidget实例中的doSomething时获得LoggingWidget实例的锁。
  • LoggingWidget实例doSomething方法中调用super.doSomething(),调用者依然是LoggingWidget实例,再次获得的锁依然是LoggingWidget实例的锁。
  • 线程再次获得LoggingWidget实例的锁,即锁的重入。

    理解这个问题的重点: 锁的持有者是线程,锁是加在当前实例。

    2.4 用锁来保护状态

    • 对于可能被多个线程同时访问的可变状态变量,在访问他的时候都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁来保护的。

    • 每个共享和可变的变量都应该由一个锁来保护,从而使维护人员知道是哪一个锁。

    • 对于每个包含多个变量的不变性条件,其中涉及的所所有变量都需要由一个锁来保护。

2.5 活跃性与性能

  • 通常,在简单性与性能之间存在相互制约因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能破坏安全性)。

  • 当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或者控制台I/O),一定不要持有锁。

第三章 对象的共享

第四章 对象的组合

第五章 基础构建模块

读后感

观点1

读完该书后,受益的核心观点与说明…

观点2

读完该书后,受益的核心观点与说明…

观点3

读完该书后,受益的核心观点与说明…

书摘

  • 该书的金句摘录…
  • 该书的金句摘录…
  • 该书的金句摘录…

    相关资料

    可通过“⌘+K”插入引用链接链接,或使用“本地文件”引入源文件。