JAVA并发实战学习笔记——3,4章

JAVA并发实战学习笔记


第三章 对象的共享

  • 失效数据
    • java程序实际运行中会出现①程序执行顺序对打乱;②数据对其它线程不可见——两种情况
    • 上述两种情况导致在缺乏同步的程序中出现失效数据这一现象,且“失效”这一情况不确定性很大,因为可能出现可能没出现。
    • JVM中没有规定对于64位变量如:long, double 的读写操作必须是原子的,因此不同步的情况下读取该类数据可能得到的值无意义(低32位和高32位没有一起形成完整的数字)
      • 商用的JVM一般会让64位变量的读取原子化。
  • 使用volatile修饰变量可以保证变量的可见性,但不会保证互斥性,是比内置锁更弱的同步机制。
    • 在以下两种情况下可以使用volatle保证同步。但是过度依赖volatile会使得代码脆弱、可读性差。
      • 1.只有一个线程对volatile变量实行写操作;
      • 2.变量不需要与其它状态变量共同参与不变约束。
    • 加锁操作兼保证原子性和可见性

  • 发布(将变量的引用保存到其它代码可以访问到的地方)与逸出(不该发布的变量被发布了):

    • 发布的几种方式:
      • public static A
      • 某个对象A的引用在另一个对象B中,发布B则间接地发布了A
      • 将对象传递给外部方法
      • 发布内部类对象A,A中隐含地包含了外部类对象B
  • 逸出

    • 在构造函数中启动了新线程(该对象的内部线程类),使得this指针在实例没有建好时便被其它线程共享了。

  • 线程封闭:线程封闭是一类编程的方法,使得各个线程中的对象是不相互共享的,如:JDBC中的Connection对象
    • Ad-hoc线程封闭:尽量少用,十分脆弱
    • 栈封闭:局部变量由于在运行中是处于线程栈的局部变量表和操作数栈中,所以局部变量无论是否是线程安全,只要其引用不发布出去,都是线程安全的。
      • 以能用局部变量就别用全局的变量,全局变量容易引起并发问题。
    • ThreadLocal类线程封闭:可将ThreadLocal t 看成是Map<Thread,T> t,保存了每个线程到T对象的一份副本,t.get()得到的是initialValue()设定的值。
      • 副本保存在线程中,随着线程的技术被垃圾回收
      • 引入了类之间的耦合性,小心使用
      • ThreadLocal类应用场景示例:移植单线程程序到多线程环境

  • 不变性
    • 满足下列三个条件的对象满足“对象不可变
      • 创建好后,不提供更改其状态的方法
      • 所有的域都是 final型
      • 对象是正确创建的
    • 对象引用不可变”————相比于对象不可变,对象的内容可以改变,但是对象地址无法改变(即用final修饰的变量)
    • 不可变对象为一系列操作提供弱原子性
      • 只要将参数传入方法,在该方法内就有指向域的引用,其它的线程修改了原对象的域,也不会影响到该方法对原域的访问。

  • 安全发布————确保可以不受JAVA不可见性的影响,得到发布的最新的对象
    • 不可变对象安全发布的方式
      • final域具有特殊的初始化安全性保证,在初始化的时候即使没有同步,也可以保证其可见性。
    • 可变对象安全发布的方式
      • 静态初始化对象(标有static的对象初始化代码是在类初始化阶段执行的,JVM自带线程安全特性)
      • 将对象的引用保存在volatile类型的域中,或者AtomicReference对象中
      • 对象引用存入final类型域中
      • 将对象引用存入某个由锁保护的域中
  • 安全访问
    • 事实不可变对象和不可变对象
      • 任意访问
    • 可变对象
      • 访问时需要同步机制,对象需要是线程安全,或者访问前先获得某个锁

第四章 对象的组合


  • 如何建立线程安全的类
    • 1.收集同步需求————候选范围为对象的域,包括对象中包含的基本类型变量,以及域对象,和域对象内的域
      • 不变性条件
        • 单个变量,即变量的值需要在其合法范围内
        • 多个变量,即多个变量的值之间需要满足某些约束
        • 访问同一个不变性条件中任何一个变量,都需要获得同一个锁,以确保对操作不会破坏不变性条件
      • 后验条件
        • 变量的值转换需要满足的约束
      • 不变性条件和后验条件约束了对象的哪些状态和状态转换是有效的
    • 2.先验条件————即依赖状态的操作

  • 实例封闭————使封装的数据被封闭在另一个对象中,被封闭的对象不超出其作用域????

    • 一般知识

      • 将对实例内封装的对象的访问限制在对象的方法上,以确保线程在访问数据是总能持有正确的锁.
      • 含有线程不安全的内部对象的线程安全类示例:
        • Collections.synchronized***()方法通过装饰器模式将容器封装在一个线程安全的对象中
       
      public class PersonSet{
      
      private final Set myset = new HashSet;
      	public synchronized void addPerson(Person p){
      		myset.add(p);
      	}
      
      	public synchronized boolean containsPerson(Person p){
      		return myset.contains(p);
      	}
      }
      
      • 疑惑之处:
        • 不超出作用域?如何保证方法有合适的返回值呢?觉得很奇怪。
          • 安全发布状态变量的三个条件
            • 1. 状态变量是线程安全的
            • 2. 变量不存在不变性约束
            • 3. 在方法中不包含使得该变量进入不合法状态的操作
        • 而且使用锁同步,和不使对象溢出有什么联系呢?我觉得这就是两个独立的东西,放在一起凑成一节,让人困惑
          • 猜测:要实现非线程封闭的线程安全类,封闭对象是第一步,限制访问方法并使访问方法同步是第二步。
    • Java监视器模式(即私有的所对象,而非内置锁)
      • 好处是不会让外部的方法得到该对象的锁
      • 若容器内的对象是非线程安全的,可以每次发布该对象的时候都深度复制

  • 线程安全性的委托
    • 将线程安全性委托给单个/多个(彼此独立的对象,且所有的方法中都不包含无效状态转换操作)线程安全的状态
    • 若类中包含不符合上一条要求的,包含多个有不
    • 条件约束的状态,实现线程安全性需要加锁机制

  • 在现有的线程安全类中添加新功能
    • 在并发中,能用现成的线程安全类就尽量用;若现成的类满足不了需求,则可能选择添加新功能
    • 四种方法:
      • 改源码 ————不现实
      • 扩展基础类 ———— 在自己的子类新添加的方法中使用内置锁——从这里来看,子类和父类中使用的内置锁应该是同一个,都是真对实例而言的。
        • 缺点:破坏了类的封装性
      • 客户端加锁机制 ———— 了解基础类对象使用的是什么锁,在客户端代码中使用相同的锁
        • 缺点:破坏了同步策略的封装性
      • 组合模式 ————使用装饰器模式将基础类如:list等封装在内部,将list的方法包装一层,使用装饰器类的内置锁。
        • 优点
          • 不会破坏封装性
          • 健壮性更强
          • 即使容器类不是线程安全的,也可以借此实现线程安全
        • 缺点
          • 多加一层锁,效率下降

  • 文档!!!文档!!!————要想使开发和用户使用遵从本类的安全机制,必须有文档记录
    • 设计文档 & 用户文档
    • Java文档中应该注明该类使用的同步机制,应该包含下面一些内容
      • 是否是线程安全的?
      • 客户回调需不需要加锁,可以加那些锁?
      • 哪些锁保护了哪些状态?(设计文档,可以用java注释便于后续开发)

 

posted @ 2018-07-20 12:30  学编程的兔子  阅读(107)  评论(0编辑  收藏  举报