如需转载,请根据 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 许可,附上本文作者及链接。
本文作者: 执笔成念
作者昵称: zbcn
本文链接: https://1363653611.github.io/zbcn.github.io/2019/12/09/concurrency_03%E5%AF%B9%E8%B1%A1%E5%85%B1%E4%BA%AB/
对象共享
问题?
- 在访问共享的可变状态时,需要进行正确的管理
- 共享和发布对象
- 临界区 /内存可见性
可见性
多个线程的同步机制,可以保证线程的可见性
重排序
- 在没有看到同步的情况下,编译器,处理器以及运行时等都可能对操作的顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,内存操作顺序是无法准确判断的。
失效数据 No Visibility
- 在缺乏同步的程序中,产生错误结果的一种情况
非原子性的64位操作
- 非 volitale 类型的 long 或者double 变量,jvm允许将64位的读操作或者写操作分解位2个32位的操作。对该变量的读写操作,是非原子性的。分别要读取高32位和低32位。
- 解决方案
- 使用volitale 关键字修饰
- 使用枷锁的方式
- 最低安全性
- 当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性(out-of-thin-air-safety)
加锁与可见性
- 加锁的含义不仅局限于互斥行为,还包含内存可见性。为了确保所有线程在都能看到共享的最新值,所有执行读操作或者写操作的线程在同一个锁上同步
Volatile 变量
- 一种消弱同步机制的变量,来确保将变量的更新操作通知到其他线程
- volatile 是一种比sychronized 更轻量级的同步机制
- 特点
- 内存可见性
- 阻止指令重排序
- 使用场景
- 自身状态的可见性,确保他们所引用对象的状态的可见性
- 标记一些重要程序生命周期事件的可见性
- 对变量的写入操作不依赖变量的当前值,或者可以确保只能有单个线程更新变量的值
- 该变量不与其他状态变量一期纳入不变性条件中
- 变量在访问时不需要加锁
- 加锁机制既可以保证可见性,又可以保证原子性,而volatile 只能保证可见性
发布与溢出
- 发布
- 使对象能在当前作用域之外的代码中使用
- 发布对象的内部状态可能破坏封装性,使得程序难以维持不变性条件
- 逸出
- 某个不该发布的对象被发布时,就成为对象逸出。(对象在构建完成之前被发布)
- 发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布
- 封装能够使得对程序的正确性进行分析变得可能,并使得无意中破环设计约束条件变得更难
- 安全的对象构造过程
- 当访问共享的可变数据时,通常需要使用同步
- 当某个对象封闭在一个线程当中,这种用法将自动实现线程安全性,即使对象本身不是线程安全的
- 使用
- SWING 中大量使用线程封闭技术
- JDBC 的connection对象
- Ad-hoc 线程封闭
- 线程封闭的指责由程序实现
- 该线程封闭特别脆弱。不建议使用
- volatile 变量上存在着一种特殊的线程封闭(要确保只有单个线程对共享的volatile变量执行写入操作)
- 栈封闭
- 是线程封闭的一种特例。
- 在栈封闭中只能通过局部变量才能访问对象。
- 封装容易使得代码更容易维持不变性条件,同步变量也能使对象更容易封闭在线程中
- 线程内部使用或者线程局部使用,不要与核型类库中的threadLocal 混淆
- ad—hoc 线程封闭更容易维护
- 局部变量的固有属性之一就是封闭在执行线程中
threadLocal 类
- 通常用于防止对可变的单实例变量 或者全局变量进行共享
- 当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次使用时都重新分配零时对象,就可以使用ThreadLocal
- threadLocal 变量类,类似于全局变量,它可以降低代码的可重用性,并且在类之间引入隐含的耦合性,因此在使用时要格外的小心。
不变性(不可变对象)
不可变对象
- 满足同步需求的另一种方法
- 如果某一个对象在创建之后其状态就不能被修改,那么这个对象就称作不可变对象。
- 不可变对象的固有属性就是线程安全
- 只有一种状态,并且该状态只有构造函数来控制。
- 不可变对象不会被恶意代码修改,因此可以安全的发布和共享
- 不可变对象满足的条件
- 对象创建后其状态就不可被修改
- 对象所有的域都是final类型的
- 对象时正确创建的(对象在创建期间,this引用没有逸出)
- “不可变对象” 和“不可变对象引用” 之间存在着差异
final 域
- final类型的域是不可被修改的(但是final域所引用的对象是可变的,即这些被引入的对象内部是可以被修改的)
- final能确保初始化的安全性,从而可以不受限制的访问对象,并在只用这些对象时无需同步
- 限制对象的可变性,即限制了对象可能的状态集
使用volatile 类型来发布不可变对象
- 访问和更新多个变量时,出现竞 态条件问题,可以通过将这些变量全部保存在一个不变的对象中来消除。
- 如果是一个可变对象,就要使用锁来保证原子性
- 如果是一个不可变对象,那么当线程获得了该对象的引用后,就不必担心另一个线程会修改对象的状态。
- 如果要更新这个不可变对象的状态,那么可以创建一个新的容器对象,但其它使用原有对象的线程,仍然会看到对象处于一致状态
- 设计范例(不使用锁的线程安全写法)
- 使用包含多个状态变量的容器对象来维持不变性条件
- 使用一个volatile类型的引用来保证可见性
安全发布
不正确的对象发布导致其他线程看到尚未创建完成的对象
不正确的发布
- 多个线程共享数据时,会发生一些非常奇怪的事情
- 不正确发布对象存在问题
- 除了发布对象的线程外,其他线程看到的域是一个失效值(空引用或者之前的旧值)
- 看到的域的引用是最新的,但是但是域的状态值是失效的
- 第一次读到的域的值是失效的,再次读取到的值是有效的
不可变对象与初始化安全性
- 不可变对象
- 状态不可修改
- 所有的域都是final
- 正确的构造过程
- 不可变对象
任何线程都可以在不需要任何额外的同步的情况下,安全的访问不可变对象,即使在发布这些对象时没有使用同步
安全发布对象的常用模式
- 可变对象
- 发布和使用该对象的线程都必须使用同步
- 前提
- 对象的引用和对象的状态对其他线程可见
- 正确构造的对象
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保存到volatile类型的域或者atommicReference 对象中
- 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中
- 可变对象
线程安全库的容器类(线程安全容器)
- 通过将一个键或者值放入hashTable、synchronizedMap、ConcurrentMap中,可以安全的将该元素发布给任何从这些容器访问它的线程(无论是直接访问还是通过迭代器访问)
- 通过将元素放入vector、copyOnWriterArrayList\copyOrWriterArraySet\SychronizedList \synchorizedSet中,可以将元素安全的发布到任何从这些容器中访问该元素的线程
- 通过将某元素放入BlockingQueue\CurrentLinkedQueue中,可以将该元素安全地发布到任何这些队列中访问该元素的线程
- 类库中的其他数据传递机制(Future、Exchanger)同样能安全实现安全发布。
正确发布静态对象:最简单和最安全的方式是使用静态的初始化器
- 静态初始化器是jvm在类的静态初始化阶段执行,由于jvm内部存在这同步机制,因此通过这种方式初始化的任何对象都可以被安全的发布
- 事实不可变对象
- 如果对象从技术上是可变的,但是在对象发布后,其状态再不会改变
- 在没有额外的同步情况下,任何线程都可以安全的使用被安全发布的事实不可变对象
对象可变
- 使用要求(不仅在对象发布时需要同步,而且每次对象访问同样需要使用同步来保证后续修改操作的可见性)
各种对象的发布要求
- 不可变对象可以使用任意机制来发布
- 事实不可变对对象比须通过安全方式来发布
- 可变对象必须通过安全方式来发布,并且必须是是线程安全的或者由某个锁保护起来
安全的共享对象
并发程序中使用和共享对象时,可以使用一些实用的策略
- 线程封闭
- 线程封闭的对象只能由一个线程拥有,对象被封闭在线程中,而且只能由这个线程修改
- 只读共享
- 没有额外同步情况下,共享只读对象可以由多个线程并发访问,但任何线程都不能修改它。
- 只读共享线程的类型
- 不可变对象
- 事实不可变对象
- 线程安全共享
- 线程安全的对象,在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步
- 保护对象
- 被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象