线程安全理论
# 线程安全理论
# 如何编写线程安全的代码 🔥
核心在于对共享的、可变的状态(成员变量,包括静态的)访问操作进行管理
- 共享:多个线程同时访问
- 可变:变量的值在其生命周期内可变
- 访问操作:只有在一或多个线程向这些资源做了写操作时才有可能发生
- 管理:使用同步机制协同线程对变量的访问。可使用如:synchronized、volatile、Lock、原子变量等
注意:无状态对象一定是线程安全的(没有管理的主体了,当然线程安全)。如:大多数 Servlet 等。
# 原子性 🔥
# 竞态条件
多个线程以非互斥的方式进入临界区,当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件
常见的竞态条件类型:
- 先检查后执行(Check-Then-Act),即基于一个可能失效的观察结果来作出判断或执行某个计算。
如:延迟初始化
@NotThreadSafe public class LazyInitRace { private ExpensiveObject instance = null; public ExpensiveObject getInstance() { if(instance == null) { instance = new ExpensiveObject(); } return instance; } }1
2
3
4
5
6
7
8
9
10 - 读取-修改-写入:基于对象之前的状态来定义对象状态的转换。
如:递增运算,
i++
# 复合操作 VS 原子操作
常见的竞态条件类型其原因在于复合操作,非原子操作!
- 当在无状态的类(本身绝对线程安全)中添加一个状态时,如果该状态由线程安全的对象管理,那么这个类还是线程安全的
- 但是,当添加多个状态时,就比较复杂了
# 加锁机制—互斥锁—原子性 🔥
# Monitor(监视器、管程)
- 也称作Intrinsic Lock,内置锁。Java 中可以使用同步代码块机制实现Monitor,支持原子性、可见性、有序性。
- 每个Java对象都可以做一个实现同步的锁。线程在进入同步代码块之前自动获得锁,并在退出同步代码块时自动释放锁(无论通过正常控制路径退出,还是通过代码中抛出异常退出)
- 获得锁的唯一途径就是进入由这个锁保护的同步代码块或方法
# 可重入
- 当某个线程请求由其他线程持有的锁时,发出请求的线程会阻塞。但是由于Monitor是可重入的,所以如果某个线程视图获取一个已经由它自己持有的锁时,该请求就会成功
- 重入意味着获取锁的操作粒度是线程,而不是调用
重入锁一种实现方式:
- 为每个锁关联一个获取计数值、一个所有者线程。
- 当计数值为0,则该锁没有被任何线程持有
- 当一个线程请求一个未被持有的锁时,JVM将记录下锁的持有者线程、计数值置为1 同一个线程再次获取锁,则计数值递增。当线程退出同步代码时,计数值递减
- 当计数值为0,则该锁被释放
# 活跃性 & 性能
常见的==错误==:==只有在写入共享变量时才需要使用同步锁==!!!
当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络IO或控制台IO),一定不要持有锁!!!
# 对象共享—可见性 🔥
# 介绍 🔥
- 也叫内存可见性(Memory Visibility)
- 我们希望:
- 原子性:防止某个线程正在使用对象状态,而另一个线程在同时修改状态!
- 可见性:确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化!
# 问题 🔥
- 单线程中,当读操作和写操作在不同的线程中执行时,我们无法确保执行读操作的线程能够实时的看到其他线程写入的值
- 为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制
- 在没有同步的情况下,处理器、编译器、运行时都可能对操作的执行顺序进行调整!!!该操作能使JVM充分利用现代多核处理器的强大性能。如:
- 未使用同步时,JMM允许编译器对操作进行重排序,将数值缓存在寄存器中。
- 未使用同步时,JMM允许CPU对操作进行重排序,将数值缓存在处理器特定的缓存中。
# 非原子的64位操作 🔥
- 当线程在没有同步的情况下读取变量时,可能会得到一个失效值(之前设置的,非随机值),这种称为最低安全性。
- JMM要求变量的读取和写入操作都必须是原子操作!
- 但是对于非volatile类型的64位数值变量(double和long),JVM允许将64位读或写操作分解为2个32位的操作(主要原因在于编写Java虚拟机规范时,许多主流处理器架构还不能有效地提供64位数值的原子操作)
# 锁与原子性、可见性、有序性 🔥
- 管程锁定规则:一个unlock操作Happend Before对同一个锁的lock操作。
- 即一个线程unlock前的操作,另一个lock线程都可以看到
# volatile 与可见性、有序性🔥
稍弱的同步机制,用于确保将变量的更新操作通知到其他线程!
变量声明为volatile后,编译器和运行时会注意到这个变量是共享的
- volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方
- 不会将该变量上的操作与其他内存操作一起重排序
不建议过度依赖volatile的可见性!!!
仅当volatile变量能简化代码的实现以及对同步策略的验证时,才该使用!如果在验证正确性时需要对可见性进行复杂的判断,则不要使用
正确使用方式:
确保它们自身状态的可见性
确保它们所引用对象的状态的可见性
标识一些重要的程序生命周期事件,如初始化、关闭
检查某个标记以判断是否退出循环
volatile boolean asleep; // ... while(!asleep){ doSomeThing(); }1
2
3
4
5
当且仅当满足以下所有条件时,才应该使用volatile变量:
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
- 该变量不会与其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
调试技巧:
对与服务器应用程序,无论是开发、测试阶段,启动JVM时一定要指定-server命令行选项,比-client模式,JVM将进行更多优化。如:将循环中未被修改的变量提升到循环外部,因此在-client中可以执行的代码,在-server可能运行失败。
# 对象共享—发布与逸出
# 简介
- 发布:使对象能够在当前作用域之外的代码中使用。如:
- 将对象的引用保存到一个公有的静态变量中,以便任何类和线程都能看到该对象
- 逸出:某个不应该发布的对象被发布时的情况
# 构造函数中逸出
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
void doSomething(Event e) {
}
interface EventSource {
void registerListener(EventListener e);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ThisEscape 构造函数中doSomething其实是this.doSomething,在还未构造完时,即发布了一个尚未构造完成的对象!即使发布对象的语句位于构造函数最后一行!不要在构造函数中使this引用逸出!!!
只有当构造函数返回时(执行完毕),this引用才应该从线程中逸出!
采用工厂方法改造后的:
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
}
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
void doSomething(Event e) {
}
interface EventSource {
void registerListener(EventListener e);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 避免同步—对象不共享、线程封闭
# 简介
访问共享的可变数据时,通常需要使用同步,一种避免使用同步的方式就是不共享数据!
如果仅在单线程内访问数据,就不需要同步,该技术称为线程封闭!它是实现线程安全性最简单方法之一!
如JDBC规范并不要求Connection对象必须是线程安全的,但是在服务器应用中,由于大多数请求(如Servlet)都是由单个线程采用同步的方式来处理,并且在Connection对象返回之前,连接池都不会把它分配给其他线程。因此这种管理模式隐含地将Connection对象封闭在线程中。
# 栈封闭—局部变量 🔥
在封闭栈中,只能通过局部变量才能访问对象,因此线程间不存在对象共享
# ThreadLocal 🔥
ThreadLocal能使线程中的某个值与保存值的对象关联起来。常用于防止对可变的单实例变量或全局变量进行共享!
public class ConnectionDispenser {
static String DB_URL = "jdbc:mysql://localhost/mydatabase";
private ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
try {
return DriverManager.getConnection(DB_URL);
} catch (SQLException e) {
throw new RuntimeException("Unable to acquire Connection, e");
}
};
};
public Connection getConnection() {
return connectionHolder.get();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 满足同步—不变性
线程安全性是不可变对象的固有属性之一!即不可变对象一定是线程安全的!
但是即使对象中所有的域都是final类型的,这个对象也可能是可变的!不变对象的域中保存的是可变对象的引用!
例如使用 volatile 发布不可变对象(对象的封装可以更好更方便保障原子性)
/**
* OneValueCache
* <p/>
* Immutable holder for caching a number and its factors
*
* @author Brian Goetz and Tim Peierls
*/
@Immutable
public class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache(BigInteger i,
BigInteger[] factors) {
lastNumber = i;
lastFactors = Arrays.copyOf(factors, factors.length);
}
public BigInteger[] getFactors(BigInteger i) {
if (lastNumber == null || !lastNumber.equals(i))
return null;
else
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* VolatileCachedFactorizer
* <p/>
* Caching the last result using a volatile reference to an immutable holder object
*
* @author Brian Goetz and Tim Peierls
*/
@ThreadSafe
public class VolatileCachedFactorizer extends GenericServlet implements Servlet {
private volatile OneValueCache cache = new OneValueCache(null, null);
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
}
void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[]{i};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 安全发布
如何保障使用对象的线程能够看到该对象处于已发布状态?则对象的引用和对象的状态必须都对其他线程可见!常用模式:
静态初始化函数中初始化一个对象的引用
静态初始化函数由JVM在类初始化阶段执行,由于JVM内部的同步机制,该初始化方法的对象必定被安全发布!
public static Holder holder = new Holder(1);1将对象的引用保存到volatile类型的域或AtomicReferance对象中
将对象的引用保存到某个正确构造对象的final类型域中。如上面的不变性
将对象的引用保存到一个由锁保护的域中