7安全性活跃性及性能问题

并发编程需要注意的问题,总结主要包括三个问题:安全性,活跃性,性能问题

并发编程是一个复杂的技术领域,
微观上涉及到原子性问题、可见性问题和有序性问题
宏观则表现为安全性、活跃性以及性能问题。

安全性

是否线程安全:本质上就是正确性,正确性即程序按照期望执行
共享数据(多线程同时读写)时才会出现并发问题
如果数据不共享或者状态不变化就能保证线程安全性。

数据竞争:多线程访问,且一个线程会写
竞态条件:程序的执行结果依赖线程执行的顺序。两个线程同时++则结果是1,顺序执行++结果为2。

存在数据竞争、竞态条件如何保证线程安全性:互斥方案(使用锁)

活跃性

活跃性问题,指的是某个操作无法执行下去,如死锁、活锁、饥饿

死锁

线程相互等待资源(不可抢占,不让出)

活锁

同时放弃,然后又重试竞争,最后死循环
解决:等待随机事件释放资源

饥饿

指的是线程因无法访问所需资源而无法执行下去的情况。

导致饥饿的情况:

  1. 持有锁的线程长时间执行
  2. 线程繁忙时,线程优先级低很难执行

解决饥饿

  • 保证资源充足
  • 公平分配
  • 避免长期持有锁

资源的稀缺性是没办法解决的,持有锁的线程执行的时间也很难缩短。所以2的适用场景相对更多。主要使用公平锁,先到先得,按顺序获取资源。

性能

阿姆达尔(Amdahl)定律,代表了处理器并行运算之后效率提升的能力,具体公式如下:S = 1 / ( (1-p) + (p/n) )

n:CPU 的核数,p:并行百分比,(1-p):串行百分比
假设串行百分比5%, CPU的核数n无穷大,那加速比 S 的极限就是20。即,如果串行率是5%,无论采用什么技术,最高也就只能提高 20 倍的性能。
所以使用锁需关注性能影响。SDK并发包里之所以有那么多东西,有很大一部分原因就是要提升在某个特定领域的性能。

方案层面解决性能问题

  1. 使用无锁算法和结构
    如线程本地存储(TLS Thread Local Storage)、写入时复制 (Copy-on-write)、乐观锁等;Java并发包里面的原子类也是一种无锁的数据结构;Disruptor则是一个无锁的内存队列,性能都非常好…
  2. 减少锁持有的时间
    如使用细粒度锁,ConcurrentHashMap分段锁技术、读写锁(读时无锁,写时互斥)

遇到具体问题,还是要具体分析,根据特定的场景选择合适的数据结构和算法。

性能指标

我认为的重要指标:并发量,吞吐量,延迟
吞吐量:指的是单位时间内能处理的请求数量。吞吐量越高,说明性能越好。
延迟:指的是从发出请求到收到响应的时间。延迟越小,说明性能越好。
并发量:指的是能同时处理的请求数量,一般来说随着并发量的增加、延迟也会增加。所以延迟这个指标,一般都会是基于并发量来说的。例如并发量是 1000 的时候,延迟是 50 毫秒

问题

1

Java Vector是一个线程安全的容器,如下代码是否存在并发问题呢?

1
2
3
4
5
void addIfNotExist(Vector v, Object o){
if(!v.contains(o)) {
v.add(o);
}
}

vector是线程安全,指的是它方法单独执行时候线程安全,组合在一起不安全,组合操作要小心。

2

服务器上存了2000万个电话号码相关的数据,要做的是把这批号码从服务器上请求下来写入到本地的文件中,为了将数据打散到多个文件中,这里通过 电话号码%1024 得到的余数来确定这个号码需要存入到哪个文件中取,比如13888888888 % 1024 =56,那么这个号码会被存入到 56.txt的文件中,写入时是一行一个号码。
为了效率这里使用了多线程来请求数据并将请求下来的数据写入到文件,也就是每个线程包含向服务器请求数据,然后在将数据写入到电话号码对1024取余的那个文件中去,如果这么做目前会有一个隐患,多线程时如果 电话号码%1024 后定位的是同一个文件,那么就会出现多线程同时写这个文件的操作,一定程度上会造成最终结果错误。

写一个文件只需要一个线程就够了。
你可以用生产者-消费者模式试一下。
可以创建64个线程,每个线程负责16个文件,
同时创建64个阻塞队列,64个线程消费这64个阻塞队列,
电话号码%1024 % 64 进入目标阻塞队列。