首页 > 代码库 > Java 并发编程(一)浅谈线程安全

Java 并发编程(一)浅谈线程安全

        首先我们要弄清楚什么叫线程安全。

        “线程安全”是指:当多个线程访问某个类时,不管运行环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

        这里有三个关键点,

                第一、“线程安全”问题是来源于多个线程访问的情况下,当个线程没有竞争便涉及到出现线程安全的问题。

                第二、类的“线程安全”性不依赖于多线程的执行顺序。

                第三、主调代码不需要同步或协同。某个类“线程安全”特性不依靠外部调用者。

        那是不是在未进行同步或协同等同步手段处理之前,所有的类都不是线程安全的呢? 非也。

        我们知道,对象分两种,有状态对象(Stateful Bean)和无状态对象(Stateless Bean)。

        有状态对象就是有数据存储功能,就是有实例变量的对象 ,可以保存数据,是非线程安全的。

        无状态对象就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。

        举个例子,在基于Servlet 的 Web 服务中,如果存在如下的一个 Servlet ,它没有成员变量,不能储存数据。不管有多少个线程使用它,每个线程都会在自己独立的 Java 栈,方法内所有的局部变量都是新建且独享的。它就是无状态对象,是线程安全的。

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

        如果为它添加一个用来统计访问用户数的变量 count 时,如下代码,由于这个Servlet 会被多个线程访问,count 变量也会被共享。在对 count 进行自增操作的时候,由于 count++  实际上需要分两步执行,即 tem = count +1 ; count = temp。假设初始值 count =1; 有线程 A 和 B 访问它。当 A 取 count=1后,进行 count++ 之前,线程B接着取 count = 1; 那么 在A B 访问完成之后,count 结果为 2,这显然是错误的。那么这个Servlet 就变成了非线程安全的了。
@NotThreadSafe
public class StatelessFactorizer 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);
	}
}

        由于不恰当的执行顺序而出现不正确的执行结果是一件很头疼的事情,它有一个正式的名字:竞态条件(Race Condition)。要避免竞态条件的问题,我们就要把自增这个动作变为原子操作。假设有两个操作 A 和 B ,如果从执行 A 的线程来看,当另一个线程执行B时,要么将B完全执行完,要么完全不执行B,那么 A 和 B 对彼此来说就是原子的。原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个原子方式执行的操作。

        为了实现count自增的原子性,我们可以使用 AtomicLong 类来实现它。

@ThreadSafe
public class StatelessFactorizer implements Servlet{
	private AtomicLong count = new AtomicLong(0);
	public AtomicLong getCount(){
		return count;
	}
	public void service (ServletRequest req , ServletResponse resp){
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = factor(i);
		count.incrementAndGet();
		encodeIntoResponse(resp,factors);
	}
}

        当 Servlet 中只有一个状态变量的时候,可以通过线程安全的对象来管理 Servlet 的状态以维护 Servlet 的线程安全性。如果在 Servlet 中添加更多的状态,即使添加更多的线程安全状态变量也不能保证Servlet 的安全性。如下面代码。
@ThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
	private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
	private final AtomicReference<BigInteger[]> lastFactors = new AtocmicReference<BigInteger[]>();

	public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
		if (i.equals(lastNumber.get()))
			encodeIntReponse(resp, lastFactors.get());
		else {
			BigInteger[] factors = factor(i);
			lastNumber.set(i);
			lastFactors.set(factors);
			encodeIntoResponse(resp, factors);
		}
	}
}

        这段代码本意是为因子分解增加一个缓存的功能。虽然这些原子引用本身都是线程安全的,但是由于存在竞态条件,这可能产生错误的结果。

        在拥有多个状态变量的对象中,如果各个变量之间并不是彼此独立存在的(例如这里的 lastNumber 和 lastFactors 必须保证一致对应),而是其中某个变量的值会对其他变量的值产生约束。因此,当更新某一个变量的时候,需要在同一个原子操作中对其他变量同时进行更新。

内置锁

        Java提供了一种内置的锁机制来支持原子性:同步代码块(synchronized block),同步代码块包含2个部分:1,作为锁的对象引用;2,该锁所保护的代码块

        因此,我们可以对 UnsafeCachingFactorizer 的 service 进行同步,即声明为  public synchronized void service(ServletRequest req, ServletResponse resp) ;这样,所有对service 的访问都会按串行排列。但是这种方法过于极端,服务的响应性非常低,无法令人接受。这是一个性能问题,而不是线程安全问题。

        内置锁是可重入锁。“重入”意味着锁的操作的粒度是“线程”而不是“调用”。重入的一种实现方法是,为每个锁关联一个获取数值和一个所有者线程。当计数值为0时,这个所就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM 将记下锁的持有者,并且将获取计数值置为1.如果同一个线程再次获取这个锁,计数值将被递增,而当线程退出同步代码块时,计数器会相应的递减。当计数值为0时,这个锁将被释放。

        由于锁能使其保护的代码以串行形式来访问,因此就可以通过锁来构造一些协议以实现对共享状态的独占访问。一种常见的加锁约定是,将所有的可变状态封装在对象内部,然后通过对象的内置锁对所有访问可变状态的方法进行同步,使得在该对象上不会发生并发访问。对于每个包含多个变量的不变形条件,其中涉及的所有的变量都需要由同一个锁保护。

        如果同步可以避免竞态条件问题,那么为什么不在每个方法声明时都是用关键字 synchromized ?事实上,如果不加区别的滥用,可能导致程序中出现过多的同步。此外,如果只是将每个方法都作为同步方法,例如 Vector ,那么并不能保证 Vector 上复合操作都是原子的:

if(!vector.contains(element))
			vector.add(element);
        虽然 contains 和 add 等方法都是原子方法,但在上面这个“如果不存在则添加(put-if-absent)” 的操作中仍然存在竞态条件。虽然 synchronized 方法可以确保单个操作的原子性,反如果要把多个操作合并为一个复合操作,还需要额外的加锁机制。此外,正如之前将整个 service 方法同步的做法一样,将每个方法都都作同步还可能导致活跃性问题和性能问题。

        幸运的是,通过缩小同步代码块的作用范围,我们很容易做到既保证servlet 的并发性,同时又维护线程安全性。要确保同步代码块不要过小,并且不要将本应是原子的操作拆分到多个同步的代码块中去,从而在这些操作的执行过程中,其他线程可以访问共享状态。

        下面,我们将修改为使用两个独立的同步代码块,每个同步代码块都只包含一小段代码。其中一个同步代码块负责保护判断是否只需返回缓存结果的“先检查后执行”操作序列,另一个同步代码块则负责确保对缓存的数值和引述分解结果进行同步操作。此外,我们还重新引入了"命中计数器“,添加了一个”缓存命中“计数器,并且在第一个同步代码块中更新这两个变量。由于这两个计数器也是共享可变状态的一部分,因此必须在所有访问它们的位置上都是用同步。位于同步代码块之外代码将以独占方式来访问局部(位于栈中)变量,这些变量不会在多个线程间共享,因此不需要同步。

public class CachedFactorizer implements Servlet {
	private BigInteger lastNumber;
	private BigInteger[] lastFactors;
	private long hits;
	private long cacheHits;

	// 也可以将 hits 和 cacheHits 声明为 volatile 来实现可见性
	public synchronized long getHits() {
		return hits;
	}

	public synchronized double getCacheHitRatio() {
		return (double) cacheHits;
	}

	public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = null;
		synchronized (this) {
			++hits;
			if (i.equals(lastNumber)) {
				++cacheHits;
				//使用克隆,缩短同步代码块大小
				factors = lastFactors.clone();
			}
		}
		if (factors == null) {
			factors = factor(i);
			synchronized (this) {
				lastNumber = i;
				lastFactors = factors.clone();
			}
		}
	}
}

Java 并发编程(一)浅谈线程安全