所有的线程安全问题,都可以归结为同一个原因:共享的可变状态。 “共享”意味着变量可以由多个线程同时访问,“可变”则意味着变量的值在其生命周期内可以发生变化。
编写线程安全代码核心就是对共享可变状态的访问操作进行管理,通过引入 同步机制 来保证任意时刻只有一个线程访问共享可变状态。
在并发编程中,共享可变对象会面临以下3个问题:
当某个计算的正确性取决于多个线程的交替执行顺序时,那么就会发生竞态条件。常见两种静态模式:
read-modify-write的示例:
@NotThreadSafe public class UnsafeCounter { private int value; public int getNext() { return value++; } } 复制代码
其中自增操作value++包含了三步子操作:读取value的值,将值加1,最后计算结果写入到vlue值,典型的读改写竞态模式。
check-then-act的示例:
@NotThreadSafe public class UnsafeSequence { private int sequence; public int getSequence() { if(sequence >= 99){ // 步骤1 检查共享变量 return 0; // 步骤2 act 检查后操作 } else { return sequence++; } } } 复制代码
如何避免竞态条件问题,就需要引入原子操作来保证某个线程在修改共享变量过程中,其他线程不可操作,其他线程只能在原子操作前或原子操作后读取和修改共享变量状态。下面我们来看下什么是原子操作:
对于涉及共享变量的访问操作,若该操作从其执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,该操作具有原子性。从概念看注意以下两点:
同时不可分割有以下两层含义:
java中两种方式实现原子性:
在多线程环境下,一个线程对某个共享变量进行更新之后,后续访问该变量的线程可能无法立刻读取到这个更新的结果,甚至永久无法读取到这个更新结果。这个是线程安全的另一个表现形式:可见性。
可见性问题是由于java的内存模型决定的,具体可以参考 java并发编程——内存模型
java平台如何保证可见性:
现代微处理器会通过指令乱序执行(out-of-order execution)来提升执行效率,除了处理器,Java自身的JIT编辑器也会对指令做重排序,最终生成的机器指令可能与字节码顺序并不一致。 在并发程序中,指令重排序可能会导致预期之外的执行结果,比如以下的程序,在多线程执行时,线程1中的语句可能会被乱序执行,flg=true可能会先于a=1被执行,则线程2可能会出乎意料地打印出 a = 2。
java平台如何保证内存访问顺序性:
无状态的对象一定是线程安全的,典型代表Servlet程序,各个Servlet自身并不持有状态,彼此隔离,互相不干扰。如果持有状态不可避免,则可以使用线程封闭技术,将状态“隐藏起来”,不让别的线程访问到。常见的方法是栈封闭和ThreadLoca两种形式。 栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量访问对象,这些局部变量被封闭在执行线程的栈内部,其它线程无法访问到它们。
public int loadTheArk(Collection<Animal> candidates) { SortedSet<Animal> animals; int numPairs = 0; Animal candidate = null; // animals confined to method, don't let them escape! animals = new TreeSet<Animal>(new SpeciesGenderComparator()); animals.addAll(candidates); for (Animal a : animals) { if (candidate == null || !candidate.isPotentialMate(a)) candidate = a; else { ark.load(new AnimalPair(candidate, a)); ++numPairs; candidate = null; } } return numPairs; } 复制代码
在上面的代码中,animals和candidate是函数的局部变量,被封闭在栈帧内部,不会逸出,被其它线程访问到,所以该方法是线程安全的。关于ThreadLocal会有专门章节介绍,这里面不展开说明。
如果某个对象在被创建后其状态就不能被修改,那么这个对象就被称为不可变对象。不可变对象一定是线程安全的。满足以下条件时,对象才是不可变的:
Guava库也提供了一组不可变类,比如ImmutabelList、ImmutableSet 这些,我们应该在代码中尽可能地使用它们
如果共享和可变都无法避免,那么只有使用下策 —— 同步机制,来保证线程安全性。在Java代码中,通常使用synchronized关键字,对类或者对象加锁,来实现同步。具体java的同步机制有哪些将在下一章分析。