一、介绍
1.1 如何写出线程安全的代码
- 核心在于要对
状态访问操作进行管理。 - 特别是对
共享的(Shared)和可变的(Mutable)状态的访问。
1.2 共享、可变的含义
共享:意味着可以由多个线程同时访问可变:意味着变量的值在其生命周期内可以发生变化。
1.3 对象是否需要是线程安全的?
- 取决于是否被多个线程访问。
- 这里是指
在程序中访问对象的方式,而不是对象要实现的功能。 - 如果要使得对象是线程安全的,需要采用同步机制,
协同线程对对象可变状态的访问。
1.4 如何协同线程对对象可变状态的访问?
- 关键字
synchronized volatile类型的变量- 显示锁 (explicit lock)
- 原子变量 (juc包):实现在数值和对象引用上的原子状态转换。
AtomicBooleanAtomicLongAtomicIntegerAtomicReference
1.5 如何修复多线程访问同一个可变状态变量的问题?
前提条件:假设这个问题是由于 未使用合适的同步造成的
解决:
不在线程之间共享该状态变量。- 将状态变量修改为
不可变的变量。 - 在访问状态变量时使用
同步。
1.6 如何设计一个线程安全的类?
- 良好的面向对象技术
- 不可修改性
- 明晰的不变性规范
二、什么是线程安全性
2.1 定义
- 当多个线程访问某个类时,这个类始终都能
表现出正确的行为,那么就称这个类是线程安全的。 - 不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为。
2.2 正确性的定义
- 某个类的行为与其规范完全一致。
- 在良好的规范中通常会定义各种
不变性条件(Invarient)来约束对象的状态,以及定义各种后验条件(Postcondition)来描述对象操作的结果。
2.3 无状态对象一定是线程安全的。
三、原子性
3.1 竞态条件(Race Condition)
- 定义
- 由于
不恰当的执行时序而出现的不正确的结果。
- 由于
- 本质:
- 基于一种可能失效的观察结果来做判断或者执行某个计算,从而导致各种问题。
- 常见的竞态条件
先检查后执行:通过一个可能失效的观测结果来决定下一步的动作。读取-修改-写入:基于对象之前的状态来定义对象状态的转换。
3.2 复合操作
- 以上两种竞态条件均涉及复合操作。
- 包含了一组必须以
原子方式执行的操作以确保线程安全性。
3.3 原子操作
- 对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。
四、加锁机制
4.1 内置锁
- 同步代码块(
Synchronized Block):Java提供的一种内置的锁机制来支持原子性。 - 同步代码块的组成:两部分
锁的对象引用锁保护的代码块
- 同步代码块语法格式:
synchronized (lock) {
// 访问/修改由锁保护的共享状态
}
static synchronized方法以Class对象作为锁。内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock)- 每个Java对象都可以用做一个实现同步的锁,这些锁被称为 内置锁 或者 监视器锁。
- 线程在进入同步代码块之前会自动获得锁,并在退出同步代码块之后释放锁。
- 每次只能有一个线程执行内置锁保护的代码块。
- 内置锁是
可重入锁。- 重入意味着
获取锁操作的粒度是线程,而不是调用。
- 重入意味着
五、用锁来保护状态
- 注意点
- 对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁。这种情况下我们称状态变量是由这个锁保护的。
六、demo
6.1 需求说明
- 前提:假设我们要获取去用户请求的数量,然后进行自增的操作。
- 要求:考虑多线程执行,使得
main方法输出正确的结果。 - 基本的类定义如下:
public class UserReq {
private long count;
public long getCount() {
return count;
}
public void addCount() {
// 为了看出计数效果,这里在方法内部循环1000次
for (int i = 0; i < 1000; i++) {
count++;
}
}
}
- 测试方法:使用
main方法执行,方法如下。
public class UserReqTest {
public static void main(String[] args) throws InterruptedException {
int loopNum = 1000;
UserReq userReq = new UserReq();
for (int i = 0; i < loopNum; i++) {
new Thread(userReq::addCount).start();
}
// 主线程sleep一会,等待全部子线程执行完毕。
Thread.sleep(1000 * 5);
System.out.println(userReq.getCount());
// output:999103。这里每次执行输出都不太一样,但是不是 1000 * 1000
}
}
- 说明:
UserReq中计数方法count++是一个复合操作,必须以原子方式执行才能保证线程安全。
6.2 改进一:使用 synchronized 关键字
- 改进方法:
- 在
UserReq类中方法addCount上添加synchronized关键字。 - 该同步代码块的锁就是方法调用时的对象
userReq(main方法中创建的)。
- 在
- 改进代码:忽略
- 问题:性能问题
6.2 改进:使用原子变量来计数
- 改进方法:
- 在
UserReq类中使用原子变量AtomicLong类型的变量代替 基础数据类型long。
- 在
- 改进代码:
public class UserReq {
private final AtomicLong count = new AtomicLong(0);
public long getCount() {
return count.get();
}
public void addCount() {
// 为了看出计数效果,这里在方法内部循环1000次
for (int i = 0; i < 1000; i++) {
count.incrementAndGet();
}
}
}
// main 方法输出:1000000
- 思考:
- 这里只有一个变量需要使用原子变量来协同状态变化,如果有
多个共享的可变变量,只通过增加原子变量能满足线程安全要求吗?
要保持状态的一致性,就需要
在单个原子操作中更新所有相关的状态变量。- 主线程无需使用
Thread.sleep(1000 * 5)的操作等待线程执行完成;可以使用二元闭锁,参考 【Ch5-基础构建模块】
- 这里只有一个变量需要使用原子变量来协同状态变化,如果有