线程安全定义:
当多个线程访问一个类的时候,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方法代码不必做其他的协调,这个类的行为任然是正确的,那么称这个类是线程安全的。
所有的例子都是一个servlet用来进行因数分解,通过request传入一个数字,然后service方法调用factor方法进行因数分解。
1. 无状态的对象永远线程安全
public class StatelessFactorizer extends GenericServlet implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[] { i };
}
}
这个类没有一个私有或公共的变量,是一个无状态的类。因为多个线程进入service方法,所有的service方法内的局部变量都封装在各个线程的栈中,只有本线程可以访问,所以,这个类是线程安全的。
2. 原子性
@NotThreadSafe
public class UnsafeCountingFactorizer extends GenericServlet 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);
}
void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[] { i };
}
}
我们在第一个例子上,添加了一个功能计数。每调一次service方法,count加1。很可惜,这个类不是线程安全的,因为++count不是一个原子操作,这是一个读-改-写三个步骤的操作。这样我们就很容易理解这个类为什么不是线程安全的了,线程A读到count值为1,然后修改为2。在线程A将count的值写回去之前,线程B读取了count的值。结果是线程A将count的值改为2,线程B也将count的值改为2。至于什么是原子操作,我也没看懂,定义如下:
假设有操作A和B,如果从执行A的线程的角度看,当其他线程执行B的时候,要么B全部执行完成,要么一点也没有执行,这样A和B互为原子操作。一个原子操作是指:该操作对于所有的操作,包括它自己,都满足前面描述的状态。(我已经晕了)
让count加一操作变成原子操作的代码如下:
@ThreadSafe
public class CountingFactorizer extends GenericServlet 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);
}
void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return null;
}
BigInteger[] factor(BigInteger i) {
return null;
}
}
AtomicLong是什么鬼,这个大家去问度娘吧。简单说,它在读-改-写的第三步写操作时还会回去查看值是否已经被改变,如果被改变,重新加载。(可以百度搜索java cas)
3. 锁
我们上面所说的自增操作,通过atomic类变成了原子操作,因此类也就变成线程安全的了。但是如果有多个全局变量,并且这多个全局变量的值具有关联性。场景如下:我们想实现应对连续两个客户请求相同的数字进行因数分解,于是我们缓存了最新的计算结果
@NotThreadSafe
public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber.get()))
encodeIntoResponse(resp, lastFactors.get());
else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[] { i };
}
这个类不是线程安全的,因为lastNumber和lastFactors这两个变量是有关系的,不是独立的。我们的atomic只能保证他们自身的操作是线程安全,无法保证两个一起是线程安全的。假设,lastNumber被线程A修改为了10,而lastFactors还是旧值9的因数分解结果,那线程B刚好请求数字10的因数分解结果,就拿到了9的因数分解结果。
这里我们很容易想到用synchronized关键字来给service方法加锁,我就不贴代码了,这样做的确能够解决问题,但是service方法一次只能进入一个线程了,性能存在很大问题。我们在做并发编程的时候,一定要兼顾性能,最好的方式如下所示:
@ThreadSafe
public class CachedFactorizer extends GenericServlet implements Servlet {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
@GuardedBy("this") private long hits;
@GuardedBy("this") private long cacheHits;
public synchronized long getHits() {
return hits;
}
public synchronized double getCacheHitRatio() {
return (double) cacheHits / (double) hits;
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = null;
synchronized (this) {
++hits;
if (i.equals(lastNumber)) {
++cacheHits;
factors = lastFactors.clone();
}
}
if (factors == null) {
factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp, factors);
}
void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[]{i};
}
}
把锁打碎在方法里面,第一个synchronized块保护着检查再运行的操作,另一个保证缓存的number和factor同步更新。提醒一点,在加锁期间,不要进行耗时操作,我们把锁移到方法里面,主要目的就是将那些耗时操作剔除到同步块之外。