Java的多线程是一把双刃剑,使用好它可以使我们的程序更高效,但是出现并发问题时,我们的程序将会变得非常糟糕。并发编程中需要注意三方面的问题,分别是安全性、活跃性和性能问题。
安全性问题我们经常说这个方法是线程安全的、这个类是线程安全的,那么到底该怎么理解线程安全呢?
要给线程安全性定一个非常明确的定义是比较复杂的。越正式的定义越复杂,也就越难理解。但是不管怎样,在线程安全性定义中,最核心的概念还是正确性,可以简单的理解为程序按照我们期望的执行。
正确性的含义是:某个类的行为与其规范完全一致。线程的安全性就可以理解为:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
我们要想编写出线程安全的程序,就需要避免出现并发问题的三个主要源头:原子性问题、可见性问题和有序性问题。(前面的文章介绍了规避这三个问题的方法)当然也不是所有的代码都需要分析这三个问题,只有存在共享数据并且该数据会发生变化,即有多个线程会同时读写同一个数据时,我们才需要同步对共享变量的操作以保证线程安全性。
这也暗示了,如果不共享数据或者共享数据状态不发生变化,那么也可以保证线程安全性。
综上,我们可以总结出设计线程安全的程序可以从以下三个方面入手:
不在线程之间共享变量。
将共享变量设置为不可变的。
在访问共享变量时使用同步。
我们前面介绍过使用Java中主要的同步机制synchronized关键字来协同线程对变量的访问,synchronized提供的是一种独占的加锁方式。同步机制除了synchronized内置锁方案,还包括volatile类型变量,显式锁(Explicit Lock)以及原子变量。而基于一二点的技术方案有线程本地存储(Thread Local Storage, LTS)、不变模型等(后面会介绍)。
数据竞争当多个线程同时访问一个数据,并且至少有一个线程会写这个数据时,如果我们不采用任何 同步机制协同这些线程对变量的访问,那么就会导致并发问题。这种情况我们叫做数据竞争(Data Race)。
例如下面的例子就会发生数据竞争。
public class Test { private long count = 0; void add10K() { int idx = 0; while(idx++ < 10000) { count += 1; } } }当多个线程调用add10K()时,就会发生数据竞争。但是我们下面使用synchronized同步机制就可以来防止数据竞争。
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); } } } 竞态条件但是此时的add10K()方法并不是线程安全的。
假设count=0, 当两个线程同时执行get()方法后,get()方法会返回相同的值0,两个线程执行get()+1操作,结果都是1,之后两个线程再将结果1写入了内存。本来期望的是2,但是结果却是1。(至于为什么会同时?我当初脑袋被“阻塞”好一会儿才反应过来,哈哈,╮(~▽~)╭,看来不能熬夜写博客。因为如果实参需要计算那么会先被计算,然后作为函数调用的参数传入。这里get()会先被调用,等其返回了才会调用set(),所以一个线程调用完了get()后,另一个线程可以马上获取锁调用get()。这也就会造成两个线程会得到相同的值。)
这种情况,我们称为竞态条件(Race Condition)。竞态条件,是指程序的执行结果依赖线程执行的顺序 。
上面的例子中,如果两个线程完全同时执行,那么结果是1;如果两个线程是前后执行,那么结果就是2。在并发环境里,线程的执行顺序是不确定的,如果程序存在竞态条件问题,那么就意味着程序执行的结果是不确定的,而执行结果不确定就是一个大问题。
我们前面讲并发bug源头时,也介绍过竞态条件。由于不恰当的执行时序而导致的不正确的结果。要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或者之后读取和修改状态,而不是在修改状态的过程中。
解决这个例子的竞态条件问题,我们可以介绍过的加锁机制来保证:其他线程只能在修改操作完成之前或者之后读取和修改状态,而不是在修改状态的过程中。
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) { synchronized(this){ set(get()+1); } } } }所以面对数据竞争和竞态条件我们可以使用加锁机制来保证线程的安全性!
活跃性问题