java并发编程(一): 线程安全性

原创
2014/03/22 13:17
阅读数 1.9K

线程安全性:

  • 要编写线程安全的代码,其核心就是要对状态访问操作进行管理,特别是共享的(Shared)可变的(Mutable)状态的访问;
  • “共享”:多个线程可访问同一变量;
  • “可变”:变量值在声明周期内可变化;
  • java的同步机制:独占锁synchronized, volatile, 显示锁Lock原子变量;
  • 编写并发程序的原则:1.代码正确运行;2.提高代码速度(需求所需时);

什么是线程安全性:

  • 其核心概念就是正确性:即某个类的行为与其规范完全一致:
  • 线程安全性:当多个线程访问某个类时,这个类始终都能表现出正确的行为,这个类就是线程安全的;
  • 无状态对象一定是线程安全的,如:
/**
 * 该类无任何属性域,且不包含任何其他类中域的引用
 * 即计算中的状态都为临时状态,保存在线程栈中
 * 由JMM知线程栈线程私有,仅由当前线程可访问
 * 因此线程安全
 */
@ThreadSafe
public class StatelessFactorizer implements Servlet {
	@Override
	public void service(ServletRequest req, ServletResponse repo) {
		Map<String, Object> params = extractFromRequest(req);
		Map<String, Object> res = doBussiness(params);
		reponseTo(res);
	}
        ...
}

原子性:

看一个非线程安全的版本:

/**
 * 竞态条件(由于不恰当的执行时序而出现不正确的结果)导致非线程安全
 */
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
	private long count = 0;
	
	@Override
	public void service(ServletRequest req, ServletResponse repo) {
		count++; //该操作并非原子(读取--自增--写入), 导致非线程安全
		Map<String, Object> params = extractFromRequest(req);
		Map<String, Object> res = doBussiness(params);
		reponseTo(res);
	}
}
例如这种情况,预计值为11,结果为10:

  • 用例:延迟初始化的竞态条件:

       看个实例:

/**
 * 延迟初始化的竞态条件
 * 由于多个线程可能同时执行到instance == null,
 * 或者由于实例初始化过程比较耗费时间,
 * 这样有可能所谓的"单例"不再单例
 */
@NotThreadSafe
public class LazyInitRace {
	private ExpensiveObject instance;
	
	private LazyInitRace(){}
	
	public ExpensiveObject getInstance() {
		if (instance == null) {
			instance = new ExpensiveObject();
		}
		return instance;
	}
}

复合操作:

  • 原子操作:对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作(要么全部执行,或全部不执行);
  • 复合操作:包含一组以原子方式执行的操作,如(检查再初始化, 读取-修改-写入);
  • 可通过java提供的原子变量实现原子操作,修改上面的计数器:
/**
 * 可通过java提供的原子变量(Atomic*)
 * 来解决竞态条件引起的非线程安全
 */
@ThreadSafe
public class CountingFactorizer implements Servlet {
	private AtomicLong count = new AtomicLong(0);
	
	@Override
	public void service(ServletRequest req, ServletResponse repo) {
		count.incrementAndGet(); //原子增加,该方法最后会调用Unsafe.compareAndSwapLong本地方法
		Map<String, Object> params = extractFromRequest(req);
		Map<String, Object> res = doBussiness(params);
		reponseTo(res);
	}
       ...
}
  • 当无状态的类中,添加一个状态时,若该状态由线程安全的对象管理,那么这个无状态的类也是线程安全的,如上面这段代码,但是当添加多个状态时就不一定了。

加锁机制:

  • 对于多个由线程安全的对象管理起来的状态组合起来,且几个状态有关联关系时,这时就可能不线程安全了,如:
/**
 * 多个关联线程安全的状态对象导致线程安全
 * 这里我们对lastNumber进行缓存,若请求的值与其相同,直接返回,反之计算
 */
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
	private final AtomicReference<BigInteger> lastNumber = 
			new AtomicReference<>();
	private final AtomicReference<BigInteger[]> lastFactors = 
			new AtomicReference<>();
	
	@Override
	public void service(ServletRequest req, ServletResponse repo) {
	   BigInteger i = extractFromRequest(req);
	   //A线程发现不等,重新计算;(A未计算完)
	   //B线程进入也发现不等,也重新计算(但其实有可能经过A计算后是相等的)
	   //从而达不到缓存效果
	   if (i.equals(lastNumber.get())){
		   reponseTo(i, lastFactors);
	   } else{ 
		   BigInteger[] factors = factor(i);
		   lastNumber.set(i);
		   lastFactors.set(factors);
		   reponseTo(i, factors);
	   }
	}
        ...
}
  • 所以要保持状态的一致性,就需要再单个原子操作中更新所有相关的状态变量

内置锁:

  • java提供的内置锁机制实现: synchronized;
  • 通过synchronized我们可以简单使上面的代码线程安全,但这样性能极低,每次只允许一个请求得到响应,凡人难以接受:
/**
 * 通过synchronized实现线程安全,但性能低下
 */
@ThreadSafe
public class CachingFactorizer implements Servlet {
	private BigInteger lastNumber = new BigInteger("");
	private BigInteger[] lastFactors = new BigInteger[]{};
	
	@Override
	public synchronized void service(ServletRequest req, ServletResponse repo) {
	   BigInteger i = extractFromRequest(req);
	   if (i.equals(lastNumber)){
		   reponseTo(i,lastFactors);
	   } else{ 
		   BigInteger[] factors = factor(i);
		   lastNumber = i;
		   lastFactors = factors;
		   reponseTo(i, factors);
	   }
	}
}

重入:

  • 重入:一个已经持有某对象锁的线程再次请求该该对象锁时,这个请求会成功(synchronized允许重入);
  • “重入”:意味着获取锁的粒度是“线程”而不是“调用”;

用锁来保护状态:

  • 对于可能被多个线程同时访问的可变状态变量,在访问时需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的;
  • 每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁

活跃性与性能:

  • 为了提升上面Servlet处理请求吞吐量,明显不能对整个方法synchronized, 下面对这个实现进行分段synchronized, 但仍必须保证多个状态变化是原子操作;
/**
 * 通过分段synchronized提升性能
 */
@ThreadSafe
public class CachedFactorizer implements Servlet {
	private BigInteger lastNumber = new BigInteger("");
	private BigInteger[] lastFactors = new BigInteger[] {};
	private long hits;
	private long cacheHits;

	@Override
	public void service(ServletRequest req, ServletResponse repo) {
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = null;
		synchronized (this) {
			++hits;
			if (i.equals(lastNumber)) {
				++cacheHits;
				factors = lastFactors;
			}
		}
		if (factors == null) {
			factors = factor(i);
			synchronized (this) {
				lastNumber = i;
				lastFactors = factors;
			}
		}
		reponseTo(i, lastFactors);
	}
       ...
}
  • 通常,在简单性与性能之间存在着相互制约的因素。当实现某个同步策略时,一定盲目地为了性能而牺牲简单性(这可能破坏安全性);
  • 当执行时间较长的计算或者可能无法快速完成的操作时(若网络I/O,控制台I/O), 一定不要持有锁。

不吝指正。

展开阅读全文
加载中
点击加入讨论🔥(8) 发布并加入讨论🔥
打赏
8 评论
88 收藏
3
分享
返回顶部
顶部