本系列文章是极客时间课程的读书总结。
背景
cpu、内存、IO速度差异过大,如何均衡速度,提高系统综合性能?
- 多核,硬件并行
- 多线程,分时复用cpu,提高IO以及cpu综合利用率
- 缓存,平衡cpu与内存速度差异
- 编译器/执行器优化指令顺序,更充分利用缓存
通过以上举措,可以较大提高系统性能,但是万物皆有利弊,也带来了线程安全性问题,如果没有解决该问题,不仅不会提升性能,还会导致程序运行异常。
线程安全性定义
在多线程访问时,程序依旧表现正常,符合预期
线程安全性问题分类
线程安全性问题出现场景:多线程同时读写共享的、可变的数据。
问题主要有一下几类:
- 原子性问题
原子性操作定义:一个或多个操作在CPU执行过程中无法被中断的特性
问题原因:多线程并发
示例:count+=1;long/double类型变量读写
解决办法:临界区加锁保护,保证单线程访问,其他线程阻塞等待,保证中间状态不可见 -
可见性问题
1.png
可见性问题定义:线程A对共享数据的修改,是否能立即被线程B看到的问题
问题原因:缓存
解决办法:按需禁用缓存 - 有序性问题
问题原因:编译器/执行器为优化性能,提高对缓存的利用,会在不改变单线程执行结果的情况下调整执行顺序
解决办法:按需禁用编译器执行器优化
举例:
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
理想顺序:开辟内存->初始化->赋值引用;
优化后顺序:开辟内存->赋值引用->初始化
解决办法:instance改为volatile变量
- 举例
class Example {
int x = 0;
boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里 x 会是多少呢?
}
}
}
Java提供哪些能力解决多线程安全性问题
volatile、synchronized 和final关键字,以及六项 Happens-Before 规则
- final
生而常量,随便优化,注意逸出问题 - volatile
保证可见性、有序性,不保证原子性
volatile禁用缓存以及编译优化,强制刷内存(volatile变量本身以及之前的其他数据更新都强制刷内存);另有一条heppen规则加强volatile语义(volatile变量写happen-before读)
int a = 1;
int b = 2;
volatile int c = 3;
int d = 4;
int e = 5;
- synchronized
解决可见性、有序性、原子性问题
临界区加锁保护,保证单线程访问,其他线程阻塞等待,保证中间状态不可见
//注意组合操作,要加锁
class Account {
private int balance;
// 转账
void transfer(
Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
class Account {
private int balance;
// 转账
synchronized void transfer(
Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
//是否保证了线程安全?
等待-通知模型
2.png
wait与sleep区别在于:
- wait会释放所有锁而sleep不会释放锁资源.
- wait只能在同步方法和同步块中使用,而sleep任何地方都可以.
- wait无需捕捉异常,而sleep需要.
- sleep是Thread的方法,而wait是Object类的方法;
- sleep方法调用的时候必须指定时间
class Allocator {
private List<Object> als;
// 一次性申请所有资源
synchronized void apply(
Object from, Object to){
// 经典写法
while(als.contains(from) ||
als.contains(to)){
try{
wait();
}catch(Exception e){
}
}
als.add(from);
als.add(to);
}
// 归还资源
synchronized void free(
Object from, Object to){
als.remove(from);
als.remove(to);
notifyAll();
}
}
- 六项Happen-before原则
A happen-before B,表示A操作对于B内存可见- 顺序性规则
单线程中,前面操作对后面操作可见,特别注意,不是表面上的代码顺序,是根据内存模型约束下编译器优化后的执行顺序 - volatile 变量规则
一个 volatile 变量的写操作, Happens-Before于该变量的读操作 - 传递性
A Happens-Before B,且B Happens-Before C,则A Happens-Before C
- 顺序性规则
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里 x 会是多少呢?
}
}
}
- 锁规则
一个锁的解锁 Happens-Before 于后续对这个锁的加锁操作 - 线程 start() 规则
线程A启动线程B之后,线程B能看到之前线程A做的所有操作 - 线程 join() 规则
线程A等待线程B执行完成后,线程B的所有操作的对线程A可见
网友评论