并发基础
线程安全性即当多个线程访问某个类时, 这个类始终能表现出正确的行为.
编写线程安全的代码, 核心是对状态访问进行管理, 特别是对可变得和共享的状态访问, 状态存储在实例或静态与中的数据, 状态可能包括其他依赖对象的域, 如HashMap的Map.Entry
并发编程中, 由于不恰当的执行时序二出现不正确结果现象称之为竟态条件.
# 保证线程安全性
- 使用线程安全类
使用AtomicLong, AtomicReference, ConcurrentHashMap等线程安全类.
- 适当的加锁
每个java对象都可以用作一个实现同步的锁. 使用同步代码块时,代码快的作用范围应当尽量小, 避免出现活跃性问题. 尽量不要使用不同的加锁机制, 不仅会带来混乱, 也不会在性能和安全性上带来好处.
内置锁是可以重入的, 重入意味着获取锁的操作的粒度是线程而不是调用.
若使用同步来协调对某个变量的访问, 那么在访问这个变量的所有位置都要加锁, 且使用同一个锁.
可以将所有可变状态都封装到一个对象中, 然后使用这个对象的内置锁来对访问可变状态的代码路径进行同步.
即使每个方法都作为同步方法使用同一把锁, 但如果出现复合操作仍然需要额外的加锁机制.
- 避免多个线程同一时间访问相同数据
# 控制对象的共享
# 对象的发布与逸出
发布指使对象能够在当前作用域之外的代码中使用. 当某个不该发布的对象被发布时, 这种情况称之为逸出.
发布对象的方式:
- 将对象的引用保存在一个拥有的静态变量中
- 当发布一个对象时, 在这个对象的非私有域(非private修饰)中引用的所有对象也会被发布.
- 发布一个内部的类实例.
安全发布对象
- 将对象的引用保存到volatile类型的域或者AtomicReference中
- 将对象的引用保存到某个正确构造对象的final域中.
- 将对象的引用保存到一个由锁保护的域中.
- 在静态初始化函数中初始化一个对象引用, 由JVM内部的同步机制来保证安全发布.
不安全发布
this引用在构造方法中逸出. 当且仅当对象的构造方法返回时, 对象才处于可预测的和一致的状态
public class A{
public A(){
...
B.doSomething(this);
...
}
}
2
3
4
5
6
7
在构造方法中启动一个线程, 当对象在其构造方法中创建一个线程时, 无论是显示创建还是隐式创建, this引用都会被新创建的线程共享
public class A {
public A(){
...
new Thread(() -> {...}).start().
...
}
}
2
3
4
5
6
7
8
# 线程封闭
- 数据只在单个线程内访问.
- 局部变量(栈封闭), ThreadLocal.
- volatile的特殊线程封闭, 只要能够保证只有一条线程对volatile执行写入操作, 就可以安全的在这些共享的volatile变量上执行读写
# 对象的不变性
- 不可变对象一定是线程安全的, 它只有一种状态, 并且由构造方法控制.
- 不可变对象创建后其状态不能再修改, 所有域都是final的.
- 当需要一组相关数据以原子方式来执行某个操作时, 就可以考虑创建一个不可变类来包含这些数据
- 对象创建时, this引用没有逸出.
# 对象的组合
- 设计线程安全类
设计线程安全类的基本要素: 找出构造对象状态的所有变量, 找出约束状态变量的不变性条件. 建立对象状态的并发访问策略. 并发访问策略规定了如何将不变性, 线程封闭和加锁机制组合起来维护线程安全性, 还规定了哪些变量需要加锁保护
- 实例封闭
将数据封装到对象内部, 将数据的访问限制在对象方法.