基础知识
线程安全性
线程安全,主要是这对那些共享的和可变的变量的访问来说的。这很容易理解,如果一个变量它都不是共享的,那就不会有线程安全问题;同理如果变量都不可变,那么再多的线程同时访问也不会有问题。
而当一个变量必须要被共享而且是可变的时候,那么就必须有同步机制来进行控制。
什么是线程安全性
这个定义我个人认为,太难了。简单的说,就是线程按照你的理解正确执行了,就是线程安全的。但是感觉跟废话一样。
看一个实际中的例子:
1 | public class StatelessFactorizer implements Servlet { |
这个例子线程安全吗?是的。为什么?首先,它的变量都是在方法里面的,所以是在栈中定义的,而栈是每个线程私有的,那么就满足了非共享的条件,所以是线程安全的。
然后稍微改进一下:
1 | public class UnsafeCountingFactorizer implements Servlet { |
这个还是线程安全的吗?不是了。为什么?因为多了个count。count是一个对象的变量,分配在堆中,所有的线程都能够对其访问,而++这个操作并不是原子性的,导致了问题的发生。
在实际中,除了这个问题外,还有一种最常见的就是check-then-act的问题:
1 | if(test!=null){ |
问题就出在,一旦你通过这个if判断并且进入到相应的函数里面,别的线程是完全有可能把你的判断条件进行修改,但是此时你已经进到这个判断条件中去了。
解决这两个问题的关键在于,我需要保证操作的原子性。比如上面的例子中的count,我只要使用AtomicLong这个原子类,就可以保证自增操作的原子性。
加锁
上面已经通过一个原子类,来解决在多个线程自增的问题。那么是不是只要让所有的共享变量都是原子类,问题就解决了呢?显然不是。
因为你并不能保证这些原子类之间进行操作,还是原子性的。比如有两个原子类A和B,一个hashmap。A作为key,而B作为value保存在其中。那么当我只放入了A和B,并且B还需要进行进一步的修改才能真正完成。那么如果有线程在B完成之前进行了切换,就会导致问题的发生。
所以真正的解决方案是:在单个原子操作中,更新所有的变量。所以,我们可以使用synchronized关键字,让这个函数都成为一个“原子性”。
用锁来保护
如果一个状态变量在代码中存在多份,那么就必须要保证所有位置对其的操作都需要同步,而如果用的是锁,那么就需要使用同一个锁。
共享对象
上一章讲的是如果并发访问对象的变量,而这章则是介绍对象的共享。且之间保证的是原子性,其实除了原子性之外,还有一个是内存可见性。
可见性
可见性说的是,当一个线程修改了某一个共享变量的值,另外一个线程并不能马上得知。并且由于重排序的存在,在对多个变量进行修改时,顺序是不一定的。
下面是一个最简单的线程不安全的例子:
1 | class Bean { |
当一个线程使用set设置好value的值之后,另一个线程使用get也无法保证一定是正确的值。所以要做的就是为这两个方法都加上synchronized关键字修饰符就可以了。
还有一点是,java中的long和double是64位的,而JVM是允许将这两个64位的值的读写拆成两个三十二位的读写,这样问题就更大了。
而锁,就可以保证可见性。但是锁有点重,对性能影响较大,如果仅仅为了可见性,那么可以使用volatile关键字来修饰变量。
注意!可见性和原子性可是没有关系的。比如你用volatile修饰的变量执行自增操作,照样会出现问题,因为volatile并不保证原子性。
发布和逸出
文中翻译的是发布,我更愿意称之为暴露,即一个对象被别人引用了,则称这个对象暴露了。当然如果这个对象里面还有别的可以访问的对象,那么这些对象也一概被打包暴露了。
线程封闭
那么如何保证一个对象不逸出呢?很简单,只需要把它封闭在一个线程之中,就能自动实现线程安全性了。java提供了一些机制来帮助线程封闭性——ThreadLocal类等。
一个最简单的线程封闭的例子就是栈封闭,只要在线程的方法中声明的局部变量,会在线程的栈中分配空间,自然不会被别的线程所访问。
ThreadLocal
这是一个更为规范的实现线程封闭的方法。一个最常用的例子就是JDBC的Connection对象。每个线程都有一个对数据库进行访问的Connection对象,为了避免别的线程的干扰,可以使用ThreadLocal进行保存。
不变性
一句话结论:不可变对象一定是线程安全的。
安全发布
很多时候我们需要在各个线程之间共享对象。之前也说了不可变对象是线程安全的,所以怎么发布无所谓。而对于可变对象,可以通过以下的方式来安全的发布:
- 在静态初始化函数中初始化引用。
- 将对象的引用保存到 volatie/AtomicReferance/final/锁 的域中。