1.线程安全
1.1 什么是线程安全
不知道是对文字的敏感还是愚钝,总觉得“下定义”是一件很难的事情。由是,特别对于那些简明扼要的定义,细细品味,竟也能衍生出优美。这一点看,自然科学未必就是那么纯粹的、无情感的。编写线程安全的代码,本质上就是管理对状态的访问,而且通常都是共享的、可变的状态。通俗的说,一个对象的状态,就是它的数据,存储在状态变量中(state variables)中,比如实例域或者静态域。
《JAVA并发编程实践》一书对于“正确性”定义如下:正确性意味着一个类与它的规约保持一致。良好的规约定义了用于强制对象状态的不变约束(invariants)以及描述影响的后验条件(postconditions)。前者保证了类满足基本的静态条件,后者保证行为结果的正确性。
1.2 一个无状态的(stateless)的servlet
StatelessFactorizer.class
package com.tree.thread;
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp,factors);
}
}
上面的程序和大多的servlet一样,是无状态的:它不包含域也没有引用其他的域。一次特定的计算的瞬时状态,会唯一存在本地的变量中,这些本地变量存储在线程的栈中,只有执行线程才能访问。一个访问StatelessFactorizer的线程,不会影响访问同一个servlet的其他线程的计算结果;因为两个线程不共享状态(其根本就没有状态)!线程访问无状态对象的行为,总是安全的。
多数的servlet都可以实现为无状态的,这一事实极大地降低了servlet线程的安全负担。
1.3 原子性
如果向无状态对象中加入一个状态元素会怎样?假设我们添加一个“counter”来计算处理请求的数量。
UnsafeCountingFactorizer.class
package com.tree.thread;
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);
}
}
显然,以上的的程序是非线程安全的。正如前一篇博客里面的UnsafeSequence,它很容易遗失更新(lost update)自增操作的语法看上去很紧凑(++counter),但其不是原子操作。这意味着,它不能作为一个单独的、不可分割的操作去执行。相反,自增操作是3个离散的操作的简写形式:获得当前的值,加1,写回新值。这是一个读-改-写(read-modify-write)操作的实例。
1.4 竞争条件
竞争条件指多个线程或者进程在读写一个共享数据时结果依赖于它们执行的相对时间的情形。
竞争条件发生在当多个进程或者线程在读写数据时,其最终的的结果依赖于多个进程的指令执行顺序。换句话说,想要得到正确的答案,要依赖于“幸运”的时序。
例如:考虑下面的例子
假设两个进程P1和P2共享了变量a。在某一执行时刻,P1更新a为1,在另一时刻,P2更新a为2。
因此两个任务竞争地写变量a。在这个例子中,竞争的“失败者”(最后更新的进程)决定了变量a的最终值。
多个进程并发访问和操作同一数据且执行结果与访问的特定顺序有关,称为竞争条件。最常见的一种竞争条件是“检查再运行”(cherk-then-act)使用一个潜在的过期值作为决定下一步操作的数据:你观察到一些事情为真(文件X不存在),然后(then)基于你的观察去执行一些动作(创建文件X);但是事实上,从你观察到执行操作的这段时间里,观察的结果可能无效了(有人在此期间创造了文件X),从而引发错误。
1.5 示例:惰性初始化中的竞争条件
检查再运行的常见用法是惰性初始化(lazy initialization)。惰性初始化的目的是延迟对象的初始化,直到程序真正的使用它,同时确保它只初始化一次。但是如下的程序存在的竞争条件会破坏其正确性。
LazyInitRace.class
package com.tree.thread;
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance(){
if(instance == null){
instance = new ExpensiveObject();}
return instance;
}
}
1.6 复合操作
LazyInitRace包含了一系列的操作,相对于在同一状态的其他操作而言,必须是原子性的或者不可分割的。为了避免竞争条件,必须阻止其他线程访问我们正在修改的变量。需要做如下保证:当其他线程想要查看或者修改某个状态时,必须在我们的线程开始之前或者完成之后,而不能再操作过程中。
为了确保线程的安全,所有的检查再运行操作以及读-改-写操作都必须是原子操作。检查再运行和读-改-写操作的全部执行过程是复合操作,为了保证线程的安全,必须原子的进行。
CountingFactorizer.class
package com.tree.thread;
import java.util.concurrent.atomic.AtomicLong;
public class CountingFactorizer implements Servlet{
private final AtomicLong count = new AtomicLong(0);
public long getCount(){return count.get();}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp,factors);
}
}
Java.util.concurrent.atomoic包中包括了原子变量(atomic variable)类,这些类用来实现数字和对象引用的原子状态的转换。如下图。