java并发编程《二》线程安全

编写线程安全的代码,本质上就是管理对状态的访问,并且通常都是共享的,可变的状态
一个对象的状态就是它的数据,存储在状态变量中,比如实例域或静态域。对象的状态还包括了其他的附属对象的域。一个对象的状态包含了任何会对它外部可见行为产生的影响的数据。
共享:指一个变量可以被多个线程访问;所谓可变,是指变量的值在其生命周期内可以使用。我们真正要做的是在,是在不可控制的并发访问中保护数据。
无论何时,只要多于一个线程访问给定的状态变量,并且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。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,难以快速完成。执行这些操作期间不要占有锁。
当你使用锁的时候,你应该清楚块中代码的功能,以及它的执行过程是否会很耗时,无论是作运算密集型操作,还是在执行一个可能存在潜在阻塞的操作,如果线程长时间地占有锁,就会引起活跃度与性能风险地问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大道至简@EveryDay

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值