摘录自 极客时间

安全性问题

并发BUG的三个主要源头是:原子性问题、可见性问题和有序性问题。只有多个线程会同时读写同一个数据的情况下需要考虑这三个问题。

数据竞争

多个线程访问下面这个就会发生数据竞争

1
2
3
4
5
6
7
8
9
public class Test {
private long count = 0;
void add10K() {
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
}
}

加俩个synchronized,看似问题解决,其实还是有点问题的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
private long count = 0;
synchronized long get(){
return count;
}
synchronized void set(long v){
count = v;
}
void add10K() {
int idx = 0;
while(idx++ < 10000) {
set(get()+1)
}
}
}

上面这个代码,假设两个线程同时执行get()方法,get()会返回相同的值0,然后(get() + 1)的操作会导致最后写入内存的结果为1,期望是2但是最后却还是1。
这里要注意的是add10K()它自己不是一个synchronized修饰的,他是可以被多个线程同时执行的,所以就可能出现两个线程先后调用了get()之后才会有其中一个调用set(),就会出现奇怪的bug。

活跃性问题

典型的活跃性问题有死锁活锁饥饿

  • 死锁: 略
  • 活锁: 两个线程各自有一些对方要的,然后又各自释放资源,然后又请求,又撞车了,就无限循环。
    • 解决活锁只要两个线程等待时间随机就行。
  • 饥饿: 饥饿指的是线程因为无法访问所需资源而无法执行下去的情况。线程优先级不均匀、持有锁的线程执行时间过长都可能诱发饥饿问题。解决饥饿有下面三种方案:
    • 保证资源充足(没钱)
    • 公平分配资源(公平锁:先来后到的方案,线程等待是有顺序的)
    • 避免持有锁的进程长时间执行(不会)

性能问题

如果把并行的程序太大面积的转换成串行,那么效率会直线下降。
阿姆达尔(Amdahl)定律

1-p指的是串行百分比,n是CPU的核数。
也就是说,如果串行率是5%,那么无论采用什么样的技术,最多也就能提升20倍的性能。

解决性能问题的三个方案:

  • 不用锁。线程本地存储(Thread Local Storage)、 写入时复制(Copy-on-write)、乐观锁等
  • 减少锁持有的时间。例如细粒度的锁

思考小题

Vector是Java一个线程安全的容器,那么下面代码是否有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void addIfNotExist(Vector v, Object o){
if(!v.contains(o)) {
v.add(o);
}
}
```
代码的问题在于数据竞争,这个`addIfNotExist()`方法本身不是线程安全的,那么他可能先后执行了`contains()`以后才开始相继`add()`,这样就会造成重复添加的问题。

这样就能解决问题,这里因为v只是一个指针而已,所以它不会变:
```java
void addIfNotExist(Vector v, Object o){
synchronized(v) {
if(!v.contains(o)) {
v.add(o);
}
}
}

不能直接在方法上加synchronized,因为它不是vector的方法,两个线程可能操作的是不同的实例买这种情况下这个v就还是可能会出现增加重复的东西的情况。