<Java 并发编程实战>抄书笔记之线程安全性

来源:互联网 发布:c语言socket编程小例子 编辑:程序博客网 时间:2024/05/19 14:55

一.线程安全性

1.线程安全性概述

编写线程安全得代码,其核心在于要对状态访问操作进行管理,特别是对共享得(shared)和可变得(mutable)状态得访问,从非正式得意义上来说,对象得状态是指存储在状态变量(例如实例和静态域)中得数据,对象得状态可能包括其他依赖对象得域,例如,某个HashMap得状态不仅存储在HashMap对象本身,还存在许多Map.Entry对象中,在对象得状态中包含了任何可能影星其外部可见行为得数据。

共享意味着变量可以由多个线程同时访问,而可变则意味着变量得值在其生命周期内可以发生变化。一个对象是否需要线程安全得,取决于它是否被多个线程访问。这指的是在程序中访问对象得方式,而不是对象要实现得功能。要使得对象是线程安全得,需要采用同步机制来协同对对象可变状态得访问。如果无法实现协同,会出现无法预料得后果。

当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量得访问。java中主要得同步机制是关键字synchronized,它提供了一种独占得加锁方式,但同步这个术语还包括volatile类型得变量,显示锁以及原子变量。

如果当多个线程访问同一个可变得状态变量时没有使用合适得同步,那么程序就会出现错误,有三种方式可以修复这个问题:

1.不在线程之间共享该状态变量

2.将状态变量修改为不可变得变量

3.在访问该状态变量得时候使用同步

java语言并没有强制要求将状态都封装在类中,开发人员完全可以将状态保存在某个公开得域中,甚至是公开得静态域中,或者是提供一个对内部对象得公开引用,然而,程序状态得封装性越好,就越容易实现程序得线程安全性,并且代码得维护人员也越容易保持这种方式。

2.什么是线程安全性

在线程安全性得定义中,最核心得概念就是正确性。正确性得含义是:某个类得行为与其规范完全一致。在良好得规范中通常会定义各种不变性条件来约束对象得状态,以及定义各种后验条件来描述对象得操作结果。当多个线程访问某个类得时候,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外得同步和协同,这个雷都能表现出正确得行为,那么这个类是线程安全得。在线程安全类得对象实例上执行得任何串行和并行操作都不会使对象处于无效状态。

2.1一个无状态的servlet

@ThreadSafe
public class StatelessFactorizer implement Servlet{
public void service(ServletRequest req,ServletResponse resp){
BigInteger i = extractFormRequest(req);
BigInteger [] factors = factor(i);
encodeIntoResponse(resp,factors);
}
}

与大多数Servlet相同StatelessFactorizer是无状态的:它即不包含任何域,也不包含任何对其他类中的引用,计算过程中的临时状态仅存在于线程栈上的局不变量中,并且只能由正在执行的线程访问,两个县城之前没有共享状态,就像他们都在访问不同的实例。由于线程访问无状态对象的行为并不会影响其他线程中操作的正确性,因此无状态对象是线程安全的。大多数servlet都是无状态的,只有当servlet在处理请求时,需要保存一些信息,线程安全才会成为一个问题。

2.2原子性

publicclassUnsafeCountingFactorizer implements Servlet {

privatelongcount = 0;

public long getLong(){returncount;}

@Override

public void service(ServletRequest arg0, ServletResponse arg1)

throws ServletException, IOException {

// TODO Auto-generated method stub

BigInteger i = extractFromRequest(arg0);

BigInteger[] factors = factor(i);

++count;

encodeIntoResponse(arg1,factors);

}

}

该类是非线程安全的,尽管它在单线程中能正确运行,但是这个类可能会丢失一些更新操作。因为++count这个操作是非原子性的,因而它并不会作为一个不可分割的操作来执行。这是一个读取-修改-写入的操作序列,并且其结果依赖于之前的状态。

2.2.1 竞态条件

在上述的类中存在多个竞态条件,从而使结果变的不可靠。当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。换句话说,就是正确的结果要取决于运气。最常见的竞态条件类型就是先检查后执行操作。即通过一个可能失效的观测结果来决定下一步的动作。

2.2.2延迟初始化中的竞态条件

使用先检查后执行的一种常见情况就是延迟初始化。延迟初始化的目的是将对象的初始化操作推迟到实际被使用时才进行,同时要确保只被初始化一次。单例设计模式是最常见的例子。

@NotThreadSafe

public class LazyInitRace{

private ExpensiveObject instance = null;

public ExpensiveObject getInstance(){

if(instance == null)

instance = newExpensiveObject();

return instance;

}

}

在上述的类中,也存在竞态条件,会导致程序出现的不一致性,假设线程a和线程b同时执行getinstance,a看到instance为空,因而创建了一个新的ExpensiveObject对象,b同样要判断instance是否为空,此时的instance是否为空,要取决于不可预测的时序,包括线程的调度方式,以及a需要花多长时间来初始化ExpensiveObject并设置instance,如果b检查instance为空,那么两次调用getinstance可能会得到不同的结果。

2.2.3 复合操作

lazyinitrace和UnsafeCountingFactorizer都包含一组需要以原子方式执行(或者说不可分割的操作)。要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或者之后读取和修改状态,而不是在修改状态的过程中。假定有两个操作a和b,如果从执行a的线程来看,当另一个线程执行b的时候,要么将b全部执行完,要么完全不执行b,那么a和b对彼此来说都是原子的。原子操作是指,对于访问同一个状态的所有操作来说,这个操作是一个以原子方式执行的操作。我们可以使用现有的线程安全类:

@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 = extractFormRequest(req);

BigInteger [] factors = factor(i);

count.incrementAndGet();

encodeIntoResponse(resp,factors);

}

}

在java.util.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象上的原子状态转换。通过使用atomiclong来代替long类型的计数器,能够确保所有对计数器状态的访问都是原子的。在实际情况中应尽可能的使用现有的线程安全对象,来管理类的状态,与非线程安全的对象相比,判断线程安全对象的可能状态及状态转换情况要更为容易,从而也更容易维护和验证线程安全性。

2.3加锁机制

当在Servlet中添加一个状态变量时,可以通过线程安全的对象来管理servlet的状态以维护servlet的线程安全性。

假设我们希望提升servlet的性能:将最近的计算结果缓存起来,当两个连续的请求对相同的数值进行因数分解时,可以直接使用上一次的计算结果,而无需重新计算。要实现该缓存策略,需要保存两个状态:最近执行因数分解的数值,以及分解结果。

我们曾通过atomiclong以线程安全的方式来管理计数器的状态,那么,在这里是否可以使用类似的atomicreference来管理最近执行因数分解的数值及其分解结果吗?如程序所示:

@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(factors);

encodeIntoResponse(resp,factors);

}

}

}

 然而这种方法并不正确,尽管这些原子引用本身都是线程安全的,但在UnsafeCachingFactorizer中存在竞态条件,这可能产生错误的结果。在线程安全性的定义中要求,多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性不被破坏。UnsafeCachingFactorizer的不变性条件之一是:在lastfactors中缓存的因数之积应该等于在lastnumber中缓存的值。当在不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量产生约束。因此当更新某一个变量时,需要在同一个原子操作中对其他变量进行同时更新。

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