并发编程(一)理论基础

并发编程的流程

并发编程(一)理论基础

分工

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

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

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

 

同步

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

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

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

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

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

 

互斥

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

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

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

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

 

并发编程解决的3个问题

 

可见性

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

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

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

 

原子性

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

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

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

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

 

有序性问题

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

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

 

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

 

三个核心关键字

 

volatile

作用

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

实现

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

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

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

 

synchronized

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

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

 

final

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

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

 

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 方法调用。