竞态与线程安全

竞态

对于同样的输入,程序的输出有时候正确而有时候却是错误的。这种一个计算结果的正确性与时间有关的现象就被称为竞态(RaceCondition)

导致竞态的常见原因是多个线程在没有采取任何措施的情况下并发更新、读取同一个共享变量。

竞态往往伴随着数据的脏读问题,即线程读取到一个过时的数据;丢失更新问题,即一个线程丢失数据所做的更新没有体现在后续其他线程对该数据的读取上。

竞态实例:

模拟RequestID生成器,RequestID是一个固定长度的编码字符串,其中最后三位是在0~999循环递增的序列号。

public final class RequestIDGenerator implements CircularSeqGenerator {
  /**
   * 保存该类的唯一实例
   */
  private final static RequestIDGenerator INSTANCE = new RequestIDGenerator();
  private final static short SEQ_UPPER_LIMIT = 999;
  private short sequence = -1;

  // 私有构造器
  private RequestIDGenerator() {
    // 什么也不做
  }
  /**
   * 生成循环递增序列号
   *
   * @return
   */
  @Override
  public short nextSequence() {
    if (sequence >= SEQ_UPPER_LIMIT) {
      sequence = 0;
    } else {
      sequence++;
    }
    return sequence;
  }

  /**
   * 生成一个新的Request ID
   *
   * @return
   */
  public String nextID() {
    SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmmss");
    String timestamp = sdf.format(new Date());
    DecimalFormat df = new DecimalFormat("000");

    // 生成请求序列号
    short sequenceNo = nextSequence();

    return "0049" + timestamp + df.format(sequenceNo);
  }

  /**
   * 返回该类的唯一实例
   *
   * @return
   */
  public static RequestIDGenerator getInstance() {
    return INSTANCE;
  }
}

竞态demo:

public class RaceConditionDemo {

  public static void main(String[] args) throws Exception {
    // 客户端线程数
//    args = new String[] {"4"};
    //Runtime.getRuntime().availableProcessors()-- 返回可用处理器的Java虚拟机的数量
    int numberOfThreads = args.length > 0 ? Short.valueOf(args[0]) : Runtime
        .getRuntime().availableProcessors();
    Thread[] workerThreads = new Thread[numberOfThreads];
    for (int i = 0; i < numberOfThreads; i++) {
      workerThreads[i] = new WorkerThread(i, 10);
    }

    // 待所有线程创建完毕后,再一次性将其启动,以便这些线程能够尽可能地在同一时间内运行
    for (Thread ct : workerThreads) {
      ct.start();
    }
  }

  // 模拟业务线程
  static class WorkerThread extends Thread {
    private final int requestCount;

    public WorkerThread(int id, int requestCount) {
      super("worker-" + id);
      this.requestCount = requestCount;
    }

    @Override
    public void run() {
      int i = requestCount;
      String requestID;
      RequestIDGenerator requestIDGen = RequestIDGenerator.getInstance();
      while (i-- > 0) {
        // 生成Request ID
        requestID = requestIDGen.nextID();
        processRequest(requestID);
      }
    }

    // 模拟请求处理
    private void processRequest(String requestID) {
      // 模拟请求处理耗时
      Tools.randomPause(50);
      System.out.printf("%s got requestID: %s %n",
          Thread.currentThread().getName(), requestID);
    }
  }
}

当args = new String[] {"4"}; 时,理论上序列号最后三位是:000-039,但是多次运行结果有时正确有时返回000-038,有时000-037。

截取部分运行结果,其中work-0和work-2线程返回的值是一样的。该程序在运行过程中出现了竞态。

worker-0 got requestID: 0049190620170236002 
worker-3 got requestID: 0049190620170236000 
worker-0 got requestID: 0049190620170236004 
worker-1 got requestID: 0049190620170236003 
worker-2 got requestID: 0049190620170236002 

nextSequence()中的 sequence++ 实际上相当于如下伪代码:

load(sequence,r1); //指令①:从内存将sequence的值读取到寄存器r1(读取共享变量)
increment(r1);     //指令②:将寄存器的r1值增加1(共享变量做计算)
store(sequence,r1); //指令③:将寄存器r1的内容写入sequence对应的内存空间(更新变量)

发生原因:

一个线程在执行完指令①之后到开始执行指令②的这段时间内其他线程可能已经更新了共享变量的值,这就使得该线程在执行指令②的时候使用的是共享变量的旧值,即脏读数据。接着,该线程把根据这个旧值算出来的结果更新到共享变量,而这又使得其他线程对该变量所做的更新被覆盖,造成更新丢失。

竞态: 一个线程读取共享变量并以该共享变量为基础进行计算的期间另外的一个线程更新了该共享变量的值而导致的干扰(读取脏数据)或冲突(丢失更新)的结果。

竞态防止

共享变量修改为局部变量

public class NoRaceCondition {

  public int nextSequence(int sequence) {

    // 以下语句使用的是局部变量而非状态变量,并不会产生竞态
    if (sequence >= 999) {
      sequence = 0;
    } else {
      sequence++;
    }
    return sequence;
  }

}

由于不同线程各自访问各自的那一部分局部变量,所以局部变量不会导致竞态。、

添加synchronized关键字

public class SafeCircularSeqGenerator implements CircularSeqGenerator {
  private short sequence = -1;

  @Override
  public synchronized short nextSequence() {
    if (sequence >= 999) {
      sequence = 0;
    } else {
      sequence++;
    }
    return sequence;
  }
}

限制只能被一个线程执行

线程安全和非线程安全

线程安全 如果一个类在单线程环境下运行正常,并且在多线程环境下,不做任何改变的情况下也能正常运行,那我们就称其是线程安全的,相应的我们称这个类具有线程安全性。

非线程安全 反之我们则为非线程安全。

线程安全概述:

原子性

原子的意思是不可再分。对于设计共享变量访问的操作,若该操作从其执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,相应的我们称该操作具有原子性
原子操作是多线程环境下的一个概念,他是针对访问共享变量的操作而言的。原子操作的不可分割包括以下两层含义:

  • 访问(读写)某个共享变量的操作从其执行线程以外的任何线程来看,
    该操作要么已经执行结束要么尚未发生,即其他线程不会‘看到’该操作执行了部分的中间效果。
  • 访问一组共享变量的原子操作是不能够被交错的。

java如何实现原子性:

  • 使用锁(lock)。锁具有排他性,它能保证一个共享变量在任意一个时刻只能够被一个线程访问。
  • 另一种是利用处理器提供的专门CAS指令。CAS指令实现原子性的方式和锁实现原子性的方式实质上是相同的,差别在于锁是在软件层次实现,而CAS是直接在硬件(处理器和内存)层次实现,可以看做“硬件锁”。

可见性

可见性:在多线程环境下,一个线程对共享变量做了更新,而其它线程在后续的访问过程中无法立刻读取到更新后的内容,甚至永远无法读取到更新后的内容。
后续访问该变量的线程可以读取到更新后的结果,我们称这线程对共享变量的更新对其他线程可见。反之,则称为不可见。
不可见产生的原因:

  • 代码没有给编译器足够的提示,使其认为该状态变量只有一个线程访问,从而使编译器为了避免重复读取该变量而对代码做了优化。
  • 可见性问题与与计算机的储存系统也有关。

如何保证可见性?
对实例变量添加关键字:volatile

  • 一个作用是提示JIT编译器被修饰的变量可能被多个线程共享,以阻止编译器做出可能导致程序不正常的优化。
  • 另一个作用是读取一个volatile关键字修饰的变量会使相应的处理器执行刷新处理器缓存的动作,写一个volatile修饰的变量会使相应的处理器执行冲刷处理器缓存的动作,从而保障了可见性。
posted @ 2019-06-21 16:19  三匝树  阅读(599)  评论(0编辑  收藏  举报