Java并发编程学习笔记——理论基础
并发的三个核心问题
- 分工
- 同步
- 互斥
微观层面
CPU增加缓存,以均衡与内存的速度差异。
缓存带来可见性问题
可见性:一个线程对共享变量的修改,另外一个线程能够立即看到操作系统增加进程线程,分时复用CPU,均衡与IO设备的速度差异。
线程切换带来原子性问题
原子性:一个或多个操作在CPU执行过程中不被中断的特性例如:count += 1; 32位机器对long型变量 都不是原子性操作
编译器优化指令执行次序,使缓存更加合理地利用。
编译优化带来有序性问题例如: 双重检查创建单例对象
宏观层面
安全性 => 互斥、锁
数据竞争:多个线程访问未加锁的共享资源
竟态条件:程序的执行结果依赖线程的执行顺序活跃性 => 资源公平分配,公平锁,等待队列
死锁,活锁,饥饿性能问题
使用无锁的数据结构和算法,减少持有锁的时间指标
吞吐量:指单位时间内能处理的请求数量
延时:指发出请求到收到响应的时间
并发量:指同时能处理的请求数量
Java内存模型对可见性和有序性的处理
Happens-Before规则
程序次序规则
在一个线程中,按照程序的顺序,前面的操作happens-before于后续的操作volatile变量规则
对volatile变量的写操作happens-before于后续对该变量的读操作 (一个线程写volatile变量后,其他线程总是可见的)管程锁定规则
对一个锁的解锁操作happens-before于后续对这个锁的加锁操作 (一个线程在锁中的操作,解锁后其他线程都是可见的)线程启动规则
线程A调用线程B的start方法时,线程B能够看到线程A在启动线程B前的操作线程终止规则
线程A得到线程B完成(调用线程B的join方法),当线程B完成返回后,线程A能够看到线程B的操作(对于共享变量而言)Happens-Before是有传递性的
实现方式: 内存屏障(Memory Barrier);对编译器而言,内存屏障会限制指令重排序;对处理器而言,内存屏障会使缓存刷新
锁对原子性问题的处理
加锁的本质是在锁对象的对象头中写入当前线程id
原子性的本质:多个资源间有一致性要求,操作的中间状态对外不可见
1 | class SafeCals { |
解决方法:get方法加锁synchronized,或value变量加volatile关键字
1 | // 加的是SafeCals.class的锁,加不同的锁也无法保证可见性 |
1 | // JVM逃逸分析后,sync代码会被优化掉 |
锁和受保护资源的关系
受保护资源和锁的关系应该是N:1 (不能多把锁保护一个资源)
1 | class Account { |
解决方法:创建Acount是传入同一个Object对象作为锁, 或者使用Account.class作为锁(但有性能问题,所有账户的转账transfer操作都会变成串行的)
不能用this.balance这类可变对象作为锁,例如Integer,String,Boolean
死锁的处理方式
1 | void transfer(Account target, int amt) { |
当以下四个条件同时满足时,才会发生死锁
- 互斥: 共享资源X和Y只能被一个线程占有 (目的)
- 占有且等待: 线程占有X在等待Y时,不会释放X
- 不可抢占: 其他线程不能抢占线程占有的资源
- 循环等待: 两个线程相互等待对方占有的资源
破坏占有且等待条件
一次申请所有资源1
2
3
4
5
6
7
8
9
10
11
12
13
14// 创建一个单例的类,负责一次申请所有资源
class Allocator {
private List<Object> als = new ArrayList<>(); // 存放表示已申请到的资源
synchronzied boolean apply(Object from, Object to) {
// list中有对象,表明锁已经被其他线程申请了
if (als.contain(from) || als.contain(to))
return false;
else {
als.add(from);
als.add(to);
}
return true;
}
}
破坏不可抢占条件
要求获取资源的线程能主动解锁 (synchronzied原语做不到主动释放资源)
破坏循环等待条件
对资源进行排序,申请资源时按顺序申请 (相对来说成本小)
等待-通知机制
1 | while(!allocator.apply(from, to)); // 不停的请求锁 |
管程
管程:管理共享变量以及对共享变量的操作过程,让其支持并发
管程对互斥的处理方式
将共享变量及其操作封装起来(类似Java),同一时间只允许一个线程进入管程执行
管程对同步的处理方式
同步意味着有条件控制
Hasen/Hoare/MESA三种管程模型的核心区别: 当条件满足后,通知线程的方式不同 (假如当线程T2使线程T1等待的条件满足,线程T1和T2如何执行)
- Hasen: 要求notify方法放在最后。T2通知完T1后,T2结束,T1执行
- Hoare:T2通知完T1后,T2阻塞,T1立马执行,T1执行完再唤醒T2
- 多了阻塞操作,本质是中断当前线程
MESA:T2通知完T1后,T2接着执行,T1从条件变量等待队列进入到入口等待队列
- 好处是notify不用放在最后,也无阻塞操作
副作用是当T1执行时,条件有可能变化,因此需要轮询执行条件
1
2
3
4// MESA管程模型的编程范式
while(条件不满足) {
wait();
}
Java内置的管程synchronized对MESA模型进行了精简,只有一个条件变量。
wati()方法只有MESA模型有超时时间的参数。因为notify后,是将等待的线程放入入口等待队列,不一定有机会执行,所以要设超时时间。Hasen/Hoare模型都是notify后,等待的线程肯定能执行到。
1 | Thread th = Thread.currentThread(); |
修改方式:
- 在catch中重置中断标志, Thread.currentThread().interrupt();
- try包在while循环外
- 在catch中break;
逸出
- 静态方式若操作了静态变量就会有线程安全问题
- 尽管方法内部考虑了线程安全,但方法的参数是引用类型时,也会产生线程安全问题