AI智能
改变未来

Java并发编程实战——读后感

未完待续。

阅读帮助

本文运用《如何阅读一本书》的学习方法进行学习。

P15 表示对于书的第15页。

Java并发编程实战简称为并发书或者该书之类的。

熟能生巧,不断地去理解,就像欣赏一部喜欢的电影,时不时就再看一遍,甚至把剧本下下来通读。

思想

1、虽然现在都是分布式系统,日新月异,但是代码层面的并发思想是可以学习借鉴的,在不同的层面上使用并发的设计理念。

2、本地上的并发安全加锁运行起来是比分布式redis锁快的,比较redis锁的交互需要通信,本地是代码层级的执行,还有就是用redis锁主要是我们现在是分布式系统,该锁的作用可能作用的范围是这几台服务器,用于比如重试、扣库存之类的,那么加锁的层级就必须是像redis这样公用的锁平台才行。

3、代码的执行效率问题,如果能在代码层级范围来操作并发执行安全的代码操作,即并发写得好,这样就能提高单台服务器的服务能力,比如1000tps和200tps的区别,还有就是分布式服务的线性扩展能力,不过这方面应该是分布式的架构问题,但回到第一个思想上来,我们可以学习其思想。

检视阅读

1 有系统的略读(目录,索引,关键字段等,已完成)

2 粗浅的阅读(不求甚解的全本阅读,已完成)

有个不合格的地方是阅读效率不高,耗费时间太长,不能在有效的时间内看完,主动性阅读只能算及格,还不是很投入式,阅读的主次不分,不能对需要花费心思理解的部分多花时间加深理解,而对于可以略读的部分加快阅读速度(当然,对于这种实用技术书一般都是重点)。

前言部分提供了这本书的各个章节的大体介绍,每个章节分属的部分。

自我要求的4个基本问题

1、这本书在谈些什么

什么是并发,为什么要用到并发,以及如何使用并发,特别是编写性能更好的并发代码。还有一部分并发代码如何测试的内容以及了解jvm的内存模型。

2、作者的主要想法、声明、和论点。

作者想通过这本书教会学者如何更好更准确的编写并发代码,关于并发代码的核心思想其实有三个方式解决问题:

一是不在线程间共享状态变量,即不用。

1、线程封闭

2、栈封闭(局部变量)

3、ThreadLocal(线程本地变量)类存储。

  最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理 。

二是将状态变量修改为不可变的变量,即保证不变性。

满足以下条件构造不变性对象:

1、对象创建以后其状态(对象的成员变量、或者说域)就不能修改。(赋值的时候注意对象赋值是赋值地址(引用传递),所以要通过copy对象内容创建新对象赋值)。

2、对象的域都是final类型。

3、对象是正确构建的(在对象的创建期间,this引用没有逸出)。

当多个线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生竞态条件多个线程同时读同一个资源不会产生竞态条件

注意:

A(译者注:注意,“不变”(Immutable)和“只读”(Read Only)是不同的。当一个变量是“只读”时,变量的值不能直接改变,但是可以在其它变量发生改变的时候发生改变。比如,一个人的出生年月日是“不变”属性,而一个人的年龄便是“只读”属性,但不是“不变”属性。随着时间的变化,一个人的年龄会随之发生变化,而一个人的出生年月日则不会变化。这就是“不变”和“只读”的区别。(摘自《Java与模式》第34章))

B 只要不是带有属性值的引用类型对象都不会因为原本值改变而导致对象的final域也改变.

三是如果一定要访问到可变的共享状态变量时使用同步,即加锁同步代码。

juc并发包的类和加锁同步。

四是安全发布对象

要安全地发布一个对象,对象的引用以及对象的状态(成员属性)必须同时对其他线程可见。可通过4种方式安全发布:

  1. 在静态初始化函数中初始化一个对象引用。(或者静态初始化块)

    //静态的初始化器,由于在JVM内部存在着同步机制,保证其安全发布。public staitc Holder holder = new Holder(42);
  2. 将对象的引用保存在volatile类型的域或者AtomicReferance对象中(可见性)。

  3. 将对象的引用保存到某个正确构造对象的final类型域中(不变性)。

  4. 将对象的引用保存到一个由锁保护的域中(直接加锁同步)。

3、这本书是否有道理?有合理性?

有道理且合理,不过有些还没弄明白,还有就是目前高并发的技术不止是在代码层面的编写,还有分布式应用等。

4、这本书跟我的关系和意义?

提高我的并发代码的理解领悟和编写能力,理解并发处理的思想,通过并发可以更高效地发挥代码的效率。

分析阅读

核心:提出问题,找出答案

一、在谈些什么

1、根据书的种类和主题分类

实用性书,工具书类型。

2、简短概况本书内容

合理的配置线程和编写并发代码可以更高效率地提高多核系统的效率,提高资源利用率和系统吞吐率,而编写并发代码的原则是能不在线程间共享状态变量尽量不共享;如果需要共享则最好是不变的状态变量;上面这两种都可以有效避免线程间的问题,实在不能避免,则在编写并发代码时用上jdk api里提供的juc包里各种线程安全类,并注意并发线程的活跃性、性能、和测试问题。

//待补充

3、按顺序和关联性列举全书大纲(简介描述)和各个部分大纲

先大后小,先粗后细,搭出骨架后再充填细的部分,分起码两层分部结构,一,一之一,一之二等

一、并发理论基础及规则(2-3章)
  1. 避免并发危险

  2. 构造线程安全的类

    加锁机制:

    1、内置锁

    同步代码块的锁就是方法调用所在的对象,该类的实例(所以不同实例可以有多个锁);静态同步方法的锁是以类Class作为锁,所以只有一把。

    2、重入

    锁是可重入的,获取锁的操作的粒度是线程而不是调用(即不是每次调用就获取一此锁,如果该线程已经持有了,则他可以重入该方法,如果不可重入,则将因为获取不到锁而永远停顿下去(已经持有了))。

    注意:能不能进入某个同步方法取决于是否取到该方法的的锁,如果是实例锁对象,则不同实例的方法调用操作分别获取不同的锁,并不会因为是同一个Class类而阻塞,只有静态类才有且只有一个锁对象。

    3、尽量缩小同步代码块的作用范围。

    4、不可能三角:安全性、简单性和性能是不能同时达到的,而安全性是必须的,所以简单些和性能之间存在着相互制约,一定不要盲目地为了性能而牺牲简单性(可能破坏安全性)。

    5、持有锁时间过长则会带来活跃性或性能问题。因此,当执行时间较长的计算或者无法快速完成的操作(I/O),一定不要持有锁。

    6、自己构建线程安全类和juc类库来构建并发应用程序。

  3. 验证线程安全

  4. 如何共享和发布对象?

  5. 发布和逸出 。当把一个对象传递给某个外部方法时(public),就相当于发布了这个对象。发布一个引用出去就可能带来安全性问题,因为该引用信息逸出了。所以我们使用封装的最主要原因就是封装能够使得对程序的正确性进行分析变得可能,减少可能的破坏设计约束条件。

  6. 线程封闭:不共享数据,就不会有并发问题。局部变量(即栈封闭)和ThreadLocal类是线程封闭的,线程封闭是在程序设计中的一个考虑因素(Swing、JDBC的Connection对象)

二、如何组合线程安全类juc模块介绍(4-5章)
  1. 线程安全组合

    设计线程安全类的三个基本要素:

      找出构成对象状态的所以变量。
    • 找出约束状态变量的不变性条件。
    • 简历对象状态(变量)的并发访问管理策略(形成正式文档)。

​ 实例封闭:将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易保证现场在访问数据时总能持有正确的锁。(即把对象的数据访问收敛到方法上,这样只需要对访问该方法加锁同步就能保证对其访问的线程安全)

  1. 线程安全容器类
  2. 线程安全同步工具类
三、结构化并发应用程序——如何利用线程提高并发应用程序的吞吐量或响应性(6-9章)
  1. 如何识别可并行执行的任务,以及如何在任务执行框架中执行任务。
  2. 健壮的并发应用程序的关键——如何实现取消和关闭线程执行任务。
  3. 线程池的使用
  4. 如何提高单线程子系统的响应性——图形用户界面应用程序
四、活跃性、性能与测试——确保并发程序执行预期任务,获得理想性能(10-12章)
  1. 如何避免活跃性故障
  2. 如何提高并发代码的性能和可伸缩性。
  3. 测试并发代码的技术
五、高级主题—— (13-16章)
  1. 显示锁
  2. 原子变量
  3. 非阻塞算法
  4. 自定义同步工具类

//待补充

4、作者想要我做什么或者想要解决什么问题。

理解和领悟并发的思想和其优缺点,并教我们怎么编写优秀的并发代码。

//待补充

二、诠释这本书内容

5、关键字,与作者达成共识

安全性:永远不会发生糟糕的事情。

活跃性:某件正确的事情最终会发生。(当某个操作无法继续执行下去时,就会发生活跃性问题,如死锁、饥饿、活锁)

性能:包含多个方面,如服务时间过长(提供的服务接口响应太慢);响应不灵敏;吞吐率过低;资源消耗过高;可伸缩性降低等。

线程、锁、

竞态条件:由于不恰当的执行时序而出现不正确的结果。最常见的竞态条件类型(先检查后执行:CHECK-THEN-ACT)

最低安全性:线程在没有同步情况下读取变量时,可能会得到一个失效值,但至少不是随机数,而是之前线程设置的值,这种安全保证称为最低安全性。(最低安全性不适用于非volatile类型的64位数值变量–> double、long,因为jvm运行将64位的读或写操作分解成两个32位的读或写操作)

依赖状态的操作:即某个操作包含基于状态的先验条件(如队列删除前判空等),那么这个操作就是依赖状态的操作。要实现某个等待先验条件为真时才执行的操作,可以通过(Blocking Queue , Semaphore 实现)。

//待补充

6、重要句子即作者的主旨

  • 要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(shared)和可变的(mutable)状态的访问。对象的状态(主要在实例或静态域)中的数据。
  • 共享:多个线程访问。
  • 可变:生命周期内可以发生变化。
  • 正确的编程方法:首先使代码正确运行,然后再提高代码的速度。(先完成,再优化)
  • 线程安全性定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,说明这个类是线程安全的。

无状态对象一定是线程安全的。(因为它既不包含任何域,也不包含任何对其他类的类中域的引用。)

  • 尽可能使用现有的线程安全对象(如AtomicLong)来管理类的状态。如往无状态的类中添加一个有线程安全的对象来管理状态的类时,这个类仍然是安全的。
  • 要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。(即使这些状态变量各自都是线程安全的,也要保证所以的状态操作在同一个原子操作中,相当于一个事务)
  • 正如“除非需要更高的可见性,否则将所有的域(属性)都声明为私有域是一个良好的编程习惯”,“除非需要某个域是可变的,否则应将其声明为final域”也是一个良好的编程习惯。
  • 任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步(疑问:我的理解是这些对象不可变,所以只发布一次;或者是这些对象内部不可变对象的三个条件,所以不会有不确定性)。
  • 实例封闭是构建线程安全类的最简单方式。即:将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。

实例封闭实例:

@ThreadSafepublic class TestBeanSet {@GuardedBy("this")private final Set<TestBean> mySet = new HashSet<TestBean>();public synchronized void addTestBean(TestBean t) {mySet.add(t);}public synchronized boolean containsTestBean(TestBean t) {return mySet.contains(t);}}
  • 如果不了解对象的不变性条件(取值不能为负数)与后验条件(取值变化范围在两个域值的范围区间,如范围的上下界,下界值应该小于等于上界值),那么就不能确保线程安全性。要满足在状态变量的有效值或者状态转换上的各种约束条件,就需要借助于原子性和封装性(怎么借助封装性?)。
  • 基于状态的先验条件判断的操作称为依赖状态的操作。如判空移除元素等。这种比较多见与生存消费模式下的操作,常见有Blocking Queue 或 Semaphore 。

//待补充

7、找出作者的的论述,总结作者的想法、看法

归纳法或者演绎法

并发程序中使用和共享对象时的实用策略,有四:

1、线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,不共享,则线程安全使用对象。

2、只读共享(即不可变对象)。在没有额外的同步情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它(有的情况下可以新建一个不可变对象去替换它)。共享的只读对象包括不可变对象和事实不可变对象。

3、线程安全共享(经过线程安全同步的数据结构或者类,如currenthashmap等juc包下的线程安全类)。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公共接口(get/set)来进行访问而不需要进一步同步(加锁等操作)。

4、保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象(是什么?),以及已发布的并且有某个特定锁保护的对象(显示锁,@GuardedBy)。

//待补充

8、作者要我们这么做的目的(解决了哪些问题)

写出线程安全的类。

三、评论这本书

前提条件:完成大纲架构

在评论时能区分真正的知识和个人的观点的不同

批评时要能证明作者的知识不足或错误、不合逻辑、分析与理由不完整等。

自己的评论,或者读后感。

一些知识整理

1、并发中的单例模式写法:

单例模式(从双重加锁走向延迟初始化占位类模式)

java并发中的延迟初始化 推荐这个

2、高效是指能在串行性和异步性之间找到合理的平衡。

3、读取volatile 变量开销只比读取非volatile变量的开销略高一点,因此远低于加锁的变量。volatile的正确使用方式包括:只能确保可见性,以及标识程序生命周期事件的发生(如初始化或关闭)。还需要寻找一下程序中volatile的使用。

原子变量(atomicInteger等)是一种更好的volatile。

满足以下三个条件,才应该使用volatile变量:

  1. 对变量的写入操作不依赖变量的当前值,或者确保只有单个线程更新变量的值(set方法是synchronized的同步方法)
  2. 该变量不会与其它状态变量一起纳入不变性条件中。(否则可见性就没有意义,因为他如果本来就不变,那不需要可见性啊)
  3. 在访问变量时不需要加锁。

4、如果想在构造函数中注册一个事件监听器或启动线程,可以使用一个私有的构造函数和一个公共的工厂方法。还需要理解下!

疑问解答

1、p1中的粗粒度通信机制交换数据,包括:套接字、信号处理器、共享内存、信号量以及文件等,里面名称的意思和通信机制一般是什么?

2、时间分片(time slicing)?

A:指每个CPU会把其时间进行分配,每个程序都有机会占有一个或多个片段,去执行自己的任务。

3、什么是冯诺依曼计算机?

4、什么是句柄?内存句柄?文件句柄?

5、上下文切换会带来多大的开销呢?

6、抽象和封装会降低程序的性能?是拿来跟面向过程(C)比?

7、volatile的使用?

8、发布和逸出需要仔细了解下?p34:不正确构造?

9、p25:搞不清楚这两个同步代码块不会因为中间释放锁而导致失去锁,应用安全性不能满足么?

10、为什么final类型的域使用越多,就越能简化对象可能状态的分析过程?或者说final 域为什么能保证是不变的?如果是个fina对象呢,对象里面的属性也必须是final的才能保证吧?

使用到并发的开源代码或组件

1、Swing 的用户界面框架

2、Servlet

3、RMI(rpc)

4、Swing、JDBC学习线程封闭技术

5、连接池是线程安全的。

参考

《Java并发编程实战》

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » Java并发编程实战——读后感