一、前期准备:
1. ThreadLocal
ThreadLocal而是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。ThreadLocal是由TreadLocalMap实现,每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。
TreadLocal使用
static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set()
sThreadLocal.get()
set方法
TreadLocal中的set()方法实现,这里的Map用的是ThreadLocalMap,存储的结构为key-value键值对,key是线程,value则是要同步的值。
//set 方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//实际存储的数据结构类型
ThreadLocalMap map = getMap(t);
//如果存在map就直接set,没有则创建map并set
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//getMap方法
ThreadLocalMap getMap(Thread t) {
//thred中维护了一个ThreadLocalMap
return t.threadLocals;
}
//createMap
void createMap(Thread t, T firstValue) {
//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
从上面代码可以看出每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。
get方法
//ThreadLocal中get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//ThreadLocalMap中getEntry方法
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
2. synchronized
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
个人理解:synchronized只是一个关键字,他不是一个锁,他要做的是锁住它后面的对象/代码块
二、可见性问题
1. 定义:
当一条线程修改了共享变量的值,其他线程可以立即得知这个修改。
2. 实现方式: 在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的方式实现,依赖主内存作为传输媒质。
3. 可以保证可见性的关键字:
- volatile:通过 volatile 的特殊规则;
- synchronized:通过“对一个变量执行unlock操作前,必须将该变量同步回主内存”这条规则;
- final:被 final 修饰的字段,一旦完成了初始化,其他线程就能看到它,并且它也不会再变了;
只要不可变对象被正确的构建出来(没有发生 this 引用溢出),它就是线程安全的。
4. 失去可见性的危害
用来作为状态判断的量被其他线程修改了,运行的那个线程就是看不见……
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
*/
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. 几个溢出的例子:
一个简单的溢出的例子:
class UnsafeStates {
private String[] states = new String[] {"AB", "CD"};
public String[] getStates() {
return states; // 可以通过这个方法得到states,然后就可以随便修改states,就逸出了
}
}
this引用溢出
每一个 ThreadLocal 都有一个唯一的的 ThreadLocalHashCode;
- 每一个线程中有一个专门保存这个 HashCode 的 Map
; - 当 ThreadLocal.get() 时,实际上是当前线程先拿到这个 ThreadLocal 对象的 ThreadLocalHashCode,然后通过这个 ThreadLocalHashCode 去自己内部的 Map 中去取值。
- 即每个线程对应的变量不是存储在 ThreadLocal 对象中的,而是存在当前线程对象中的,线程自己保管封存在自己内部的变量,达到线程封闭的目的。
- 也就是说,ThreadLocal 对象并不负责保存数据,它只是一个访问入口。
详见 ThreadLocal
4. 不可变对象
- 定义
- 对象创建后,其状态不能被修改
- 对象是正确创建的(无 this 引用逸出)
- 使用方法
- 因为对象是不可变的,所以多个线程可以放心大胆的同时访问
- 当这个对象中的状态需要被改变时,之间废掉当前的对象,new 一个新对象代替现在的旧对象
- final 域
final 域是我们用来构造不可变对象的一个利器,因为被它修饰的域一旦被初始化后,就是不可修改的了,不过这有一个前提,就是如果 final 修饰的是一个对象引用,必须保证这个对象也是不可变对象才行,否则 final 只能保证这个引用一直指向一个固定的对象,但这个对象自己的状态是可以改变的,所以一个所有域都是 final 的对象也不一定是不可变对象。
final的两种初始化方式
final i = 42;
final i; // 之后在每一个构造函数中给i赋值
注意:对于含有 final 域的对象,JVM 必须保证对象的初始引用在构造函数之后执行,不能乱序执行(也就是说,一旦得到了对象的引用,那么这个对象的 final 域一定是已经完成了初始化的)!
四、安全发布对象
1. 什么是安全发布对象
保证发布的对象的初始化构造过程不会受到任何其他线程干扰,就像加了锁一样,被创建它的线程构造好了,在发布给其他线程。
2. 常用发布模式
- 在静态块中初始化一个对象引用
- 将对象引用保存到 volatile 类型的域或者 AtomicReference 对象中
- 将对象引用保存到某个正确构造的对象的 final 域中
-
3. 最简单和安全的发布方式
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
- BlockQueue
- ConcurrentLinkedQueue
参考文献
[1] https://www.jianshu.com/p/3c5d7f09dfbd
[2] 让你彻底理解Synchronized https://www.jianshu.com/p/d53bf830fa09
[3] Java中Synchronized的用法 https://www.cnblogs.com/fnlingnzb-learner/p/10335662.html