【Java并发.2】线程安全性

  要编写线程安全的代码,其核心在于要对状态访问操做进行管理,特别是对共享(Shared)和可变的(Mutable)状态的访问。java

  “共享”意味着变量能够由多个线程同时访问,而“可变”则意味着变量的值在其生命周期内能够发生变化。咱们将像讨论代码那样来讨论线程安全性,但更侧重于如何防止数据在数据上发生不可控的并发访问。缓存

  当多个线程访问某个状态变量而且其中有一个线程执行写入操做时,必须采用同步机制来协同这些线程对变量的访问。Java 中的主要同步机制是关键字 synchronized ,它提供了一种独占的加锁方式,但“同步”这个术语还包括 volatile 类型的变量,显式锁(Explicit Lock)以及原子变量。安全

若是当多个线程访问同一个可变的状态变量时,没有使用合适的同步,那么程序就会出现错误。有三种方式能够修复这个问题:
  • 不在线程之间共享该状态变量
  • 将专题图变量修改成不可变的变量
  • 在访问状态变量时使用同步

  若是从一开始就设计一个线程安全的类,那么比在之后再将这个类修改成线程安全的类要容易的多。网络

当设计线程安全的类时,良好的面向对象技术、不可修改性,以及明晰的不变性规范都能起到必定的帮助做用。

 

2.1  什么是线程安全性  并发

   给线程安全性给出一个确切的定义:less

当多个线程访问某个类时,无论运行时环境采用何种调度方式或者这些线程将如何交替执行,而且在主调代码中不须要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称为这个类时线程安全的。 
在线程安全类中封装了必要的同步机制,所以客户端无须进一步采起同步措施。

  示例:一个无状态的 Servletide

public class StatelessFactorizer implements Servlet {
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = extractFromRequest(servletRequest);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(servletResponse, factors)
    }
}

  与大多数Servlet 相同,StatelessFactorizer 是无状态的:它既不包含任何域,也不包含任何对其余类中域的引用。计算过程当中的临时状态仅存在于线程栈上的局部变量中,而且只能由正在执行的线程访问。性能

无状态对象必定是线程安全的。

  

2.2  原子性this

  假设咱们但愿增长一个“命中计数器”来统计所处理的请求数量。例以下代码atom

public class StatelessFactorizer implements Servlet {        【皱眉脸--不要这么作private long count = 0;
    public long getCount() {
        return count;
    }
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = extractFromRequest(servletRequest);
        BigInteger[] factors = factor(i);
        ++count;
        encodeIntoResponse(servletResponse, factors)
    }
}

  不幸的是 ++count 不是原子操做,这是一个“读取-修改-写入”的操做序列。(上一节讲的竞态条件)。

  2.2.1  竞态条件

    当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。

    最多见的竞态条件类型就是“先检查后执行(Check-Then-Act)”操做,即经过一个可能失效的观测结果来决定下一步的动做。

  2.2.2  实例:延迟初始化中的竞态条件

public class LazyInitRace {            【皱眉脸--不要这样作private UnsafeSequence instance = null;
    public UnsafeSequence getInstance() {
        if (instance == null) {
            instance = new UnsafeSequence();
        }
        return instance;
    }
}

    存在另外一种竞态条件,在“读取-修改-写入”这种操做。

  2.2.3  复合操做

    LazyInitRace 须要以原子方式执行(不可分割)的操做。要避免竞态条件问题,就必须在某个线程修改该变量时,经过某种方式防止其余线程使用这个变量,从而确保其余线程只能在修改操做完成以前或以后读取和修改状态,而不是在修改状态的过程当中。

假定有两个操做A 和B ,若是从执行 A 的线程来看,当另外一个线程执行 B 时,要么将 B 所有执行完,要么彻底不执行 B,那么 A 和 B 对彼此来讲就是原子的。原子操做是指:对于访问同一个状态的全部操做(包括该操做自己)来讲,这个操做时一个以原子方式执行的操做。

    因此,咱们将介绍加锁机制,这是Java 中用于确保原子性的内置机制。使用 AtomicLong 类型的变量来统计已处理请求的数量。

public class StatelessFactorizer implements Servlet {
    private AtomicLong count = new AtomicLong(0);
    public long getCount() {
        return count.get();
    }
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = extractFromRequest(servletRequest);
        BigInteger[] factors = factor(i);
        count.incrementAndGet();
        encodeIntoResponse(servletResponse, factors);
    }
}

    在 java.util.concurrent.atomic包中包含了一些原子变量类,用于实如今数值和对象引用上的原子状态转换。

 

2.3  加锁机制

  当在Servlet 中添加一个状态变量时,能够经过线程安全的对象来管理 Servlet 的状态以维护 Servlet 的线程安全性。但若是想在 Servlet 中添加更多的状态,那么是否只须要添加更多的线程安全状态变量就足够了?

  假设咱们但愿提高 Servlet 的性能:将最近的计算结果缓存起来。看代码:

public class UnsafeCachingFactorizer implements Servlet {                    【皱眉脸--不要这样作private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
    private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = extractFromRequest(servletRequest);
        if (i.equals(lastNumber.get())) {
            encodeIntoResponse(servletResponse, lastFactors.get());
        } else {
            BigInteger[] factors = factor(i);
            lastNumber.set(i);      //同步1
            lastFactors.set(factors);  //同步2
            encodeIntoResponse(servletResponse, factors);
        }
    }
}

  尽管这里面的原子引用自己都是线程安全的,可是 仍是存在着竞态条件。同步1同步2 不是原子操做,可能存在问题。

要保持状态的一致性,就须要在单个原子操做中更新全部相关的状态变量

  2.3.1  内置锁

    Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包括两部分:一个做为锁的对象引用,一个做为由这个锁保护的代码块。静态的synchronized 方法以 Class 对象做为锁。

    Java 的内置锁至关于一种互斥锁,这意味着最多只有一个线程能持有这种锁。由这个锁保护的同步代码会以原子方式执行,多个线程在执行该代码块时也不会相互干扰。

    在上一个程序清单里,若是使用关键字 synchronized 来修饰 service 方法,所以在同一个时刻只有一个线程能够执行 service 方法。然而,这种方法却过于极端,由于多个客户端没法同时使用Servlet ,服务响应性很是低,没法使人接受。

  2.3.2  重入

    当某个线程请求一个由其余线程持有的锁时,发出请求的线程会阻塞。然而,因为内置锁是可重入的,所以若是某个线程试图得到一个由本身持有的锁,那么这个请求就会成功。

    “重入” 意味着获取锁的操做的粒度是“线程”,而不是“调用”。

    下面代码清单:若是内置锁不是可重入的,那么这段代码将发生死锁。

public class Widget {
    public synchronized void doSomething() {
      ...  
    }
}
public class LoggingWidget extends Widget {
     public synchronized void doSomething() {
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();
    }   
}

 

2.4  用锁来保护状态

  因为锁能使其保护的代码路径以串行形式来访问,所以能够经过锁来构造一些协议来实现对共享状态的独占访问。

对于可能被多个线程同时访问的可变状态变量,在访问它时都须要持有同一个锁,在这种状况下,咱们称状态变量是由这个锁保护的。
每一个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员指导是哪个锁。

  一种常见的加锁约定是,将全部的可变状态都封装在对象内部,并经过对象的内置锁对全部访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问。 

  当类的不可变性条件涉及多个状态变量时,那么还有另一个需求:在不变性条件中的每一个变量都必须由同一个锁来保护

  若是同步能够避免竞态条件,那么为何不在每一个方法声明时都使用同步呢?事实上,若是不加区别的滥用 synchronized ,可能致使程序中出现过多的同步。

  此外,将每一个方法都做为同步方法还可能致使活跃性问题(Liveness)或性能问题(Performance)。

 

2.5  活跃性与性能

  以前有介绍对整个 service 方法进行同步,虽然这种简单且粗粒度的方法能确保线程安全性,但付出的代价却很高。咱们将这种应用程序称为不良并发(Poor Concurrent)应用程序:可同时调用的数量,不只受到可用处理资源的限制,还受到应用程序自己结构的限制。 

  缓存最近执行因数分解的数值 及其计算结果的 Servlet:

public class UnsafeCachingFactorizer implements Servlet {
    private BigInteger lastNumber;
    private BigInteger[] lastFactors;
    private long hits;
    private long cacheHits;
    public synchronized  long getHits() { return hits;}
    public synchronized  double getCacheHitRatio() {
        return (double) cacheHits / (double) hits;
    }
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = extractFromRequest(servletRequest);
        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(servletResponse, factors);
    }
} 

   在上面的改造后的代码 实现了在简单性(对整个方法进行同步) 与并发性(对尽量短的代码路径进行同步)之间的平衡。在获取与释放锁等操做上都须要必定的开销,所以若是将同步代码块分解的过细,那么一般并很差,尽管这样不会破坏原子性。

一般,在简单性与性能之间存在着相互制约因素。当实现某个同步策略时,必定不要盲目地为了性能而牺牲简单性(着可能会破坏安全性)

  不管是执行计算密度的操做,仍是在执行某个可能阻塞的操做,若是持有锁的时间过长,那么都会带来活跃性或性能问题  

当执行时间较长的计算或者可能没法快速完成的操做时(例如:网络 I/O 或控制台 I/O),必定不要持有锁。
相关文章
相关标签/搜索