一、前期准备:

1. ThreadLocal

ThreadLocal而是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。ThreadLocal是由TreadLocalMap实现,每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。

TreadLocal使用

  1. static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
  2. sThreadLocal.set()
  3. sThreadLocal.get()

set方法

TreadLocal中的set()方法实现,这里的Map用的是ThreadLocalMap,存储的结构为key-value键值对,key是线程,value则是要同步的值。

  1. //set 方法
  2. public void set(T value) {
  3. //获取当前线程
  4. Thread t = Thread.currentThread();
  5. //实际存储的数据结构类型
  6. ThreadLocalMap map = getMap(t);
  7. //如果存在map就直接set,没有则创建map并set
  8. if (map != null)
  9. map.set(this, value);
  10. else
  11. createMap(t, value);
  12. }
  13. //getMap方法
  14. ThreadLocalMap getMap(Thread t) {
  15. //thred中维护了一个ThreadLocalMap
  16. return t.threadLocals;
  17. }
  18. //createMap
  19. void createMap(Thread t, T firstValue) {
  20. //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
  21. t.threadLocals = new ThreadLocalMap(this, firstValue);
  22. }

从上面代码可以看出每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。

get方法

  1. //ThreadLocal中get方法
  2. public T get() {
  3. Thread t = Thread.currentThread();
  4. ThreadLocalMap map = getMap(t);
  5. if (map != null) {
  6. ThreadLocalMap.Entry e = map.getEntry(this);
  7. if (e != null) {
  8. @SuppressWarnings("unchecked")
  9. T result = (T)e.value;
  10. return result;
  11. }
  12. }
  13. return setInitialValue();
  14. }
  15. //ThreadLocalMap中getEntry方法
  16. private Entry getEntry(ThreadLocal<?> key) {
  17. int i = key.threadLocalHashCode & (table.length - 1);
  18. Entry e = table[i];
  19. if (e != null && e.get() == key)
  20. return e;
  21. else
  22. return getEntryAfterMiss(key, i, e);
  23. }

2. synchronized

synchronizedJava中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

    个人理解:synchronized只是一个关键字,他不是一个锁,他要做的是锁住它后面的对象/代码块

二、可见性问题

1. 定义:

当一条线程修改了共享变量的值,其他线程可以立即得知这个修改。

2. 实现方式: 在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的方式实现,依赖主内存作为传输媒质。

3. 可以保证可见性的关键字:

  • volatile:通过 volatile 的特殊规则;
  • synchronized:通过“对一个变量执行unlock操作前,必须将该变量同步回主内存”这条规则;
  • final:final 修饰的字段,一旦完成了初始化,其他线程就能看到它,并且它也不会再变了;

    只要不可变对象被正确的构建出来(没有发生 this 引用溢出),它就是线程安全的。

4. 失去可见性的危害

  1. 用来作为状态判断的量被其他线程修改了,运行的那个线程就是看不见……

    1. public class NoVisibility {
    2. private static boolean ready;
    3. private static int number;
    4. private static class ReaderThread extends Thread {
    5. @Override
    6. public void run() {
    7. while (!ready) {
    8. Thread.yield(); // 放弃当前CPU资源
    9. }
    10. System.out.println(number);
    11. }
    12. }
    13. public static void main(String[] args) {
    14. new ReaderThread().start();
    15. number = 42;
    16. ready = true;
    17. }
    18. }
    19. /*
    20. 这段代码可能有一下三种输出:
    21. 1. 正确输出42
    22. 2. 持续循环下去,如ReaderThread放弃当前CPU资源后,立即再次抢到CPU资源
    23. 3. 输出0,因为在没有同步的情况下,Java编译器,处理器及运行时会对操作顺序进行重排序,
    24. 所以number = 42;和ready = true;这两句的执行顺序可能会互换,
    25. 导致ready为true时,number还没被赋值为42
    26. */

    b. 非原子的64位操作:读到的数高位已经被改了,低位还没来得及改

    5. 解决方法

    volatile 变量是用来确保将变量的更新操作通知给其他线程的,即 在读取 volatile 变量时,总会返回最新写入的值! 是一种比synchronized 更轻量级的同步机制。

    6. volatile特点:

  • 该变量保证对所有线程的可见性
  • 禁止指令重排序优化

    7. 上述两个特点的实现原理

    在 volatile 变量的赋值操作的反编译代码中,在执行了赋值操作之后加了一行:lock addl $0x0,(%esp),这一句的意思是:给 ESP 寄存器 +0,是一个空操作,重点在 lock 上,首先 lock 的存在相当于一个内存屏障,使得重排序时,不能把后面的指令排在内存屏障之前,同时,lock 指令会将当前 CPU 的 Cache 写入内存,并无效化其他 CPU 的 Cache,相当于对 Cache 中的变量做了一次 store -> write 操作这使得其他 CPU 可以立即看见 volatile 变量的修改,因为其他 CPU 在读取 volatile 变量前会先从主内存中读取 volatile 变量,即进行一次 read ->load 操作。

    8. Java 内存模型中对 volatile 变量定义的特殊规则

    在对 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)

      详见 Java内存模型

9. 与 synchronized 的区别

  • synchronized:既保证可见性,又保证原子性
  • volatile:只保证可见性(所以count++原子性无法保证)

    对任意单个volatile变量的读/写具有原子性,但类似于n++这种复合操作不具有原子性。

三、通过确保状态不被发布来保证安全性

1. 发布与溢出

发布
使对象能在当前作用域之外使用。
发布方法:

  • 最简单的发布方法:public static
  • 将指向该对象的引用保存到其他代码可以访问的地方
  • 在某个非私有的方法中返回该引用
  • 将引用传递到其他类的方法中

溢出
发布了不该发布的对象。

2. 几个溢出的例子:

  • 一个简单的溢出的例子:

    1. class UnsafeStates {
    2. private String[] states = new String[] {"AB", "CD"};
    3. public String[] getStates() {
    4. return states; // 可以通过这个方法得到states,然后就可以随便修改states,就逸出了
    5. }
    6. }
  • this引用溢出

    • 产生原因
      • 在一个对象的构造方法中启动了一个线程,并在这个线程的 public 方法中调用了这个对象的方法,相当于将还没构造好的对象的 this 实例泄露了。
    • 解决方法
      • 私有化构造方法,只在构造方法中写新线程的代码但不 start,然后写一个工厂方法 newInstance 来创建实例,在工厂方法中先调用构造函数创建实例,再启动线程,这样就不会把一个还没有构造好的对象发布出去了。

        3. 线程封闭

        栈封闭
        使用局部变量,并保证这个局部变量不溢出。
        ThreadLocal 类
        类似于对应线程的全局变量,但是每一个线程维护一个自己的该变量对应值。
        ThreadLocal 实现原理
  • 每一个 ThreadLocal 都有一个唯一的的 ThreadLocalHashCode;

  • 每一个线程中有一个专门保存这个 HashCode 的 Map
  • ThreadLocal.get() 时,实际上是当前线程先拿到这个 ThreadLocal 对象的 ThreadLocalHashCode,然后通过这个 ThreadLocalHashCode 去自己内部的 Map 中去取值。
    • 即每个线程对应的变量不是存储在 ThreadLocal 对象中的,而是存在当前线程对象中的,线程自己保管封存在自己内部的变量,达到线程封闭的目的。
    • 也就是说,ThreadLocal 对象并不负责保存数据,它只是一个访问入口。

      详见 ThreadLocal

4. 不可变对象

  • 定义
    • 对象创建后,其状态不能被修改
    • 对象是正确创建的(无 this 引用逸出)
  • 使用方法
    • 因为对象是不可变的,所以多个线程可以放心大胆的同时访问
    • 当这个对象中的状态需要被改变时,之间废掉当前的对象,new 一个新对象代替现在的旧对象
  • final 域

final 域是我们用来构造不可变对象的一个利器,因为被它修饰的域一旦被初始化后,就是不可修改的了,不过这有一个前提,就是如果 final 修饰的是一个对象引用,必须保证这个对象也是不可变对象才行,否则 final 只能保证这个引用一直指向一个固定的对象,但这个对象自己的状态是可以改变的,所以一个所有域都是 final 的对象也不一定是不可变对象
final的两种初始化方式

  1. final i = 42;
  2. final i; // 之后在每一个构造函数中给i赋值

注意:对于含有 final 域的对象,JVM 必须保证对象的初始引用在构造函数之后执行,不能乱序执行(也就是说,一旦得到了对象的引用,那么这个对象的 final 域一定是已经完成了初始化的)!

四、安全发布对象

1. 什么是安全发布对象

保证发布的对象的初始化构造过程不会受到任何其他线程干扰,就像加了锁一样,被创建它的线程构造好了,在发布给其他线程。

2. 常用发布模式

  • 在静态块中初始化一个对象引用
  • 将对象引用保存到 volatile 类型的域或者 AtomicReference 对象中
  • 将对象引用保存到某个正确构造的对象的 final 域中
  • 将对象引用保存到一个被锁保护的域中

    3. 最简单和安全的发布方式

    1. public static Holder holder = new Holder(42);

    可以保证安全的原因:静态变量的赋值操作在加载类的初始化阶段完成,包含在<clinit>()方法的执行过程中,因此这个过程受到 JVM 内部的的同步机制保护,可以用来安全发布对象。

    4. Java 提供的可以安全发布对象的容器

  • 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