首页 > 代码库 > Java并发编程学习笔记(一)线程安全性 1

Java并发编程学习笔记(一)线程安全性 1

什么是线程安全性:

   要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。“共享”意味着变量可以由多个线程同时访问,而“可变”则意味着变量的值在其生命周期内可以发生变化。

   一个对象是否需要线程安全的,取决于他是否被多个线程访问。这指的是在程序中访问对象的方式,而不是对象要实现的功能。要使得对象时线程安全的,需要采用同步机制来协同对对象可变状态的访问。如果无法实现协同,那么可能导致数据破坏以及其他不该出现的结果。

如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:1.不在线程之间共享该状态变量;2.将状态变量修改为不可变的变量;3.在访问状态变量时使用同步。

   在线程安全性的定义中,最核心的概念就是正确性。正确性的含义是,某个类的行为与其规范完全一致。当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么称这个类是线程安全的。

   来看一个基于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一样,StatelessFactorizer是无状态的,它不包含任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态仅存在于线程栈上的局部变量中,并只能有正在执行的线程访问。因此无状态对象一定是线程安全的


原子性

   假设我们希望增加一个“命中计数器”来统计所处理得请求数量。一种直观的方法是在Servlet中增加一个long类型的域,并且每处理一个请求就将这个值加1:

@NotThreadSafe                  //不好的代码
public class UnsafeCountingFactorizer implements Servlet {
    private long counts = 0;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           
    public long getCounts(){return counts;}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
    public void service (ServletRequest req,ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++counts;
        encodeIntoResponse(resp,factors);
    }
}


UnsafeCountingFactorizer是非线程安全的。虽然递增造作++counts是一种紧凑的语法,使其看上去只是一个操作,但这个操作并非原子的,因而他并不会作为一个不可分割的操作来执行。它包含了三个独立的操作“读取counts ——> 修改counts+1 ——> 写入counts”。 假设计数器的初始值为9,那么在某些情况下,每个线程督导的值都是9,接着执行递增操作,并且都将计数器的值设为10,那么命中计数器的值将会偏差1.这种由不恰当的执行时序而出现的不正确的结果是一种竞态条件。

   当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。最常见的竞态条件类型就是“先检查后执行”操作:通过一个可能失效的观测结果决定下一步的动作。

   “先检查后执行”的一种常见情况就是延迟初始化,即将对象的初始化操作推迟到实际被使用时才进行,同时要确保只备初始化一次。

@NotThreadSafe
public class LazyInitRace {
    private ExpensiveObject instance = null;
    public ExpensiveObject getInstance() {
        if(instance == null)
            instance = new ExpensiveObject();
        return instance;
    } 
}


   在LazyInitRace中包含了一个竞态条件,它可能会破坏这个类的正确性。

LazyInitRaceUnsafeCountingFactorizer都包含一组需要以原子方式执行的操作。要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。

   如果UnsafeCountingFactorizer的递增操作是原子操作,那么竞态条件就不会发生。为了确保线程安全性,“先检查后执行”和“读取-修改-写入”等操作必须是原子的。我们把“先检查后执行”和“读取-修改-写入”等操作统称为复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。

@ThreadSafe            
public class CountingFactorizer implements Servlet {
    private final AtomicLong counts = new AtomicLong(0);
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         
    public long getCounts(){return counts.get();}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
    public void service (ServletRequest req,ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        counts.incrementAndGet();
        encodeIntoResponse(resp,factors);
    }
}


   在实际情况下,应尽可能地使用现有的线程安全对象来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性。


加锁机制

   当在Servlet中添加状态变量时,可以通过线程安全的对象来管理Servlet的状态以维护Servlet的线程安全性。但如果想在Servlet中添加更多的状态,只添加更多线程安全状态变量是不够的。我们希望提升Servlet的性能:将最近的计算结果缓存起来,dangliangge 连续的请求对相同的数值进行因数分解时,可以直接使用上一次的结果,而无需重新计算。这时,需要保存两个状态:最近执行因数分解的数值,以及分解结果。前面通过AtomicLong以线程安全的方式来管理计数器状态,是否可以使用类似的AtomicReference来管理最近执行因数分解的数值以及分解结果?

@ThreadSafe          
public class CountingFactorizer 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,lastNumber.get());
        else{
            BigInteger[] factors = factor(i);
            lastNumber.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(resp,factors);
        }
    }

   尽管这些原子引用本身都是线程安全的,但在UnsafeCachingFactorizer中存在着竞态条件,这可能产生错误的结果。

   在线程安全性的定义中要求,多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性不被破坏。UnsafeCachingFactorizer的不变性条件之一是:在lastFactors中的缓存的因数之积应该等于在lastNumber中的缓存值。当在不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此,当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。

   在上述代码中,尽管set方法的每次调用都是原子的,但仍然无法同时更新lastNumber和lastFactors。如果只修改了其中一个变量,那么在这两次修改操作之间,其他线程将发现不变性条件被破坏了。

   所以,要保持状态的一致性,就需要在单原子操作中更新所有相关的状态变量。

   未完待续... Java并发编程学习笔记(一)线程安全性 2    



本文出自 “Programming in XMU” 博客,请务必保留此出处http://liuxp0827.blog.51cto.com/5013343/1412874