上文介绍了线程安全性问题相关知识,但解决了安全性问题并不意味着不需要关注其他方面,如果不加注意还会有活跃性及性能问题。
活跃性问题
死锁
- 原因:互相抢夺资源,形成死循环
- 出现条件:互斥;占有且等待;不可抢占已有资源;循环等待
- 现象:应用无响应,但是CPU占用低
- 定位手段:
top查看未响应进程状态,此时cpu利用率低;
top -pH 进程ID查看线程状态;
jstack或者gdb 查看线程堆栈,找到死锁线程;
结合代码逻辑找到bug - 解决办法:重启应用
- 如何避免死锁
- 资源一次性申请
while(!actr.apply(this, target))
- 破坏不可抢占条件
并发包里的Lock(ReentrantLock)解决该问题
- 资源一次性申请
// 支持中断的 API
void lockInterruptibly() throws InterruptedException;
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();
- 破坏循环等待条件
class Account {
private int id;
private int balance;
// 转账
void transfer(Account target, int amt){
Account left = this ①
Account right = target; ②
if (this.id > target.id) { ③
left = target; ④
right = this; ⑤
} ⑥
// 锁定序号小的账户
synchronized(left){
// 锁定序号大的账户
synchronized(right){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
活锁
线程虽然没有发生阻塞,但仍然会存在执行不下去的情况
class Account {
private int balance;
private final Lock lock
= new ReentrantLock();
// 转账
void transfer(Account tar, int amt){
while (true) {
if(this.lock.tryLock()) {
try {
if (tar.lock.tryLock()) {
try {
this.balance -= amt;
tar.balance += amt;
} finally {
tar.lock.unlock();
}
}//if
} finally {
this.lock.unlock();
}
}//if
}//while
}//transfer
}
饥饿
一直得不到执行,其他线程占用锁太长,线程优先级太低
性能问题
延迟、吞吐量、并发量
- 减少锁范围
- 减少锁颗粒度
- 读写锁ReadWriteLock
适合读多写少
class Cache<K,V> {
final Map<K, V> m =
new HashMap<>();
final ReadWriteLock rwl =
new ReentrantReadWriteLock();
final Lock r = rwl.readLock();
final Lock w = rwl.writeLock();
V get(K key) {
V v = null;
// 读缓存
r.lock(); ①
try {
v = m.get(key); ②
} finally{
r.unlock(); ③
}
// 缓存中存在,返回
if(v != null) { ④
return v;
}
// 缓存中不存在,查询数据库
w.lock(); ⑤
try {
// 再次验证
// 其他线程可能已经查询过数据库
v = m.get(key); ⑥
if(v == null){ ⑦
// 查询数据库
v= 省略代码无数
m.put(key, v);
}
} finally{
w.unlock();
}
return v;
}
}
-
无锁结构
1)copy-on-write
比如CopyOnWriteArrayList:
image.png
缺点:浪费内存,增大垃圾回收压力;有很短时间的数据不一致性
不建议在数据较大或者更新频繁的场景应用
2) 原子类
缺点:浪费CPU,适合写冲突不大的场景
class SimulatedCAS{
volatile int count;
// 实现 count+=1
addOne(){
do {
newValue = count+1; //①
}while(count !=
cas(count,newValue) //②
}
// 模拟实现 CAS,仅用来帮助理解
synchronized int cas(
int expect, int newValue){
// 读目前 count 的值
int curValue = count;
// 比较目前 count 值是否 == 期望值
if(curValue == expect){
// 如果是,则更新 count 的值
count= newValue;
}
// 返回写入前的值
return curValue;
}
}
3)乐观读
适合读多写少的场景
final StampedLock sl =
new StampedLock();
// 乐观读
long stamp =
sl.tryOptimisticRead();
// 读入方法局部变量
......
// 校验 stamp
if (!sl.validate(stamp)){
// 升级为悲观读锁
stamp = sl.readLock();
try {
// 读入方法局部变量
.....
} finally {
// 释放悲观读锁
sl.unlockRead(stamp);
}
}
// 使用方法局部变量执行业务操作
......
写模板:
long stamp = sl.writeLock();
try {
// 写共享变量
......
} finally {
sl.unlockWrite(stamp);
}
-
合理控制线程数
原因:线程数过多会造成上下文切换过多,浪费cpu,每个线程开辟栈空间,也浪费内存
IO密集型应用可加大线程数,CPU密集型应用不宜用过多线程
image.png
最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 /CPU 耗时)]
尽量不随意开线程,使用线程池
网友评论