1. 什么是线程安全性

    在线程安全性的定义中,最外围的概念就是正确性。正确性的含意是,某个类的行为与其标准完全一致。在良好的标准中会定义各种不变性条件束缚对象的状态,以及定义各种后验条件来形容对象操作的后果

    定义:当多个线程拜访某个类时,不论运行时环境采纳何种调度形式或者这些线程将如何交替执行,并且在主调代码中不须要任何额定的同步或协同,这个类都能体现出正确的行为

    举例:Servlet是无状态,即它不蕴含任何域,也不蕴含任何对其余类中域的援用。也就是线程之间的变量不存在依赖关系,或者说线程之间没有共享的变量,彼此互不烦扰。那么无状态对象都是线程平安的

  2. 原子性

    • 竞态条件

      最常见的竞态条件类型就是“先查看后执行”,通过一个可能生效的观测后果来决定下一步的动作

      书中举了一个计数的例子,那么计数的操作能够拆解成以下三个步骤:读取上一次的数值 => 批改值(++)=> 写入值。也就是值是依赖于上一次的值。那么当线程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的谬误对应。解决办法详见下文的内置锁

  3. 加锁机制

    • 内置锁

      含意:动态的synchronized的办法以Class对象作为锁,每个Java对象都能够用作一个实现同步的锁。取得内置锁的惟一路径就是进入由这个锁爱护的同步代码块或办法

      取得锁机会:线程在进入同步代码块之前主动获取。

      开释锁机会:失常的管制门路退出 / 从代码块中抛出异样退出。

      毛病:尽管保障了线程平安然而效率很低,因为在事务设计中应该是短小精悍的,而这种对这个办法的加锁使得效率变低。

    • 重入

      内置锁是能够重入的,重入就意味着获取锁操作的粒度是“线程”,而不是“调用”。举个很简略的例子,比方一个办法是通过递归实现的,那么一个线程调用了该办法后,仍旧能够一直递归实现而不会阻塞;也就是说一个线程取得了锁之后,能够重复对该同步代码调用。

  4. 用锁来爱护状态

    这个其实在上文曾经具体阐释了。那么此处咱们将加锁的解决抽象化,在上文中,只对一个变量时,咱们利用原子变量的形式加锁,实际上是锁住了那一个过程或者说操作。在多个变量时,咱们利用内置锁解决,实际上也是锁住了一个“更宏观的操作”。所以加锁实际上是锁住了一系列须要原子性的操作。那么这个操作能够一直宏观,比方多个办法组成的一组操作,这样咱们在这组操作之后再加锁。显然这样粗犷的加锁,可能会导致程序中呈现过多的同步,从而导致性能降落。

  5. 活跃性和性能

    上文中咱们了解一下锁的作用范畴(或者说同步代码块的作用范畴),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来实现了,而摈弃了原子变量的形式。