Java并发之并发基础

160 阅读5分钟

进程和线程

进程是运行中的应用的抽象,是资源分配的基本单位。线程是运行过程的抽象,是系统调度的基本单位。操作系统中用PCB(Process Control Block,进程控制块)来描述进程。Linux中的PCB是task_strcut。Linux调度器是识别task_struct进行调度的。那么无论线程或者进程底层都对应一个task_struct,进程和线程的区别就是共享资源的多少,两个进程间是隔离的完全不共享资源,同一个进程间的两个线程共享进程的所有资源。

fork

fork系统调用是创建子进程的一种方式,执行fork后,父进程的task_struct拷贝给子进程,父子进程最初的资源完全相同,但是两份不同的拷贝,因此任何改动都会造成二者分裂。父子进程对内存资源到的管理使用到了Copy-On-Write,写时复制技术.

  1. fork之前,一片内存区域对应一份物理地址和一份虚拟地址,内存权限为RW
  2. fork之后,父子进程看到的虚拟地址和物理地址空间都是相同的,未发生拷贝,权限为RO
  3. 父子进程对内存触发写操作将触发PageFault,此时就会发生内存拷贝。父子进程看到的虚拟地址仍然一样,但是物理地址已经不一样了(各进程的虚拟地址到物理地址的映射有MMU统一管理)

fork必须运行在有MMU的CPU上

vfork

对于无MMU的CPU无法使用写时复制也无法支持fork。无MMU的CPU使用vfork创建子进程,父进程将一直阻塞知道子进程exit或者exec。vfork中父子进程共用同一片内存区。

pthread_create

Linux线程本质上就是进程,只是与进程共享所有资源。每个线程都有自己的task_struct,因此每个线程都可以被cpu调度。多线程都共享同一进程资源。这两点正好满足线程定义,Linux就是这样用进程实现了线程,所以线程又称为轻量级进程。

并发问题根源

在计算机的发展更新中,为了充分发挥CPU的算力,平衡成本以及实现更为丰富的更功能。计算机科学家做了诸多优化,比较有代表性的优化有:

  1. 使用多级分层的存储结构,来平衡成本以及均衡CPU处理速度和内存速率的差异
  2. 向上做高级抽象,有了计算机高级语言。向下是更为复杂和强大的指令集。一条简单的高级语言语句对应可能是多条汇编指令构成的强大功能
  3. 设计了进程,线程等概念。通过分时复用,来均衡CPU和IO设备的速度差异。提高CPU使用率,指令吞吐,减小等待延迟
  4. 通过设计指令流水线,提高CPU的效率

这些革命性的优化使计算机蓬勃发展,但是也伴随着一些问题祸福相依

可见性问题

因为CPU和内存之间有极大的速度差异,我们使用多级分层的存储结构来均衡成本和速率差异

每种设备只和相邻的存储设备打交道。上层级的高速缓存,充当下层更大更慢设备中数据对象的缓存。既然上层只和相邻层级的数据交互,而线程又共享同一进程的资源,CPU内部的寄存器和缓存每个核心之间又是隔离的,势必存在线程修改了当前核心的寄存器或者缓存中的数据,而对其他线程不可见。这就是我们常说的不可见问题。

原子性问题

CPU是分时复用的,势必会存在线程A正在执行完某个指令后响应中断CPU被切换到另一线程B执行。而高级语言的一条语句对应的往往是多条汇编指令(机器码指令),那么可能在执行到其中一条指令后,发生上下文切换,导致无法保证高级语言中的一条语句是原子(不可中断)执行的,这就是常说的原子性问题。

有序性问题

由于现在的CPU都是采用指令流水线,导致编译器(CPU)在编译(执行)时,为了提高流水线的效率,在保证单线程执行语义的前提下,会对指令进行重排序。有三种类型的重排序分别是:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于 CPU 使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

这三种重排序都可能导致多线程程序出现内存可见性问题。下面是Java中一个常见的有序性问题例子的演示

我们使用的测试工具:jcstressopenjdk提供的并发正确性测试套件

idea插件:jcstress插件可以免去打包直接执行并发测试

项目依赖

  <dependencies>
    <dependency>
      <groupId>org.openjdk.jcstress</groupId>
      <artifactId>jcstress-core</artifactId>
      <version>0.5</version>
    </dependency>
    <dependency>
      <groupId>org.openjdk.jcstress</groupId>
      <artifactId>jcstress-samples</artifactId>
      <version>0.5</version>
    </dependency>
  </dependencies>

测试用例

@JCStressTest
@Outcome(id = {"1","4"}, expect = Expect.ACCEPTABLE, desc = "right")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!")//输出结果的可能性
@State
public class ConcurrentOrderTest {
    private int value = 0;
    private boolean flag = false;


    @Actor
    public void process(I_Result result) {
        if (flag) {
            result.r1 = value + value;
        } else {
            result.r1 = 1;
        }
    }


    @Actor //表示一个线程执行的方法
    public void process2(I_Result result) {
        value = 2;
        flag = true;
    }
}

说明

  • 方法process是压测第一个线程干的活,将结果保存到I_Result中。
  • 方法process2是压测第二个线程干的活
  • 类前面的@Outcome注解用来展示验证结果,特别是id="0"这个是我们感兴趣的结果,如果没有有序性问题指令重排序问题我们预期的结果是4或者1

测试结果

  • 大多数结果都出现我们预期的1和4
  • 极少数情况下出现了结果为0