07 | 安全性、活跃性以及性能问题
本节课总结:
安全性:
编写并发程序的初衷是为了提升性能,但在追求性能的同时由于多线程操作共享资源而出现了安全性问题,所以才用到了锁技术,一旦用到了锁技术就会出现了死锁,活锁等活跃性问题,而且不恰当的使用锁,导致了串行百分比的增加,由此又产生了性能问题,所以这就是并发程序与锁的因果关系。
- 数据竞争: 多个线程同时访问一个数据,并且至少有一个线程会写这个数据。
竞态条件: 程序的执行结果依赖程序执行的顺序。
也可以按照以下的方式理解竞态条件: 程序的执行依赖于某个状态变量,在判断满足条件的时候执行,但是在执行时其他变量同时修改了状态变量。也就是不能保证原子性if (状态变量 满足 执行条件) { 执行操作 }
问题: 数据竞争一定会导致程序存在竞态条件吗?有没有什么相关性?
public class Test {
private long count = 0;
synchronized long get(){
return count;
}
synchronized void set(long v){
count = v;
}
//这里是不同步的,虽然add和set是同步的,但是放在一起不能保证原子性
void add10K() {
int idx = 0;
while(idx++ < 10000) {
set(get()+1);
}
}
}
get虽然有锁,只能保证多个线程不能同一时刻执行。但是出现不安全的可能是线程a调用get后线程b调用get,这时两个get返回的值是一样的。然后都加一后再分别set.这样两个线程就出现并发问题了。问题在于同时执行get,而在于get和set是两个方法,这两个方法组合不是原子的,就可能两个方法中间的时间也有其它线程分别调用,出现并发问题
关于add10()的理解是这样的,第一个线程拿到了count等于1,还没执行set操作的时候,第二个线程拿到了count等于1,这两个线程都将count等于写入到各自的寄存器里面,这时候发生了任务切换,因为不是原子的,所以会导致最后写到内存的count等于1而不是想要的2
class Account {
private int balance;
// 转账
void transfer(
Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
转账这个例子如果不加措施,因为可见性,两个线程彼此看不见对方,初始值都>150 然后都减150变成50,或者因为任务切换 切换到另一个线程减了150也是50
会存在一个线程减到50,另一个线程从50开始减嘛?也就是一个减操作结束后刷到内存,另一个做减法的时候又从内存读值
购买书籍 《Java并发编程的艺术》
另外想到SpringFrameWork中各种模板类都是线程安全的,模板类访问数据不同的持久化技术要绑定不同的会话资源,这些资源本身不是线程安全的。多线程环境下使用synchronized会降低并发,正是使用了ThreadLocal杜绝了不用线程同步情况下解决了线程安全的问题。
活跃性:
死锁:破坏造成死锁的条件,
- 1,使用等待-通知机制的Allocator;
- 2主动释放占有的资源;
- 3,按顺序获取资源。
- 活锁:虽然没有发生阻塞,但仍会存在执行不下去的情况。我感觉像进入了某种怪圈。解决办法,等待随机的时间,例如Raft算法中重新选举leader。
- 饥饿:我想到了没有引入时间片概念时,cpu处理作业。如果遇到长作业,会导致短作业饥饿。如果优先处理短作业,则会饿死长作业。长作业就可以类比持有锁的时间过长,而时间片可以让cpu资源公平地分配给各个作业。当然,如果有无穷多的cpu,就可以让每个作业得以执行,就不存在饥饿了。
互斥锁本质上是将并行的程序串行化
性能:
核心就是在保证安全性和活跃性的前提下,根据实际情况,尽量降低锁的粒度。即尽量减少持有锁的时间。JDK的并发包里,有很多特定场景针对并发性能的设计。还有很多无锁化的设计,例如MVCC,TLS,COW等,可以根据不同的场景选用不同的数据结构或设计。
最后,在程序设计时,要从宏观出发,也就是关注安全性,活跃性和性能。遇到问题的时候,可以从微观去分析,让看似诡异的bug无所遁形。
评论已关闭