什么是线程安全性:java
要编写线程安全的代码,其核心在于要对状态访问操做进行管理,特别是对共享的和可变的状态的访问。“共享”意味着变量能够由多个线程同时访问,而“可变”则意味着变量的值在其生命周期内能够发生变化。
编程
原文出处:http://liuxp0827.blog.51cto.com/5013343/1412874
缓存
一个对象是否须要线程安全的,取决于他是否被多个线程访问。这指的是在程序中访问对象的方式,而不是对象要实现的功能。要使得对象时线程安全的,须要采用同步机制来协同对对象可变状态的访问。若是没法实现协同,那么可能致使数据破坏以及其余不应出现的结果。
安全
若是当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式能够修复这个问题:1.不在线程之间共享该状态变量;2.将状态变量修改成不可变的变量;3.在访问状态变量时使用同步。多线程
在线程安全性的定义中,最核心的概念就是正确性。正确性的含义是,某个类的行为与其规范彻底一致。当多个线程访问某个类时,无论运行时环境采用何种调度方式或者这些线程将如何交替执行,而且在主调代码中不须要任何额外的同步或协同,这个类都能表现出正确的行为,那么称这个类是线程安全的。
并发
来看一个基于Servlet的因数分解服务,并逐渐扩展它的功能,同时确保它的线程安全性。{}less
//原文出处:http://liuxp0827.blog.51cto.com/5013343/1412874 @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是无状态的,它不包含任何域,也不包含任何对其余类中域的引用。计算过程当中的临时状态仅存在于线程栈上的局部变量中,并只能有正在执行的线程访问。所以无状态对象必定是线程安全的。ide
原子性性能
假设咱们但愿增长一个“命中计数器”来统计所处理得请求数量。一种直观的方法是在Servlet中增长一个long类型的域,而且每处理一个请求就将这个值加1:学习
//原文出处:http://liuxp0827.blog.51cto.com/5013343/1412874 @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.这种由不恰当的执行时序而出现的不正确的结果是一种竞态条件。
当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。最多见的竞态条件类型就是“先检查后执行”操做:经过一个可能失效的观测结果决定下一步的动做。
“先检查后执行”的一种常见状况就是延迟初始化,即将对象的初始化操做推迟到实际被使用时才进行,同时要确保只备初始化一次。
//原文出处:http://liuxp0827.blog.51cto.com/5013343/1412874 @NotThreadSafe public class LazyInitRace { private ExpensiveObject instance = null; public ExpensiveObject getInstance() { if(instance == null) instance = new ExpensiveObject(); return instance; } }
在LazyInitRace中包含了一个竞态条件,它可能会破坏这个类的正确性。
LazyInitRace和UnsafeCountingFactorizer都包含一组须要以原子方式执行的操做。要避免竞态条件问题,就必须在某个线程修改该变量时,经过某种方式防止其余线程使用这个变量,从而确保其余线程只能在修改操做完成以前或以后读取和修改状态,而不是在修改状态的过程当中。
若是UnsafeCountingFactorizer的递增操做是原子操做,那么竞态条件就不会发生。为了确保线程安全性,“先检查后执行”和“读取-修改-写入”等操做必须是原子的。咱们把“先检查后执行”和“读取-修改-写入”等操做统称为复合操做:包含了一组必须以原子方式执行的操做以确保线程安全性。
//原文出处:http://liuxp0827.blog.51cto.com/5013343/1412874 @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来管理最近执行因数分解的数值以及分解结果?
//原文出处:http://liuxp0827.blog.51cto.com/5013343/1412874 @NotThreadSafe public class UnsafeFactorizer 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