- 什么是线程安全性
在线程安全性的定义中,最外围的概念就是正确性。正确性的含意是,某个类的行为与其标准完全一致。在良好的标准中会定义各种不变性条件来束缚对象的状态,以及定义各种后验条件来形容对象操作的后果。
定义:当多个线程拜访某个类时,不论运行时环境采纳何种调度形式或者这些线程将如何交替执行,并且在主调代码中不须要任何额定的同步或协同,这个类都能体现出正确的行为。
举例:Servlet是无状态,即它不蕴含任何域,也不蕴含任何对其余类中域的援用。也就是线程之间的变量不存在依赖关系,或者说线程之间没有共享的变量,彼此互不烦扰。那么无状态对象都是线程平安的。
原子性
竞态条件
最常见的竞态条件类型就是“先查看后执行”,通过一个可能生效的观测后果来决定下一步的动作。
书中举了一个计数的例子,那么计数的操作能够拆解成以下三个步骤:读取上一次的数值 => 批改值(++)=> 写入值。也就是值是依赖于上一次的值。那么当线程A将数值读取(查看)进去到批改(执行)这个过程中,可能线程B就曾经把线程A读取进去的值扭转了,那么线程A读取的数值就是生效了,这里就是一个竞态条件。
if(instance == null){ instance = new ExpensiveObject();}
下面是一个经典的懒汉式单例(提早加载),然而这种单例是线程不平安的,这里也存在竞态条件,在并发的环境下无奈保障单例。
复合操作
要防止竞态条件问题,就必须在某个线程批改该变量时,通过某种形式避免其余线程应用这个变量。
- 单个须要同步的变量
比方下面计数的例子,只有一个count是须要同步的变量,这样能够间接用java为咱们提供的原子变量(Atomic Variable)来保障在某个线程批改该变量时,为该变量上锁(实际上是锁住了读取 => 批改 => 写入这个过程),之后再拜访该变量的线程会被阻塞。
多个须要同步的变量
举个例子:
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, lastFactors.get()); else { BigInteger[] factors = factor(i); lastNumber.set(i); lastFactors.set(factors); encodeIntoResponse(resp, factors); } }
能够看到lastNumber、lastFactors都加上了原子变量,然而仍旧存在竞态条件。先解释一下这个代码的含意,它是实现了某种简略的缓存,当用户反复对某个number进行两次的factor计算,能够间接从数组中取出来,这就要求上一次的number和上一次对应计算出来的factor应该一一对应的存储在数组中(具体能够看P19)。
那么这里的竞态条件的产生是因为多个变量引起的,比如说number=2,对应的factor=5,此时线程A将2存储到了lastNumber中,然而此时线程B忽然将他的后果10存到了lastFactors中,这样导致2和10的谬误对应。解决办法详见下文的内置锁。
- 单个须要同步的变量
加锁机制
- 内置锁
含意:动态的synchronized的办法以Class对象作为锁,每个Java对象都能够用作一个实现同步的锁。取得内置锁的惟一路径就是进入由这个锁爱护的同步代码块或办法
取得锁机会:线程在进入同步代码块之前主动获取。
开释锁机会:失常的管制门路退出 / 从代码块中抛出异样退出。
毛病:尽管保障了线程平安然而效率很低,因为在事务设计中应该是短小精悍的,而这种对这个办法的加锁使得效率变低。
- 重入
内置锁是能够重入的,重入就意味着获取锁操作的粒度是“线程”,而不是“调用”。举个很简略的例子,比方一个办法是通过递归实现的,那么一个线程调用了该办法后,仍旧能够一直递归实现而不会阻塞;也就是说一个线程取得了锁之后,能够重复对该同步代码调用。
- 内置锁
- 用锁来爱护状态
这个其实在上文曾经具体阐释了。那么此处咱们将加锁的解决抽象化,在上文中,只对一个变量时,咱们利用原子变量的形式加锁,实际上是锁住了那一个过程或者说操作。在多个变量时,咱们利用内置锁解决,实际上也是锁住了一个“更宏观的操作”。所以加锁实际上是锁住了一系列须要原子性的操作。那么这个操作能够一直宏观,比方多个办法组成的一组操作,这样咱们在这组操作之后再加锁。显然这样粗犷的加锁,可能会导致程序中呈现过多的同步,从而导致性能降落。
活跃性和性能
上文中咱们了解一下锁的作用范畴(或者说同步代码块的作用范畴),synchronized是加在办法上的,所以作用域是在整个办法上。若咱们能放大同步代码块的作用范畴,咱们很容易做到既确保并发性,又保护线程安全性。
@GuardedBy("this") private BigInteger lastNumber; //GuardedBy的含意就是该变量受内置锁爱护 @GuardedBy("this") private BigInteger[] lastFactors; @GuardedBy("this") private long hits; @GuardedBy("this") private long cacheHits; public synchronized long getHits() { return hits; } public synchronized double getCacheHitRatio() { return (double) cacheHits / (double) hits; } public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = null; synchronized (this) { ++hits; if (i.equals(lastNumber)) { ++cacheHits; factors = lastFactors.clone(); } }//放大同步代码范畴 if (factors == null) { factors = factor(i); synchronized (this) { lastNumber = i; lastFactors = factors.clone(); }//放大同步代码范畴 } encodeIntoResponse(resp, factors); }
为了使得同步机制的对立,所以都采纳synchronized来实现了,而摈弃了原子变量的形式。