首页 > 代码库 > java线程(二) - 线程安全性

java线程(二) - 线程安全性

前言: 
     要编写线程安全的代码,其核心在于要对状态访问的操作进行管理,特别是对共享的和可变的状态的访问。
     当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误,有三种方式可以修复这个问题:
  1. 不在线程之间共享该状态变量
  2. 将状态变量修改为不可变的变量
  3. 在访问状态变量时使用同步

线程安全性的定义:
     当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
     那什么是类的正确行为?不会违反约束对象状态的各种不变性条件以及描述对象操作结果的各种后验条件。                  

     无状态对象一定是线程安全的
@ThreadSafe
public class StatelessFactorizer implements Servlet{
public void service(ServletRequest req,ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp,factors);
         }
}
     由于线程访问无状态对象的行为并不会影响其他线程中操作的正确性,因此无状态对象是线程安全的。

原子性:
     在并发编程中,由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,称为竞速条件。
     例:
     
@NotThreadSafe
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);
}
}

     上面的UnsafeCountingFactorizer类并发线程安全的,是由于++count,这是一个"读取-修改-写入"的操作序列,并且其结果依赖与之前的状态。当一个线程执行在++count中某一步时,可能会被其他线程给打断,所以在多个线程在并发访问这段代码时,容易造成对象严重的数据完整性问题。
     最常见的竞速条件类型是"先检查后执行(check then act)"操作,即通过一个可能失效的观测结果来决定下一步的动作。 以及常见的"读取-修改-写入"操作。
     通常可以将引起竞速条件的复合操作转换为原子操作来保证线程安全性,加锁是最常见的一种做法。
  在单状态的情况下对于"读取修改写入操作"可以使用atomic包中一些设施来保证复合操作的线程安全性,如AtomicLong。
     @ThreadSafe
public class UnsafeCountingFactorizer 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);
     }
}

加锁机制:
     当在Servlet中添加一个状态变量时,可以通过线程安全的对象来管理Servlet的状态以维护Servlet的线程安全性。当类中含有多个状态时,是否只需要添加多个线程安全状态变量就够了?
     以下例子假设要为Servlet的因式分解提高性能,当两个连续的请求对相同的数值进行因式分解时,可以直接使用上一次的计算结果,而无须重新计算。
     @NotThreadSafe
public class UnsafeCacheingFactorizer implements Servlet {
     private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
     private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
     public long getCount() { return count.get() ;}
     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,lastFactors.get());
          }
     }
}

     UnsafeCachingFactorizer的不变性条件之一是:在lastFactors中缓存的因数之积应该等于在lastNumber中缓存的数值。只有确保了这个不变性条件不被破坏,上面的Servlet才是正确的。而事实的情况是,尽管set方法是原子的,但仍然无法同时更新lastNumber和lastFactors,同样,我们也不能保证会同时获取两个值。
     当类中含有多个状态变量时,应保证这些状态变量的一致性。要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。这时就需要用到锁。
     内置锁:synchronized方法以class对象作为锁。每个java对象都可以用作一个实现同步的锁,这些锁被称为内置锁或监视锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时会释放锁。Java的内置锁相当于一种互斥锁,这意味着最多只有一个线程能持有这种锁。
     使用synchronized修改上面例子:
     @ThreadSafe
public class SynchronizedFactorizer implements Servlet {
     @GuardedBy("this") private final BigInteger lastNumber;
     @GuardedBy("this") private final 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);
          }
     }
}
     这里虽然保证线程安全性,但是并发性却降到了极致,一会我们将继续进行改进。
     重入:在java中,内置的锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。"重入"意味着获取锁的操作的粒度是"线程",而不是"调用"。
     例:如果内置锁不是可重入的,那么这段代码将发生死锁
public class Widget {
     public synchronized void doSomething() {
          .....
     }     
}
public class LoggingWidget extends Widget {
     public synchronized void doSomething() {
          ......
          super.doSomething();
     }
}

用锁来保护状态:
     对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。
     对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护,使得其他线程来破坏这里完整性条件变得不可能。

活跃性与性能:
     当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络IO或控制台IO),一定不要持有锁。
     通常我们可以通过缩小同步代码来提高程序的并发性,但判断同步代码块的合理大小,需要在各种设计需求之间进行权衡,包括安全性(这个需求必须要得到满足)、简单性和性能。
     通常,在简单性与性能之间存在着相互制约因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)。
     在我们之前使用的SynchronizedFactorizer类中,Service是一个synchronized方法,因此每次只有一个线程可以执行,这样就违背了Servlet框架的初衷,并发性为0,就算存在多个cpu,后一个servlet请求也得等着前一个请求完成才能进行下一步。
     下面我们将对同步代码块减小范围,将计算步骤独立于同步之外,并且利用同一个锁来保护类的不变性条件。
@ThreadSafe
public class CachedFactorizer implements Servlet {
@GuardedBy("this") private final BigInteger lastNumber;
@GuardedBy("this") private final BigInteger[] lastFactors;
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = null;
synchronized(this) {
if(i.equals(lastNumber)) {
     factors = lastFactors.clone();
}
if(factors == null){
     BigInteger[] factors = factor(i);
     synchronized (this) {
          lastNumber = i;
          lastFactors = factors.clone();
      }
}
encodeIntoResponse(resp,lastFactors);
}
}


java线程(二) - 线程安全性