一. 安全性问题
-
线程安全的本质是 正确性
,而正确性的含义是 程序按照预期执行
-
理论上 线程安全
的程序,应该要避免出现 可见性问题(CPU缓存)、原子性问题(线程切换)和有序性问题(编译优化)
-
需要分析是否存在线程安全问题的场景: 存在共享数据且数据会发生变化,即有多个线程会同时读写同一个数据
-
针对该理论的解决方案:不共享数据,采用 线程本地存储
(Thread Local Storage,TLS); 不变模式
Ⅰ. 数据竞争
数据竞争(Data Race):多个线程 同时访问
同一数据,并且 至少有一个
线程会写这个数据
1. add
private static final int MAX_COUNT = 1_000_000;
private long count = 0;
// 非线程安全
public void add() {
int index = 0;
while (++index < MAX_COUNT) {
count += 1;
}
}
2. add + synchronized
private static final int MAX_COUNT = 1_000_000;
private long count = 0;
public synchronized long getCount() {
return count;
}
public synchronized void setCount(long count) {
this.count = count;
}
// 非线程安全
public void add() {
int index = 0;
while (++index < MAX_COUNT) {
setCount(getCount() + 1);
}
}
- 假设count=0,当两个线程同时执行getCount(),都会返回0
- 两个线程执行getCount()+1,结果都是1,最终写入内存是1,不符合预期,这种情况为竟态条件
Ⅱ. 竟态条件
- 竟态条件(Race Condition):程序的执行结果依赖于线程执行的顺序
-
在并发环境里,线程的执行顺序是不确定的
- 如果程序存在竟态条件问题,那么意味着程序的执行结果是不确定的
1. 转账
public class Account {
private int balance;
// 非线程安全,存在竟态条件,可能会超额转出
public void transfer(Account target, int amt) {
if (balance > amt) {
balance -= amt;
target.balance += amt;
}
}
}
Ⅲ. 解决方案
面对数据竞争和竟态条件问题,可以通过互斥的方案来实现线程安全,互斥的方案可以统一归为锁
二. 活跃性问题
活跃性问题: 某个操作无法执行下去
,包括三种情况: 死锁、活锁、饥饿
Ⅰ. 死锁
-
发生死锁后线程会 相互等待
,表现为线程 永久阻塞
-
解决死锁问题的方法是 规避死锁
(破坏发生死锁的条件之一)
-
互斥
:不可破坏,锁定目的就是为了互斥
-
占有且等待
:一次性申请 所有
需要的资源
-
不可抢占
:当线程持有资源A,并尝试持有资源B时失败,线程 主动释放
资源A
-
循环等待
:将资源编号 排序
,线程申请资源时按 递增
(或递减)的顺序申请
Ⅱ. 活锁
-
活锁
:线程并没有发生阻塞,但由于相互谦让,而导致执行不下去
-
解决方案
:在谦让时,尝试等待一个随机时间(分布式一致算法Raft也有采用)
Ⅲ. 饥饿
-
饥饿:线程因无法访问所需资源而无法执行下去
- 线程的优先级是不相同的,在CPU繁忙的情况下,优先级低的线程得到执行的机会很少,可能发生线程饥饿
- 持有锁的线程,如果执行的时间过长(持有的资源不释放),也有可能导致饥饿问题
-
解决方案
- 保证资源充足
- 公平地分配资源(公平锁) – 比较可行
- 避免持有锁的线程长时间执行
三. 性能问题
-
锁的 过度使用
可能会导致 串行化的范围过大
,这会影响多线程优势的发挥(并发程序的目的就是为了 提升性能
)
-
尽量减少串行
,假设 串行百分比
为5%,那么 多核多线程
相对于 单核单线程
的提升公式(Amdahl定律)
S=1/((1-p)+p/n),n为CPU核数,p为并行百分比,(1-p)为串行百分比
- 假如p=95%,n无穷大,加速比S的极限为20,即无论采用什么技术,最高只能提高20倍的性能
Ⅰ. 解决方案
-
无锁算法和数据结构
- 线程本地存储(Thread Local Storage,TLS)
- 写入时复制(Copy-on-write)
- 乐观锁
- JUC中的原子类
- Disruptor(无锁的内存队列)
-
减少锁持有的时间,互斥锁的本质是将并行的程序串行化,要增加并行度,一定要减少持有锁的时间
- 使用细粒度锁,例如JUC中的ConcurrentHashMap(分段锁)
- 使用读写锁,即读是无锁的,只有写才会互斥的
Ⅱ. 性能指标
-
吞吐量
:在单位时间内能处理的请求数量,吞吐量越高,说明性能越好
-
延迟
:从发出请求到收到响应的时间,延迟越小,说明性能越好
-
并发量
:能同时处理的请求数量,一般来说随着并发量的增加,延迟也会增加,所以延迟一般是基于并发量来说的
写在最后
-
第一:看完点赞,感谢您的认可;
-
...
-
第二:随手转发,分享知识,让更多人学习到;
-
...
-
第三:记得点关注,每天更新的!!!
-
...
最后,欢迎做Java的工程师朋友们加入Java高级架构进阶Qqun:963944895
群内有技术大咖指点难题,还提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)
比你优秀的对手在学习,你的仇人在磨刀,你的闺蜜在减肥,隔壁老王在练腰, 我们必须不断学习,否则我们将被学习者超越!
趁年轻,使劲拼,给未来的自己一个交代!
原文
https://segmentfault.com/a/1190000019680447