思维导图:
引言:
并发编程学习系列是我对《Java 并发编程实战》这本书的学习总结。
所谓的线程安全性就是指在多个线程访问某个类时,这个类都能表现出正确的行为,那么称这个类是线程安全的。本文主要分两个部分来介绍线程安全性:
- 原理部分:主要介绍原子性和原子操作
- 使用部分:主要介绍如何用锁来保证线程安全性及并发性能的提高
一.原子性
此小节会通过类的状态(域对象,此后都称为状态)从无到有,从有到多的顺序介绍类应该如何通过保证原子性来来保证线程安全性。
1.1 无状态对象
当一个类没有没有状态时一定是线程安全的。因为方法之中的参数都保存在局部变量表中,而局部变量表又是属于特定的线程的方法栈的一部分,其他线程无法访问,所以,无状态的类一定是线程安全的。
例如下列代码,主要是service方法,其他的方法并没有真正的实现,@ThreadSafe表示此类是线程安全的:
@ThreadSafe
public class StatelessFactorizer extends GenericServlet implements Servlet {
@Override
public void service(ServletRequest req, ServletResponse resp) {
// 方法的具体实现不在描述
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}
1.2 单一状态对象
当类中含有一个状态的时候,可能就会出现竞态条件这种并发问题。
在下列代码中,count用于统计service方法的执行次数,但是,++count这个操作会导致竞态条件。其原因是因为++count操作会先获取count值,然后count值+1,再然后,给count赋予新值。在这个过程中,如果有另一个线程在此线程给count赋予新的值之前执行了++count操作,其获取的count值还没有+1,,其结果就导致count统计的service调用次数是不正确的。
@NotThreadSafe
public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
@Override
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
}
按照上述例子总结一下,静态条件的表现就是由于不恰当的执行时序而出现的不正确的结果。
竞态条件的本质则是基于某一个可能已经失效的观察结果来做出判断或者计算,在上述例子中,value=9就是一个已失效的观察结果。
同理,“先检查后执行“的程序也会导致竞态条件的发生,比如延迟初始化,代码如下:
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
那么,上述统计service调用次数的代码如何才能变得线程安全呢?我们就需要在某个状态被使用时防止其他线程读取或修改的机制(保证的机制在第二部分描述)。
@ThreadSafe
public class CountingFactorizer extends GenericServlet implements Servlet {
// 使用AtomicLong来保证原子性
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);
}
}
1.3 多状态对象
最后,如果一个类中所有的状态的单独访问都保证了原子性,整个类的访问就会保证原子性吗?答案是不会。以下代码用于计算某个值的因式分解并进行缓存,其中,lastNumber和lastFacotrs其状态都保证了原子性,但是在组合这个状态的变化的操作时,依然可能会出现竞态条件,最终的结果就是导致lastNumber和lastFacotrs可能是不匹配的。
@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);
}
}
}
所以,当类中存在多个状态且各个状态之间不是互相独立而是互有约束的时候,更新某个状态就需要在原子操作中更新其他状态,以保证状态之间的一致性。原子操作指对于访问同一个状态的所有操作来说,这个操作是一个原子方式执行的操作。
如何保证一系列的操作是原子操作呢,请看下文:
二.加锁机制
这一小节介绍如何进行简单的加锁以保证原子操作及如何优化加锁的方式以提高并发性能
2.1 同步代码块
Java提供了一种内置的加锁机制来支持原子性:同步代码块。同步代码块分为两个部分,其一是锁对象,其二则是锁保护的代码块。
每个Java对象都可以被用作一个实现同步的锁,称为内置锁。线程会在进入同步代码块之前自动获取此锁,在正常或者异常退出后释放该锁。
例如Synchronized关键字用于普通方法申明中,他的锁既是方法对象的内置锁,而保护的代码块则是方法中的代码块。当其用于静态方法申明是,锁则是此类的Class对象,保护的代码块不变。所以,当某个线程获取的某个类的锁时,其他线程不能访问用Synchronized关键字修饰的方法,因为锁已被占用。
但是,如果这个线程已经获取了当前对象的锁,那么,如果此线程访问其他加锁的方法是可以成功的,这种机制称为重入。如果没有重入机制,那么以下这种情况就会发生死锁。因为进入子类方法时,因为子类方法调用了父类方法,所以会获取父类对象的锁,而在方法体体中,再一次调用了父类方法,如果没有重入机制,那么就会因为父类对象的锁已被获取而发生死锁。
class Widget {
public synchronized void doSomething() {
}
}
class LoggingWidget extends Widget {
public synchronized void doSomething() {
// 进入方法是以获取父类对象的锁
System.out.println(toString() + ": calling doSomething");
// 调用父类的方法时就会再次获取父类对象的锁
super.doSomething();
}
}
同时synchronized关键字不能滥用于方法申明中,因为他们的锁都是当前对象,这回导致性能变差。一般来说,每个保证状态一致性的方法都需要获取一个特定的锁,以便于维护和性能的提升。
2.2 代码块内加锁
现在回想一下用户缓存计算因式分解结果的那个Service,我们可以在其Service方法上添加Synchronized以保证原子性,但是折回极大的抑制Service的性能,因为Service在某一时间点只能服务一个请求。而优化的方法则是在代码内部加锁,如下所示:
@ThreadSafe
public class CachedFactorizer extends GenericServlet implements Servlet {
//@GuardedBy("this") 表示锁是当前所在的对象,即CachedFactorizer
@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);
}
}