1.线程安全性
定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
无状态对象一定是线程安全的。
原子性
有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作
竟态条件
某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。换句话说,就是正确的结果要取决于运气[2]。最常见的竞态条件类型就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能失效的观测结果来决定下一步的动作。
竞态条件这个术语很容易与另一个相关术语“数据竞争(Data Race)”相混淆。数据竞争是指,如果在访问共享的非final类型的域时没有采用同步来进行协同,那么就会出现数据竞争
延迟初始化的竟态条件
@NotThreadSafe
public class LazyInitRace{
private ExpensiveObject instance=null;
public ExpensiveObject getInstance(){
if(instance==null) //a线程和b线程同时执行
instance=new ExpensiveObject();
return instance;
}
}复合操作
要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中.
@ThreadSafe
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.atomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。通过用AtomicLong来代替long类型的计数器,能够确保所有对计数器状态的访问操作都是原子的。[1]由于Servlet的状态就是计数器的状态,并且计数器是线程安全的,因此这里的Servlet也是线程安全的
在实际情况中,应尽可能地使用现有的线程安全对象(例如AcomicLong)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性
加锁机制
当在Servlet中添加一个状态变量时,可以通过线程安全的对象来管理Servlet的状态以维护Servlet的线程安全性。但如果想在Servlet中添加更多的状态,那么是否只需添加更多的线程安全状态变量就足够了?
@NotThreadSafe
public class UnsafeCachingFactorizer 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);
}
}
}然而,这种方法并不正确。尽管这些原子引用本身都是线程安全的,但在UnsafeCaching Factorizer中存在着竞态条件,这可能产生错误的结果.
在线程安全性的定义中要求,多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。UnsafeCachingFactorizer的不变性条件之一是:在lastFactors中缓存的因数之积应该等于在lastNumber中缓存的数值。只有确保了这个不变性条件不被破坏,上面的Servlet才是正确的
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量
内置锁
Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)
synchronized(lock){
//访问或修改由锁保护的共享状态
}
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁。如果B永远不释放锁,那么A也将永远地等下去。
这种同步机制使得要确保因数分解Servlet的线程安全性变得更简单。在程序清单2-6中使用了关键字synchronized来修饰service方法,因此在同一时刻只有一个线程可以执行service方法。现在的SynchronizedFactorizer是线程安全的。然而,这种方法却过于极端,因为多个客户端无法同时使用因数分解Servlet,服务的响应性非常低,无法令人接受。这是一个性能问题,而不是线程安全问题.
程序清单2-6 这个Servlet能正确地缓存最新的计算结果,但并发性却非常糟糕(不要这么做)
@ThreadSafe
public class SynchronizedFactorizer implements Servlet{
@GuardedBy("this")private BigInteger lastNumber;
@GuardedBy("this")private BigInteger[]lastFactors;
public synchronized void service(ServletRequest req,
ServletResponse resp){
BigInteger i=extractFromRequest(req);
if(i.equals(lastNumber))
encodeIntoResponse(resp, lastFactors);
else{
BigInteger[]factors=factor(i);
lastNumber=i;
lastFactors=factors;
encodeIntoResponse(resp, factors);
}
}
}
重入
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放
如果内置锁不是可重入的,那么这段代码将发生死锁
public class Widget{
public synchronized void doSomething(){
……
}
}
public class LoggingWidget extends Widget{
public synchronized void doSomething(){
System.out.println(toString()+":calling doSomething");
super.doSomething();
}
用锁来保护状态
由于锁能使其保护的代码路径以串行形式来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。只要始终遵循这些协议,就能确保状态的一致性