Java并发编程实践 上

2018/04/07

第一章 简介

1. 计算机加入操作系统实现多个程序的同时执行,是为了:

  • 资源利用率
  • 公平性:不同用户和程序对计算机上资源的同等使用权,比如时间分片的运行方式
  • 便利性:从编程角度看,可以把任务更好的分解开,而不用一个程序来完成所有的任务

线程被称为轻量级进程,现代操作系统中基本的调度单位是线程(而不是进程),线程允许在同一个进程中同时存在多个程序控制流,线程会共享同一个进程范围内的资源,比如:内存句柄,文件句柄。

2. 线程的优势

  • 发挥多处理器的强大能力
  • 建模的简单性
    • Servlet/RMI框架辅助让建模简单,框架负责解决细节问题:请求管理、线程创建、负载平衡,并在正确的时刻把请求分发给正确的应用程序组件,这样编写Servlet的开发人员就不需要了解有多少 请求在同一时刻需要被处理,也不需要了解套接字的输入流/输出流是否阻塞。当调用Servlet的Service方法来响应Web请求时,可以以同步的方式处理这个请求,就好像它是一个单线程程序
  • 异步事件的简化处理:作者的意思是如果线程开销足够低(系统能够支持创建更多的线程),那么很多异步的操作可以用多线程方式来代替(每个线程互不影响),进而避开处理异步复杂性
  • 更灵敏的响应用户界面

3. 线程的风险

  • 安全性问题:永远不发生糟糕的(错误的)事情。比如发生竞态条件(TODO:添加链接进行详细说明)时结果不可预期或者错误

    非线程安全的序列生成器

    @NotThreadSafe
    public class UnsafeSequence {
      private int value;
    
      public int getNext() {
          return value++;
      }
    }
    

    A. (value->9) —> (9+1=10) —> (value=10)

    B. —> (value->9) —> (9+1=10) —> (value=10)

    解决办法可以是:

    @ThreadSafe
    public class Sequence {
      @GuardedBy("this") private int value;
      public synchronized int getNext() {
          return value++;
      }
    }
    
  • 活跃性问题:死锁,饥饿,活锁等
  • 性能问题:上下文切换等开销

4. 线程无处不在

  • 每一个Java应用程序都在使用线程(垃圾回收线程,终结处理线程,运行main方法的主线程等)
  • Servlet/JSP:Servlet规范规定一个Servlet必须为多个用户同时调用它做好准备(也即Servlet应该是线程安全的)
  • RMI:rmi对象和Servlet一样需要是线程安全的

第二章 线程安全性

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

  • 共享:变量可以由多个线程同时访问
  • 可变:变量在其生命周期内可以发生变化

如果多个线程访问同一个可变的状态变量时没有使用合适的同步,那么错误就会发生,有三种方式进行修复:

  • 不在线程之间共享该状态变量
  • 将状态变量修改为不可变的变量
  • 在访问状态变量时使用同步

1. 线程安全性

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

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

示例:一个无状态的Servlet

@ThreadSafe
public class StatelessFactorizer extends GenericServlet implements Servlet {

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(resp, factors);
    }
}

2. 原子性

一个或者一组(复合)操作,能够逻辑上呈现出最小执行单元执行的效果,则具备原子性。(和数据库事务里的一个事务具备原子性类似)

  • 竞态条件(Race Condition):TODO 由于不恰当的执行时序而出现不正确的结果。有2类典型的竞态条件(复合操作)
    • 先检查后执行(Check-Then-Act):如单例实现中先判空后new对象;也比如:
      if (!vector.contains(element)) {
      	vector.add(element);
      }
      
    • 读取修改写入(Read-Modify-Write):如count++ 示例:(不要这么做)在没有同步的情况下统计已处理请求数量的Servlet
      @NotThreadSafe
      public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
          private long count = 0;
      
          public long getCount() {
              return count;
          }
      
          public void service(ServletRequest req, ServletResponse resp) {
              BigInteger i = extractFromRequest(req);
              BigInteger[] factors = factor(i);
              ++count;
              encodeIntoResponse(resp, factors);
          }
      }    
      

      一种改进方式是使用AtomicLong类型的变量来统计已处理的请求的梳理代码 这种改进方式试用于:当在无状态的类中添加一个状态时,如果该状态由线程安全的对象来管理,那么这个类仍然是线程安全的。 但是,当状态数量由1个变为多个时,情况复杂许多,可能需要用到加锁机制。

3. 加锁机制

要保持状态的一致性,需要在单个原子操作中更新所有相关的状态变量。 如下是一个示例:(不要这么做)该Servlet在没有足够原子性保证的情况下对其最近计算结果进行缓存:

@NotThreadSafe
public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {
    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);
        }
    }
}

无法保证对lastNumberlastFactors的同时原子性更新。Java提供了内置的锁机制来支持原子性。

  • 内置锁/监视器锁 每个Java对象都可以用作一个实现同步的锁。内置锁体现在同步代码块(Synchronized Block)上,同步代码库包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。静态的synchronized方法以Class对象作为锁。 获得内置锁的唯一途径是进入由这个锁所保护的同步代码块或方法。由于每次只能有一个线程执行内置锁保护的代码块,因此这个锁保护的同步代码块会以原子方式执行。 特点:
    • 互斥性:对于某个内置锁,一旦有一个线程获得了该锁且未释放锁,别的线程就无法获得这个锁
    • 可重入:如果某个线程试图获得一个已经由它持有的锁,那么这个请求会成功

    获取锁的粒度,是线程,而非调用。持有锁的不是方法,而是线程,完整的表述是:线程持有某个对象/类的锁

    当子类改写父类的synchronized方法时,

当执行时间较长的计算或者可能无法快速完成的操作时(比如,网络 I/O 或者控制台 I/O),一定不要持有锁。

http://jcip.net/listings.html

Search

    Table of Contents