二、活跃性及性能问题

上文介绍了线程安全性问题相关知识,但解决了安全性问题并不意味着不需要关注其他方面,如果不加注意还会有活跃性及性能问题。

活跃性问题

死锁

  • 原因:互相抢夺资源,形成死循环
  • 出现条件:互斥;占有且等待;不可抢占已有资源;循环等待
  • 现象:应用无响应,但是CPU占用低
  • 定位手段:
    top查看未响应进程状态,此时cpu利用率低;
    top -pH 进程ID查看线程状态;
    jstack或者gdb 查看线程堆栈,找到死锁线程;
    结合代码逻辑找到bug
  • 解决办法:重启应用
  • 如何避免死锁
    1. 资源一次性申请
      while(!actr.apply(this, target))
    2. 破坏不可抢占条件
      并发包里的Lock(ReentrantLock)解决该问题
// 支持中断的 API
void lockInterruptibly()  throws InterruptedException;
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit)  throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();
  1. 破坏循环等待条件
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
}

饥饿

一直得不到执行,其他线程占用锁太长,线程优先级太低

性能问题

延迟、吞吐量、并发量

  1. 减少锁范围
  2. 减少锁颗粒度
  3. 读写锁ReadWriteLock
    适合读多写少
class Cache {
  final Map 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. 无锁结构
    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);
}
  1. 合理控制线程数
    原因:线程数过多会造成上下文切换过多,浪费cpu,每个线程开辟栈空间,也浪费内存
    IO密集型应用可加大线程数,CPU密集型应用不宜用过多线程


    image.png

    最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 /CPU 耗时)]
    尽量不随意开线程,使用线程池

你可能感兴趣的:(二、活跃性及性能问题)