欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

网易Java研发面试官眼中的Java并发——安全性、活跃性、性能

程序员文章站 2022-09-07 12:43:21
一. 安全性问题 线程安全的本质是正确性,而正确性的含义是程序按照预期执行 理论上线程安全的程序,应该要避免出现可见性问题(CPU缓存)、原子性问题(线程切换)和有序性问题(编译优化) 需要分析是否存在线程安全问题的场景:存在共享数据且数据会发生变化,即有多个线程会同时读写同一个数据 针对该理论的解 ......

一. 安全性问题

  1. 线程安全的本质是正确性,而正确性的含义是程序按照预期执行

  2. 理论上线程安全的程序,应该要避免出现可见性问题(cpu缓存)、原子性问题(线程切换)和有序性问题(编译优化)

  3. 需要分析是否存在线程安全问题的场景:存在共享数据且数据会发生变化,即有多个线程会同时读写同一个数据

  4. 针对该理论的解决方案:不共享数据,采用线程本地存储(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,不符合预期,这种情况为竟态条件

ⅱ. 竟态条件

  1. 竟态条件(race condition):程序的执行结果依赖于线程执行的顺序
  2. 在并发环境里,线程的执行顺序是不确定的
    • 如果程序存在竟态条件问题,那么意味着程序的执行结果是不确定的

1. 转账

public class account {
    private int balance;
    // 非线程安全,存在竟态条件,可能会超额转出
    public void transfer(account target, int amt) {
        if (balance > amt) {
            balance -= amt;
            target.balance += amt;
        }
    }
}

 

ⅲ. 解决方案

面对数据竞争和竟态条件问题,可以通过互斥的方案来实现线程安全,互斥的方案可以统一归为锁

二. 活跃性问题

活跃性问题:某个操作无法执行下去,包括三种情况:死锁、活锁、饥饿

ⅰ. 死锁

  1. 发生死锁后线程会相互等待,表现为线程永久阻塞
  2. 解决死锁问题的方法是规避死锁(破坏发生死锁的条件之一)
    • 互斥:不可破坏,锁定目的就是为了互斥
    • 占有且等待:一次性申请所有需要的资源
    • 不可抢占:当线程持有资源a,并尝试持有资源b时失败,线程主动释放资源a
    • 循环等待:将资源编号排序,线程申请资源时按递增(或递减)的顺序申请

ⅱ. 活锁

  • 活锁:线程并没有发生阻塞,但由于相互谦让,而导致执行不下去
  • 解决方案:在谦让时,尝试等待一个随机时间(分布式一致算法raft也有采用)

ⅲ. 饥饿

  1. 饥饿:线程因无法访问所需资源而无法执行下去
    • 线程的优先级是不相同的,在cpu繁忙的情况下,优先级低的线程得到执行的机会很少,可能发生线程饥饿
    • 持有锁的线程,如果执行的时间过长(持有的资源不释放),也有可能导致饥饿问题
  2. 解决方案
    • 保证资源充足
    • 公平地分配资源(公平锁) – 比较可行
    • 避免持有锁的线程长时间执行

三. 性能问题

  1. 锁的过度使用可能会导致串行化的范围过大,这会影响多线程优势的发挥(并发程序的目的就是为了提升性能)
  2. 尽量减少串行,假设串行百分比为5%,那么多核多线程相对于单核单线程的提升公式(amdahl定律)
    s=1/((1-p)+p/n),n为cpu核数,p为并行百分比,(1-p)为串行百分比
  • 假如p=95%,n无穷大,加速比s的极限为20,即无论采用什么技术,最高只能提高20倍的性能

ⅰ. 解决方案

  1. 无锁算法和数据结构
    • 线程本地存储(thread local storage,tls)
    • 写入时复制(copy-on-write)
    • 乐观锁
    • juc中的原子类
    • disruptor(无锁的内存队列)
  2. 减少锁持有的时间,互斥锁的本质是将并行的程序串行化,要增加并行度,一定要减少持有锁的时间
    • 使用细粒度锁,例如juc中的concurrenthashmap(分段锁)
    • 使用读写锁,即读是无锁的,只有写才会互斥的

ⅱ. 性能指标

  1. 吞吐量:在单位时间内能处理的请求数量,吞吐量越高,说明性能越好
  2. 延迟:从发出请求到收到响应的时间,延迟越小,说明性能越好
  3. 并发量:能同时处理的请求数量,一般来说随着并发量的增加,延迟也会增加,所以延迟一般是基于并发量来说的

写在最后

 网易Java研发面试官眼中的Java并发——安全性、活跃性、性能