当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但同步这个属于还包括volatile类型的变量,显式锁以及原子变量。
2.1 什么是线程安全性
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
线程安全性的使用并非来源于对线程的直接使用,而是使用像servlet这样的框架。
//一个无状态的servlet
//这是一个简单的因数分解servlet
@ThreadSafe
public class StatelessFactorizer implements Servlet{
public void service(ServletRequest req,ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp,factors);
}
}
与大多数servlet相同,上面的servlet是无状态的:它既不包含任何域,也不包含任何对其它类中域的引用。
无状态对象一定是线程安全的。
大多数servlet都是无状态的,从而极大的降低了在实现servlet线程安全性时的复杂性。只有当servlet在处理请求时需要保存一些信息,线程安全性才会成为一个问题。
2.2 原子性
++count:它看上去是一个操作,但这个操作并不是院子原子的,它包含三个独立的操作——读取count的值,将值加1,将计算结果写入count。
竞态条件:在并发编程中由于不恰当的执行时序而出现不正确的结果。
@NotThreadSafe
public class LazyInitRace{
private ExpensiveObject instance = null;
public ExpensiveObject getInstance(){
if(instance == null)
instance = new ExpensiveObject();
return instance;
}
}
使用“先检查后执行”的一种常见情况就是延迟初始化。延迟初始化的目的就是将对象的初始化操作推迟到实际被使用时才进行,同时要确保只被初始化一次。
A,B同时执行这个程序,有可能都读到instance为null,这是都会创建一个新的instance,最后调用getInstance时会返回不同的结果。
在Java.util.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。通过用AtomicLong来代替long类型的计数器,能够确保所有对计数器状态的访问操作都是原子的。下面是一个例子:
@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);
}
}
2.3 加锁机制
2.3.1 内置锁
Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。而无论是通过正常的控制路径退出还是通过从代码中抛出异常退出,获得内置锁的唯一途径就是进入由这个锁保护的同步代码快或方法。
Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。
//这个servlet能正确的缓存最新的计算结果,但并发性却非常糟。
@ThreadSafe
public class SynchronizedFactorizer implements Servlet{
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lasstFactors;
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);
}
}
}
2.3.2 重入
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。
如果内置锁不是可重入的,那么这段代码将发生死锁
public class Widget{
public synchronized void doDomething(){
...
}
}
public class LoggingWidget extends Widget(){
System.out.println(toString()+":calling doSomething");
super.doSomething();
}
2.4 活跃性和性能
当多个请求同时到达因数分解Servlet时,这些请求将排队等待处理。我们将这种web应用程序称之为不良并发应用程序:可同时调用的数量不仅受到可用处理资源的限制,还受到应用程序本身结构的限制。
解决方案:通过缩小同步代码块的作用范围,很容易做到既确保servlet的并发性同时又维护线程安全性。要确保同步代码块不要过小,并且不要将本应是原子的操作拆分到多个同步代码块中。应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。
下面的程序清单中,CatchedFactorized将Servlet的代码修改为使用两个独立的同步代码块,每个同步代码块都只包含一小段代码。其中一个同步代码块负责保护判断是否只需返回缓存结果的“先检查后执行”操作序列,另一个同步代码块则负责确保对缓存的数值和因数分解结果进行同步更新。此外,我们还重新引入了“命中计数器”,添加了一个“缓存命中”计数器,并在第一个同步代码块中更新这两个变量。由于这两个计数器也是共享可变状态的一部分,因此必须在所有访问他们的位置上都使用同步。位于同步代码块之外的代码都将以独占方式来访问局部(位于栈上的)变量,这些变量不会再多个线程间共享,因此不需要同步。
@ThreadSafe
public class CatchedFactorizer implements Servlet{
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
@GuardedBy("this") private long hits;
@GuardedBy("this") private long catchHits;
public synchronized long getHits(){
return hits;
}
public synchronized double getCatchedHitRatio(){
return (double)catchHits/(double)hits;
}
public void service(ServletRequest req,ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
synchronized (this){
++hits;
if(i.equals(lastNumber)){
++catcheHits;
factors = lastFactors.clone();
}
}
if(factors == null){
factors = factor(i);
synchronized this(){
lastNumber = i;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp,factors);
}
}