线程安全性:
- 要编写线程安全的代码,其核心就是要对状态访问操作进行管理,特别是共享的(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), 一定不要持有锁。
不吝指正。