Java并发编程实践

基础知识

线程安全性

线程安全,主要是这对那些共享的和可变的变量的访问来说的。这很容易理解,如果一个变量它都不是共享的,那就不会有线程安全问题;同理如果变量都不可变,那么再多的线程同时访问也不会有问题。

而当一个变量必须要被共享而且是可变的时候,那么就必须有同步机制来进行控制。

什么是线程安全性

这个定义我个人认为,太难了。简单的说,就是线程按照你的理解正确执行了,就是线程安全的。但是感觉跟废话一样。

看一个实际中的例子:

1
2
3
4
5
6
7
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}

这个例子线程安全吗?是的。为什么?首先,它的变量都是在方法里面的,所以是在栈中定义的,而栈是每个线程私有的,那么就满足了非共享的条件,所以是线程安全的。

然后稍微改进一下:

1
2
3
4
5
6
7
8
9
10
public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;
public long getCount() { return count; }
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
}

这个还是线程安全的吗?不是了。为什么?因为多了个count。count是一个对象的变量,分配在堆中,所有的线程都能够对其访问,而++这个操作并不是原子性的,导致了问题的发生。

在实际中,除了这个问题外,还有一种最常见的就是check-then-act的问题:

1
2
3
if(test!=null){
// do something
}

问题就出在,一旦你通过这个if判断并且进入到相应的函数里面,别的线程是完全有可能把你的判断条件进行修改,但是此时你已经进到这个判断条件中去了。

解决这两个问题的关键在于,我需要保证操作的原子性。比如上面的例子中的count,我只要使用AtomicLong这个原子类,就可以保证自增操作的原子性。

加锁

上面已经通过一个原子类,来解决在多个线程自增的问题。那么是不是只要让所有的共享变量都是原子类,问题就解决了呢?显然不是。

因为你并不能保证这些原子类之间进行操作,还是原子性的。比如有两个原子类A和B,一个hashmap。A作为key,而B作为value保存在其中。那么当我只放入了A和B,并且B还需要进行进一步的修改才能真正完成。那么如果有线程在B完成之前进行了切换,就会导致问题的发生。

所以真正的解决方案是:在单个原子操作中,更新所有的变量。所以,我们可以使用synchronized关键字,让这个函数都成为一个“原子性”。

用锁来保护

如果一个状态变量在代码中存在多份,那么就必须要保证所有位置对其的操作都需要同步,而如果用的是锁,那么就需要使用同一个锁。

共享对象

上一章讲的是如果并发访问对象的变量,而这章则是介绍对象的共享。且之间保证的是原子性,其实除了原子性之外,还有一个是内存可见性。

可见性

可见性说的是,当一个线程修改了某一个共享变量的值,另外一个线程并不能马上得知。并且由于重排序的存在,在对多个变量进行修改时,顺序是不一定的。

下面是一个最简单的线程不安全的例子:

1
2
3
4
5
6
7
8
9
10
11
class Bean {
private int value;

public int getValue() {
return value;
}

public void setValue(int value) {
this.value = value;
}
}

当一个线程使用set设置好value的值之后,另一个线程使用get也无法保证一定是正确的值。所以要做的就是为这两个方法都加上synchronized关键字修饰符就可以了。

还有一点是,java中的long和double是64位的,而JVM是允许将这两个64位的值的读写拆成两个三十二位的读写,这样问题就更大了。

而锁,就可以保证可见性。但是锁有点重,对性能影响较大,如果仅仅为了可见性,那么可以使用volatile关键字来修饰变量。

注意!可见性和原子性可是没有关系的。比如你用volatile修饰的变量执行自增操作,照样会出现问题,因为volatile并不保证原子性。

发布和逸出

文中翻译的是发布,我更愿意称之为暴露,即一个对象被别人引用了,则称这个对象暴露了。当然如果这个对象里面还有别的可以访问的对象,那么这些对象也一概被打包暴露了。

线程封闭

那么如何保证一个对象不逸出呢?很简单,只需要把它封闭在一个线程之中,就能自动实现线程安全性了。java提供了一些机制来帮助线程封闭性——ThreadLocal类等。

一个最简单的线程封闭的例子就是栈封闭,只要在线程的方法中声明的局部变量,会在线程的栈中分配空间,自然不会被别的线程所访问。

ThreadLocal

这是一个更为规范的实现线程封闭的方法。一个最常用的例子就是JDBC的Connection对象。每个线程都有一个对数据库进行访问的Connection对象,为了避免别的线程的干扰,可以使用ThreadLocal进行保存。

不变性

一句话结论:不可变对象一定是线程安全的。

安全发布

很多时候我们需要在各个线程之间共享对象。之前也说了不可变对象是线程安全的,所以怎么发布无所谓。而对于可变对象,可以通过以下的方式来安全的发布:

  • 在静态初始化函数中初始化引用。
  • 将对象的引用保存到 volatie/AtomicReferance/final/锁 的域中。