读书笔记 JAVA 并发编程实战 第三章 对象的共享
第二章讲过正确的并发程序,关键在于:访问共享的可变状态时需要进行正确的管理。并且也介绍了如何共享和发布对象,从而使得他们能够由多线程访问。
同步代码块和同步方法可以确保原子操作,且常见错误是认为synchronized只能用于实现原子性或者确定“临界区(Critical Section)”,同步的另一个重要方面,内存可见性(Memory Visibility)。
可见性
主线程启动读线程,然后设置number 42,将ready 设为true。 读线程一直循环直到发现ready 变为true。然后输出number。看起来可能输出42。但是实际上,读线程可能永远看不到ready的值,还有可能输出0(看到了ready,但是没看到写入后的number值)。称为重排序。当主线程首先写入number,然后没有同步的情况下写入ready,那么读线程看到的顺序,可能与写入顺序完全相反。
Tips:放弃当前线程的执行权,转给包括自己在内的其他线程执行
失效的数据
失效数据,当读线程查看ready变量时,可能得到失效值,除非每次访问变量时,都是用同步,否则可能看到失效值。最坏的情况是,一个线程获得失效值,一个线程获得最新值。
同步的问题:get 可能会得到更新后的value,也可能看不到。
通过对于set,get方法同步,得到了一个线程安全的类。只对set方法同步是不够的。
非原子性的 64位操作
没有同步的情况,可能看到一个失效值。但是不并不是一个随机值,这种安全性保证成为最低安全性。
最低安全性适用于绝大多数变量,唯一例外:非volatile 类型的64位数值变量。Java内存模型要求,读取写入都必须是原子性操作。但是对于非volatile 类型的long和double,JVM允许将64位的读操作和写操作分解为两个32位操作。可能会读取到一个值高32位,和另一个值低32位。
加锁与可见性
共享且可变的变量需要所有的线程在同一个锁上同步,就是为了确保某个线程写入的时候,变量的值对于其他线程都是可见的。避免读到失效值。
Volatile 变量
稍弱的同步机制 volatile变量,确保看见性,对于volatile变量,线程会将其拷贝进共享内存操作。并且禁止指令重排序。访问volatile变量时不会进行加锁,因此不会阻塞,是一种更加轻量级的同步方式。
典型用法是:检查某个状态以判断是否退出循环,但是需要注意,volatile不保证递增操作的原子性。所以除非确保只有单线程对变量执行写操作。否则不能这样写。
满足一下条件时,使用volatile变量:
- 对于变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值(单线程写)
- 该变量不会与其他状态变量一起纳入不变性条形中
- 在访问变量时不需要加锁(读不加锁)
发布与逸出
“发布(publish)”一个对象的意思是,对象能够在当前的作用域之外的代码使用。例如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回改引用。如果发布时要确保线程安全,就要进行同步,发布内部状态可能破坏线程安全性。当发布了某个不改发布的对象,这种就叫“逸出”。
发布对象的最简单方法:将对象的引用保存在共有的静态变量
发布的时候,也会间接的发布其他对象。因为任何代码都可以遍历这个集合,获取对个新Secret对象的引用。从非私有方法中返回一个引用。
按照上述的方法,就会导致任何调用者可以修改这个数组的内容。数组states 已经逸出了他的作用域。已经私有的变量被发布了。
如果一个已经发布的对象能够通过非私有的变量引用和方法调用到其他对象,那么这些对象也都会被发布。当把一个对象传递给某个方法时,就相当于发布了这个对象。你就无法确定呢这个对象的状态。即,那些代码会执行,还是会保留对象的引用并由另一个线程使用。
当某个对象逸出后,必须假设某个类或线程可能会误用该对象。 这是需要封装的主要原因:封装可以使得对程序的正确性进行分析变得可能,并使得破坏设计约束条件变得更难。