并发编程(一)理论基础

109 阅读5分钟

1. 并发编程的流程

1.1 分工

分工就是将一个比较大的任务,拆分成多个大小合适的任务,交给合适的线程去完成,强调的是性能

分工给合适的线程去做。也就是说,应该主线程执行的任务不要交给子线程去做。

Executor、Fork/Join、Future 框架就是一种分工模式

1.2 同步

工作中遇到的线程协作问题,基本上都可以描述为这样的一个问题:当某个条件不满足时,线程需要等待,当某个条件满足时,线程需要被唤醒执行。

在 Java 并发编程领域,解决协作问题的核心技术是管程,几乎提到的所有线程协作技术底层都是利用管程解决的。

管程是一种解决并发问题的通用模型,同时还能解决下面我们将要介绍的互斥问题。

CountDownLatch 就是一种典型的同步方式

Java SDK 里提供的 CountDownLatch、CyclicBarrier、Phaser、Exchanger 也都是用来解决线程协作问题的。

1.3 互斥

所谓互斥,指的是同一时刻,只允许一个线程访问共享变量

Java SDK 里提供的 ReadWriteLock、StampedLock 就可以优化读多写少场景下锁的性能。

还可以使用无锁的数据结构,例如 Java SDK 里提供的原子类都是基于无锁技术实现的。

Java 还提供了 Thread Local 和 final 关键字,还有一种 Copy-on-write 的模式。(原理是不共享变量或者变量只允许读)

2. 并发编程解决的3个问题

2.1 可见性

CPU 增加了高速缓存,以均衡与内存的速度差异。

但是在多核的情况下,每个cpu单独访问自己的L1,L2级别缓存时是独立的,不与其他cpu共享的,就会引起共享资源的问题。

在多线程的程序中,一个线程对共享变量的修改,另外一个线程能够立刻看到,就称为可见性

2.2 原子性

操作系统允许某个进程执行一小段时间。

例如 50 毫秒,过了 50 毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”),这个 50 毫秒称为“时间片”。

现代的操作系统都基于更轻量的线程来调度,现在我们提到的“任务切换”都是指“线程切换”。

我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。

2.3 有序性问题

cpu的为了提升效率使用了指令重排的方式。

编译优化不仅带来了效率提升,但是也导致了一些我们不想被调换顺序执行的代码被cpu替换了

3. 可见性、原子性、有序性的解决

3.1 三个核心关键字

volatile

作用

告诉编译器,该变量的读写不能够使用CPU的独立缓存,必须从共享内存中读取或者写入

实现

主要是通过内存屏障(memory barrier)禁止重排序的,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。

对编译器而言,内存屏障将限制它所能做的重排序优化。

而对于处理器而言,内存屏障将会导致缓存的刷新操作。对于volatile,编译器将在volatile字段的读写操作前后各插入一些内存屏障。

synchronized

偏向锁加锁的本质就是在锁对象的对象头中写入当前线程的 id

统一线程再次访问就减少了锁访问的开销

final

final修饰的实例字段则是涉及到新建对象的发布问题。

当一个对象包含final修饰的实例字段时,其他线程能够看到已经初始化的final实例字段,这是安全的。

3.2 Happen-Before 8 个规则

前一个操作对后一个操作是可见的

Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。

单线程 happen-before 原则

在同一个线程中,书写在前面的操作 happen-before 后面的操作。

锁的 happen-before 原则

同一个锁的 unlock 操作 happen-before 此锁的 lock 操作。

在解锁的时候,JVM需要强制刷新缓存,使得当前线程所修改的内存对其他线程可见。

volatile 的 happen-befor 原则

对一个 volatile 变量的写操作 happen-before 对此变量的任意操作(当然也包括写操作了)。

volatile字段可以看成是一种不保证原子性的同步但保证可见性的特性,其性能往往是优于锁操作的。

但是,频繁地访问 volatile字段也会出现因为不断地强制刷新缓存而影响程序的性能的问题。

happen-before 的传递性原则

如果A操作 happen-before B 操作,B操作 happen-before C 操作,那么A操作 happen-before C 操作。

线程的 join() happen-before 原则

当线程 A 调用了线程 B 中的 join() 方法后,那么 A 线程能够看到所有对共享变量的操作

线程启动的 happen-before 原则

  1. 同一个线程的 start() 方法 happen-before 此线程的其它方法。
  2. 基于传递性原则,主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

线程中断的 happen-before 原则

对线程 interrupt 方法的调用 happen-before 被中断线程的检测到中断发送的代码。

线程终结的 happen-before 原则

线程中的所有操作都 happen-before 线程的终止检测。

对象创建的 happen-before 原则

一个对象的初始化完成先于他的 finalize 方法调用。