学习使用哪种同步机制来实现线程安全
对象的发布与逸出
发布(publish) 使对象能够在当前作用域之外的代码中使用
逸出(escape) 当某个不应该发布的对象被发布了
常见逸出的有下面几种方式:
- 静态域逸出
- public修饰的get方法
- 方法参数传递
- 隐式的this
安全发布对象
-
在静态域中直接初始化 :
public static Person = new Person()
; -
- 静态初始化由JVM在类的初始化阶段就执行了,JVM内部存在着同步机制,致使这种方式我们可以安全发布对象
-
对应的引用保存到volatile或者AtomicReferance引用中
-
- 保证了该对象的引用的可见性和原子性
-
由final修饰
-
- 该对象是不可变的,那么线程就一定是安全的,所以是安全发布
-
由锁来保护
-
- 发布和使用的时候都需要加锁,这样才保证能够该对象不会逸出
解决多线程遇到的问题
java实现线程安全:
-
无状态(没有共享变量)
-
使用final使该引用变量不可变(如果该对象引用也引用了其他的对象,那么无论是发布或者使用时都需要加锁)
-
加锁(内置锁,显示Lock锁)
-
使用JDK为我们提供的类来实现线程安全(此部分的类就很多了)
-
- 原子性(就比如上面的
count++
操作,可以使用AtomicLong来实现原子性,那么在增加的时候就不会出差错了!) - 容器(ConcurrentHashMap等等…)
- ……
- 原子性(就比如上面的
-
…等等
原子性和可见性
JDK中有atomic包提供给我们实现原子性操作(AtomicLong,LongAdder,AtomicLongArray,AtomicLongFieldUpdater[属性原子修改器]等)
对于可见性,java提供了一个关键字volatile(轻量级同步机制)
volatile经典总结:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性
-
保证该变量对所有线程的可见性
-
- 在多线程的环境下:当这个变量修改时,所有的线程都会知道该变量被修改了,也就是所谓的“可见性”
-
不保证原子性
-
- 修改变量(赋值)实质上是在JVM中分了好几步,而在这几步内(从装载变量到修改),它是不安全的。
使用了volatile修饰的变量保证了三点:
- 一旦你完成写入,任何访问这个字段的线程将会得到最新的值
- 在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
- volatile可以防止重排序(重排序指的就是:程序执行的时候,CPU、编译器可能会对执行顺序做一些调整,导致执行的顺序并不是从上往下的。从而出现了一些意想不到的效果)。而如果声明了volatile,那么CPU、编译器就会知道这个变量是共享的,不会被缓存在寄存器或者其他不可见的地方。
一般来说,volatile大多用于标志位上(判断操作),满足下面的条件才应该使用volatile修饰变量:
- 修改变量时不依赖变量的当前值(因为volatile是不保证原子性的)
- 该变量不会纳入到不变性条件中(该变量是可变的)
- 在访问变量的时候不需要加锁(加锁就没必要使用volatile这种轻量级同步机制了)
线程封闭[不使用成员变量(不共享数据)]
Servlet所有的数据都是在方法(栈封闭)上操作的,每个线程都拥有自己的变量,互不干扰
线程封闭上还有另一种方法,就是ThreadLocal,这个类的API就可以保证每个线程自己独占一个变量
不变性
String就是一个不可变对象,没有遵循“对象所有的域都是final修饰的”,因为JVM在内部做了优化的。
final
final仅仅是不能修改该变量的引用,但是引用里边的数据是可以改的
final HashMap hashMap = new HashMap<>();
不可变的对象引用在使用的时候还是需要加锁的
- 或者把Person也设计成是一个线程安全的类
- 因为内部的状态是可变的,不加锁或者Person不是线程安全类,操作都是有危险的!
设计成不可变对象的要点:
- 对象创建后状态就不能修改
- 对象所有的域都是final修饰的
- 对象是正确创建的(没有this引用逸出)
线程安全性委托
很多时候我们要实现线程安全未必就需要自己加锁,自己来设计。
我们可以使用JDK给我们提供的对象来完成线程安全的设计。