编写线程安全的代码,本质上就是管理对状态的访问,并且通常都是共享的,可变的状态
一个对象的状态就是它的数据,存储在状态变量中,比如实例域或静态域。对象的状态还包括了其他的附属对象的域。一个对象的状态包含了任何会对它外部可见行为产生的影响的数据。
共享:指一个变量可以被多个线程访问;所谓可变,是指变量的值在其生命周期内可以使用。我们真正要做的是在,是在不可控制的并发访问中保护数据。
无论何时,只要多于一个线程访问给定的状态变量,并且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。java中首要的同步机制时synchronized关键字,它提供了独占锁。除此之外,术语“同步”还包括volatile变量,显示锁和原子变量的使用。
在没有正确同步的情况下,如果多个线程访问了同一变量,你的程序存在隐患,有三种方法修复他
- 不要跨线程共享变量;
- 使状态变量为不可变的;
- 在任何访问状态变量的时候使用同步
注意:一开始就将一个类涉及成线程安全的比在后期修复它更加容易
1,什么使线程安全性
合理的线程安全性的定义,关键在于正确性概念。
线程安全性:一个类是线程安全的,是指在被多个线程访问时,类可以持续进行正确的行为。
书面语形容—当多个线程访问一个类的时候,如果不同考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及调用代码不变做其他的协调,这个类的行为仍然时正确的,可预测的,那么这个类就是线程安全的
注意:线程安全的类封装了任何必要的同步, 因此客户不需要自己提供
1.1,示例一个无状态的servlet
@ThreadSafe
public class StatelessFactorizer implements Servlet{
public void service(Servlet req; ServletResponse rep){
BigInteger i=extractFromRequest(req);
BigInteger[] factors=factor(i);
encodeIntoResponse(rep,factors);
}
}
上述代码中我们简单的因数分解的Servlet.它可以从Servlet Request中解包数据,然后将这个数据进行因数分解,最后将结果封装到Servlet Response中。StatelessFactorizer像大多数Servlet一样是无状态的:它不包含域也没有引用其他类的域。一次特定的计算的瞬时状态,会唯一地存在本地变量中,这些本地变量存储在线程栈中,只有执行线程才能访问。
注意:无状态的对象是线程安全的
2,原子性
假如我们向无状态的对象中加入一个状态元素会怎样?我们添加命中数(hit counter)来处理请求的数量,显而易见的方法是Servlet中加入一个long类型的域,并在每个请求中递增它,如下所示
@NotThreadSafe
public class UnsafeCountingFacorizer implements Servlet{
private long count=0;
public long getCount(){return counter;}
public void service(Servlet req; ServletResponse rep){
BigInteger i=extractFromRequest(req);
BigInteger[] factors=factor(i);
++count;
encodeIntoResponse(rep,factors);
}
}
UnsafeCountingFacorizer并非线程安全的,尽管它在单线程的环境中运行良好。
++counter并不是原子操作,自增是三个离散操作的简写形式:获取当前值,加1,写回新值。
如果计数器用于生成序列或对象唯一的标识符,多重调用返回相同的结果会导致严重的数据完整性的问题。
1,竞争条件
UnsafeCountingFacorizer中存在数个竞争条件,导致结果是不可靠的,
竞争条件如何产生?当计算机的正确性依赖运行时中相关的时序或者多线程交替时
竞争条件的诱因:为获取期望的结果,需要依赖相关的事件的分时。
竞争条件的特点:使用潜在的过期观察值来做决策或执行计算
检查再运行:你观察到一些事情为真,然后基于你的观察去执行一些动作。但事实上从观察到执行操作的这段时间内,观察结果可能已经失效了(有人执行了操作),从而引发错误(非预期异常)
最常见的一种竞争条件:“检查再运行”,使用一个潜在的过期值作为决定下一步操作的依据
2,示例:惰性初始化中的竞争条件
检查再运行的常见用法时惰性初始化。
惰性初始化的目的:延迟对象的初始化,直到程序真正使用它,同时确保它只初始化一次。下面是用法举例(不要这样做)
@NotThreadSafe
public class LazyInitRace{
private ExpensiveObject instance=null;
public ExpensiveObject getInstance(){
if(instance==null) instance=new ExpensiveObject();
return instance;
}
}
LazyInitRace中的竞争条件会破坏其正确性,比如说线程A和线程B同时执行getInstance(),由于时序,使调度的无常性
3,复合操作
LazyInitRace和UnsafeCountingFacorizer都包含一系列操作,相对于在同一状态下的其他操作而言,必须使原子性的或者不可分割的。为了避免竞争的条件,必须阻止其他线程访问我们正在修改的变量,让我们可以确保:当其他线程想要查看或修改一个状态时,必须在我们的线程开始之前或者完成之后,而不能在操作之后
假设有操作A和B,如果从执行A的线程的角度看,当其他线程执行B时,要么B全部执行完成,要么一点都没有执行,这样A和B互为原子操作。
原子操作:该操作对于所有的操作,包括自己,都满足前面的描述的状态
我们将检查再运行和读改写操作的全部执行过程看作时复合操作:为了保证线程安全,操作必须原子的执行
修复 —使用已有的线程安全类,
@ThreadSafe
public class UnsafeCountingFacorizer implements Servlet{
private final AutomicLong count=new AutomicLong();
public long getCount(){return count.get();}
public void service(Servlet req; ServletResponse rep){
BigInteger i=extractFromRequest(req);
BigInteger[] factors=factor(i);
count.incrementAndGet();
encodeIntoResponse(rep,factors);
}
}
java.util.concurrent.atomic包中包括了***原子变量***类,这些类用来实现数字和对象引用的原子状态的转换。把long类型转换为AtomicLong类型的,我们可以确保所有访问计数器状态的操作到都是原子的。计数器时是线程安全的,而计数器的状态而Servlet的状态,所以我们的Servlet再次成为线程安全的了
利用象AtomicLong这样已有的线程安全对象管理类的状态是非常实用的,相比于非线程安全对象,判断一个线程安全对象的可能状态和状态转换要容易的多,这简化了维护和验证安全性的功能。
3,锁
如果想加入更多的状态,可以仅仅加入更多的线程安全的状态变量吗?
我们缓存最新的计算结果,以应对两个连续的客户请求相同的数字进行因数分解,希望提高由此提高Servlet的性能。要实现这个策略。我们记住两件事:最新请求的数字和它的因数
没有正确原子化的Servlet试图缓存它的最新结果,
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servler{
private final AtomicReference<BigInteger> lastNumber=new AtomicReference<BigInteger[]>();
private final AtomicReference<BigInteger> lastFactors=new AtomicReference<BigInteger[]>();
publilc 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(i);
lastFactors.set(factors);
encodeIntoResponse(resp,factors);
}
}
}
这种方法并不正确。尽管原子引用(atomic reference)自身是线程安全的, 不过UnsafeCachingFactorizer中存在竞争条件,会导致它产生错误的答案。
线程安全性的定义:要求无论是多线程中的时序或者交替操作,都要保证不破坏那些不变的约束。
UnsafeCachingFactorizer中的不变约束:缓存在lastFactors中各个因子乘积应该等于缓存在lastNumber中的数值。
漏洞:当某个线程只修改了一个变量而另一个还没有开始修改时,其他线程将看到Servlete违反了不变约束
为了保护一致性,要在单一的原子操作中更新相互关联的状态变量
###1,内部锁
java提供了强制原子性的内置锁机制:synchronized块
synchronized块:锁对象的引用,以及这个锁保护的代码块。
synchronized方法:是对跨越了整个方法体synchronized块简短描述,锁----》该方法所在的对象本身(静态的synchronized方法从class对象了获取锁)
synchronized(lock){
//访问或修改被锁保护的共享状态
}
每一个java对象都可以隐式地扮演用于同步地锁角色;这些内置地锁被称为内部锁或监视器锁。执行线程进入synchronized块之前自动获取锁,而不论通过正常控制路径推出,还是从块中抛出异常,线程都会放弃对synchronized块的控制自动释放锁,获取内部锁的唯一途径:进入内部锁保护的同步块或方法
内部锁在java中扮演互斥锁(mutual exclusion lock)的角色,意味着只多只有一个线程可以拥有锁,当线程A尝试请求一个被线程B占有的锁时,线程A必须等待或者阻塞,直到B释放它,如果B不释放锁,A将永远等待下去。
@ThreadSafe
public class UnsafeCachingFactorizer implements Servlet{
@GuardBy("this") private final AtomicReference<BigInteger> lastNumber=new AtomicReference<BigInteger[]>();
@GuardBy("this") private final AtomicReference<BigInteger> lastFactors=new AtomicReference<BigInteger[]>();
publilc synchronized 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(i);
lastFactors.set(factors);
encodeIntoResponse(resp,factors);
}
}
}
###2,重进入
当一个线程请求其他的已经占有锁时,请求线程将被阻塞。然而内部锁是可重进入的,因此线程试图获得它自己占有的锁时,请求就会成功。
重进入的条件:所有的请求是基于“每线程”,而不是基于”每调用“的。
重进入的实现:通过为每一个锁关联一个请求技术和一个占有它的线程。当计数为0时,认为锁时未被占有的。当线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数置为1。如果同一个线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器将递减。直到计数器达到0时,锁被释放
重进入的意义:方便了锁行为的封装,简化面向对象并发代码开发
public class Widget{
public sysnchronized void doSomething(){
...
}
}
public class LoggingWidget extends Widget{
public sysnchronized void doSomething(){
System.out.println(toString()+": calling doSomething");
super.doSomething();
}
}
子类覆写了父类synchronized类型,并且调用父类中的方法。如果没有死锁,这段代码很容易就产生死锁
4,用锁来保护状态
锁可以使线程创新的访问它所保护的代码路径,所以我们可以用锁来创建相关的协议,以保证线程对共享状态独占访问。只要始终如一的遵循这些协议,就能保证状态的一致性。
复合操作会在完整运行期间占有锁,以确保其行为是原子的,然而仅仅用synchronized包装复合操作是不够的;如果用同步来协调访问变量,每次访问变量都需要同步。如果用锁协调访问变量时,每次访问变量都需要同一个锁
错误观念:只有写入共享变量时才需要同步,其实并非如此
对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下我们称这个变量是由这个锁保护的
常见锁规则:在对象内部封装所有的可变状态,通过对象的内部锁来同步任何访问可变状态代码路径,保护它在并发访问中的安全,很多线程安全类都是这个模式,例如vector和其他同步容器(Collection)类,缺点:如果添加新的方法或者代码路径而忘记使用锁,这种锁协议也很容易损坏。
锁保护的变量:每一次访问变量时都要获得该锁,确保同一时刻只有线程可以访问这个变量。若类的不变约束涉及多个状态变量,那么另外还需要一个附加需求:每个参与到不变约束的变量由同一个锁保护
仅仅同步它的每个方法,并不足以确保Vector上执行的复合操作时原子的。虽然contain和add都是原子的,但在尝试缺少即加入操作过程仍然存在竞争条件。把多个操作整合到一个复合操作时还是需要额外的锁(详见4.4),同步方法还会导致活跃度或性能问题
5,活跃度与性能
service()声明为synchronized,因此每次只能有一个线程执行它,这违背Servlet使用初衷—Servlet可以同时处理多个请求—并且当负载过高时会引起用户的不满。
上图演示了多个请求到达同步的Factoring Servlet:这些请求排队等候并依次被处理。我们把这种Web应用的运行方式描述为弱并发
弱并发:限制并发调用数量,并非可用的处理器资源,而时应用程序的自身结构。
不过我们可以通过缩小synchronized块来维护线程的安全性,提升servlet的并发
@ThreadSafe
public class CachingFactorizer implements Servlet{
@GuardBy("this") private BigInteger lastNumber;
@GuardBy("this") private BigInteger[] lastNumber;
@GuardBy("this") private Long hits;
@GuardBy("this") private Long cacheHits;
public synchronized long getHits(){return hits;}
public synchronized double getCacheHitRatio(){
return (double) cacheHits/(double)hits;
}
publilc synchronized void service(ServletRequest req,ServletResponse resp){
BigInteger i=extractFromRequest(req);
BigInteger[] factors=null;
synchronized(this){
++hits;
if(i.equals(lastNumber.get())){
++cacheHits;
factors=lastFactors.clone();
}
if(factors==null){
factors=factor(i);
synchronized(this){
lastNumber=i;
lastFactors=factors.clone();
}
}
encodeInfoResponse(resp,factors);
}
}
原子变量可以保证单一变量的操作时原子的,然而我们已经使用了synchronized块构造了原子操作,使用了两种机制会引起混肴
决定synchronized块大小需要权衡各种设计要求,包括安全性,简单性,性能。
通常简单性与性能之间时相互牵制的,实现一个同步策略时,不要过早地为了性能而牺牲简单性(这是对安全性潜在的妥协)
注意:有些耗时的计算或操作,比如网络或控制台I/O,难以快速完成。执行这些操作期间不要占有锁。
当你使用锁的时候,你应该清楚块中代码的功能,以及它的执行过程是否会很耗时,无论是作运算密集型操作,还是在执行一个可能存在潜在阻塞的操作,如果线程长时间地占有锁,就会引起活跃度与性能风险地问题。