这一节主要研究如何安全的让对象可以被多个线程访问。
- **定义:**当一条线程修改了共享变量的值,其他线程可以立即得知这个修改。
- **实现方式:**在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的方式实现,依赖主内存作为传输媒质。
- 可以保证可见性的关键字:
- volatile:通过 volatile 的特殊规则;
- synchronized:通过“对一个变量执行unlock操作前,必须将该变量同步回主内存”这条规则;
- final:被 final 修饰的字段,一旦完成了初始化,其他线程就能看到它,并且它也不会再变了;
- 只要不可变对象被正确的构建出来(没有发生 this 引用溢出),它就是线程安全的。
-
用来作为状态判断的量被其他线程修改了,运行的那个线程就是看不见……
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { @Override public void run() { while (!ready) { Thread.yield(); // 放弃当前CPU资源 } System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } } /* 这段代码可能有一下三种输出: 1. 正确输出42 2. 持续循环下去,如ReaderThread放弃当前CPU资源后,立即再次抢到CPU资源 3. 输出0,因为在没有同步的情况下,Java编译器,处理器及运行时会对操作顺序进行重排序, 所以number = 42;和ready = true;这两句的执行顺序可能会互换, 导致ready为true时,number还没被赋值为42 */
-
非原子的64位操作:读到的数高位已经被改了,低位还没来得及改
volatile 变量是用来确保将变量的更新操作通知给其他线程的,即**在读取 volatile 变量时,总会返回最新写入的值!**是一种比synchronized 更轻量级的同步机制。
- 该变量保证对所有线程的可见性
- 禁止指令重排序优化
- 在 volatile 变量的赋值操作的反编译代码中,在执行了赋值操作之后加了一行:
lock addl $0x0,(%esp)
- 这一句的意思是:给 ESP 寄存器 +0,是一个空操作,重点在 lock 上
- 首先 lock 的存在相当于一个内存屏障,使得重排序时,不能把后面的指令排在内存屏障之前
- 同时,lock 指令会将当前 CPU 的 Cache 写入内存,并无效化其他 CPU 的 Cache,相当于对 Cache 中的变量做了一次 store -> write 操作
- 这使得其他 CPU 可以立即看见 volatile 变量的修改,因为其他 CPU 在读取 volatile 变量前会先从主内存中读取 volatile 变量,即进行一次 read -> load 操作
在对 volatile 变量执行 read、load、use、assign、store、write操作时:
- use 操作必须与 load、read 操作同时出现
- use <- load <- read
- assign 操作必须与 store、write 操作同时出现
- assign -> store -> write
- 同一个线程进行如下两套动作,可以保证:如果 A 先于 B 执行,那么 P 先于 Q 执行
- 第一套:A (use/assign) -> F (load/store) -> P (read/write)
- 第二套:B (use/assign) -> G (load/store) -> Q (read/write)
- synchronized:既保证可见性,又保证原子性
- volatile:只保证可见性(所以count++原子性无法保证)
- 常见的应用:
- 某个操作完成的标志
- 发生中断的标志
- volatile变量不适用的场景:
- 运算结果依赖与该变量当前的值,比如
i++
- 运算结果依赖于其他状态变量
- 运算结果依赖与该变量当前的值,比如
使对象能在当前作用域之外使用。
- 最简单的发布方法:
public static
- 将指向该对象的引用保存到其他代码可以访问的地方
- 在某个非私有的方法中返回该引用
- 将引用传递到其他类的方法中
发布了不该发布的对象。
class UnsafeStates {
private String[] states = new String[] {"AB", "CD"};
public String[] getStates() {
return states; // 可以通过这个方法得到states,然后就可以随便修改states,就逸出了
}
}
- 产生原因
- 在一个对象的构造方法中启动了一个线程,并在这个线程的 public 方法中调用了这个对象的方法,相当于将还没构造好的对象的 this 实例泄露了。
- 解决方法
- 私有化构造方法,只在构造方法中写新线程的代码但不 start,然后写一个工厂方法 newInstance 来创建实例,在工厂方法中先调用构造函数创建实例,再启动线程,这样就不会把一个还没有构造好的对象发布出去了。
我的理解:我们需要保证别的线程拿到我们的对象时,要是一个完整的执行完构造函数的对象,不能是一个构造函数执行了一半的对象。
使用局部变量,并保证这个局部变量不溢出。
类似于对应线程的全局变量,但是每一个线程维护一个自己的该变量对应值。
- 每一个 ThreadLocal 都有一个唯一的的 ThreadLocalHashCode;
- 每一个线程中有一个专门保存这个 HashCode 的
Map<ThreadLocalHashCode, 对应变量的值>
; - 当
ThreadLocal#get()
时,实际上是当前线程先拿到这个 ThreadLocal 对象的 ThreadLocalHashCode,然后通过这个 ThreadLocalHashCode 去自己内部的 Map 中去取值。- 即每个线程对应的变量不是存储在 ThreadLocal 对象中的,而是存在当前线程对象中的,线程自己保管封存在自己内部的变量,达到线程封闭的目的。
- 也就是说,ThreadLocal 对象并不负责保存数据,它只是一个访问入口。
- 对象创建后,其状态不能被修改
- 对象是正确创建的(无 this 引用逸出)
- 因为对象是不可变的,所以多个线程可以放心大胆的同时访问
- 当这个对象中的状态需要被改变时,之间废掉当前的对象,new 一个新对象代替现在的旧对象
final 域是我们用来构造不可变对象的一个利器,因为被它修饰的域一旦被初始化后,就是不可修改的了,不过这有一个前提,就是如果 final 修饰的是一个对象引用,必须保证这个对象也是不可变对象才行,否则 final 只能保证这个引用一直指向一个固定的对象,但这个对象自己的状态是可以改变的,所以一个所有域都是 final 的对象也不一定是不可变对象。
final i = 42;
final i; // 之后在每一个构造函数中给i赋值
注意:对于含有 final 域的对象,JVM 必须保证对象的初始引用在构造函数之后执行,不能乱序执行(也就是说,一旦得到了对象的引用,那么这个对象的 final 域一定是已经完成了初始化的)!
保证发布的对象的初始化构造过程不会受到任何其他线程干扰,就像加了锁一样,被创建它的线程构造好了,在发布给其他线程。
- 在静态块中初始化一个对象引用
- 将对象引用保存到 volatile 类型的域或者 AtomicReference 对象中
- 将对象引用保存到某个正确构造的对象的 final 域中
- 将对象引用保存到一个被锁保护的域中
public static Holder holder = new Holder(42);
可以保证安全的原因:静态变量的赋值操作在加载类的初始化阶段完成,包含在<clinit>()
方法的执行过程中,因此这个过程受到 JVM 内部的的同步机制保护,可以用来安全发布对象。
- Map
- HashTable
- ConcurrentMap
- Collections.SynchronizedMap
- 使用
Collections.synchronizedMap(Map<K,V> m)
获得 - 所有方法都被 synchronized 修饰
- 使用
- List
- Vector
- CopyOnWriteArrayList
- CopyOnWriteSet
- Collections.SynchronizedSet
- 使用
Collections.synchronizedSet(Set<T> s)
获得 - 所有方法都被 synchronized 修饰
- 使用
- Queue
- BlockQueue
- ConcurrentLinkedQueue