竞态与线程安全

摘要:
竞态对于同样的输入,程序的输出有时候正确而有时候却是错误的。这种一个计算结果的正确性与时间有关的现象就被称为竞态导致竞态的常见原因是多个线程在没有采取任何措施的情况下并发更新、读取同一个共享变量。该程序在运行过程中出现了竞态。

竞态

对于同样的输入,程序的输出有时候正确而有时候却是错误的。这种一个计算结果的正确性与时间有关的现象就被称为竞态(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修饰的变量会使相应的处理器执行冲刷处理器缓存的动作,从而保障了可见性。

免责声明:文章转载自《竞态与线程安全》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇vue 时间戳转换状态机学习及对一段 java 代码的改写下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

pcntl_fork()函数说明

pcntl_fork()函数复制了当前进程的PCB,并向父进程返回了派生子进程的pid,父子进程并行,打印语句的先后完全看系统的调度算法,打印的内容控制则靠pid变量来控制。因为我们知道pcntl_fork()向父进程返回了派生子进程的pid,是个正整数;而派生子进程的pid变量并没有被改变,这一区别使得我们看到了他们的不同输出。 1. 派生子进程的进程,...

[ PyQt入门教程 ] PyQt5中多线程模块QThread使用方法

本文主要讲解使用多线程模块QThread解决PyQt界面程序唉执行耗时操作时,程序卡顿出现的无响应以及界面输出无法实时显示的问题。用户使用工具过程中出现这些问题时会误以为程序出错,从而把程序关闭。这样,导致工具的用户使用体验不好。下面我们通过模拟上述出现的问题并讲述使用多线程QThread模块解决此类问题的方法。 PyQt程序卡顿和无法实时显示问题现象 使...

sleep() 与 wait()的比较

1、这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。 sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。 2、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法...

python_14(js)

第1章 图片方法 1.1 设置背景图:1.2 背景图问题:1.3 background-repeat; noa-repe 1.4 background-attachment: fixed1.5 background-position 1.6 background-position-x 1.7 截取局部1.7.1 透明色第2章 定位 2.1 定义形式2.2...

用windbg分析一个dead lock的问题

难得Winform项目中碰到dead lock,记录一下。 QA报告说,有时候晚上跑完自动化脚本,第二天早上来发现系统hang在屏保界面没反应,从日志看也没有报错。这种属于很少才会发生,也不知道怎么重现,但是很严重的bug,于是抓个dump来研究一下。 # Windbg加载dump文件后的一些文件信息 Microsoft (R) Windows Debu...

Handler 机制(一)—— Handler的实现流程

   由于Android采用的是单线程模式,开发者无法在子线程中更新 UI,所以系统给我提供了 Handler 这个类来实现 UI 更新问题。本贴主要说明 Handler 的工作流程。 1. Handler 的作用 在Android为了保障线程安全,规定只能由主线程来更新UI信息。而在实际开发中,会经常遇到多个子线程都去操作UI信息的情况,那么就会导致U...