一 并发的概念
首先能用一个线程完成的事就不要用多线程做。
分工
并发编程 本质上是将原来一个线程做的事分给多个线程做(分工)
常⻅的 Executor,⽣产者-消费者模式,Fork/Join 等,这都是分⼯思想 的体现
同步/协作
将一个整体的任务分为几个任务段之前 要进行通信,讨论怎么完成一个整体的任务,比如当一个线程执行完后,如何通知后续其他线程执行。
线程之间的协作可能是主线程与⼦线程的协作,可能是⼦线程与 ⼦线程的合作, Java SDK 中 CountDownLatch 和 CyclicBarrier 就是⽤来解决线 程协作问题的
互斥
分⼯和同步强调的是性能,但是互斥是强调正确性---线程安全
互斥--同一时刻,只允许一个线程访问共享变量
当多个线程同时访问⼀个共享变量/成员变量时,就可能发⽣不确定性,造成 不确定性主要是有 可⻅性 、 原⼦性 、 有序性 这三⼤问题,⽽解决这些问题的核⼼ 就是互斥
二 并发编程三大问题
可见性,原⼦性,和有序性
为什么会出现这三种问题呢?
在线程执行中,涉及到 CPU,内存与 IO ,但是三者的运行速度差异很大 CPU > 内存 > IO分
那为了尽可能的利用CPU ,从而提升整体的效率,怎么做呢?
1. CPU 增加缓存,还不⽌⼀层缓存,平衡内存的慢
2. CPU 能者多劳,通过分时复⽤,平衡 IO 的速度差异
3. 优化编译指令
经过以上的优化,效率高了,那问题也产生了。
可见性
一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性 volatile解决谈到可⻅性,要先引出 JMM (Java Memory Model) 概念, 即 Java 内存模型,Java 内存模型规定,将所有的变量都存放在 主内存 中,当线程使⽤变量时,会把主内存 ⾥⾯的变量 复制 到⾃⼰的⼯作空间或者叫作 私有内存 ,线程读写变量时操作的是 ⾃⼰⼯作内存中的变量。
在 Java 中,所有的实例域,静态域和数组元素都存储 在堆内存中,堆内存在线程之间共享,这些在后续⽂章中都称之为「共享变 量」,局部变量,⽅法定义参数和异常处理器参数不会在线程之间共享,所以 他们不会有内存可⻅性的问题,也就不受内存模型的影响
⼀句话,要想解决多线程可⻅性问题,所有线程都必须要刷取主内存中的变量 怎么解决可⻅性问题呢?
Java 关键字 volatile 帮你搞定
原子性 :所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch
如何保证多个操作的原⼦性呢?最粗暴的⽅式是在⽅法上加 synchronized 关键字
synchronized 是独占锁 (同⼀ 时间只能有⼀个线程可以调⽤),没有获取锁的线程会被阻塞;另外也会带来很多线 程切换的上下⽂开销
所以 JDK 中就有了⾮阻塞 CAS (Compare and Swap) 算法实现的原⼦操作类 AtomicLong 等⼯具类
private static final Unsafe unsafe = Unsafe.getUnsafe();
这个类是 JDK 的 rt.jar 包中的 Unsafe 类提供了 硬件级别 的原⼦性操作,类中的 ⽅法都是 native 修饰的
有序性
编译器会为了执行效率 擅自优化代码顺序
- 可⻅性/原⼦性/有序性 三个问 题,这些问题通常违背我们的直觉和思考模式,也就导致了很多并发 Bug
- 为了解决 CPU,内存,IO 的短板,增加了缓存,但这导致了可⻅性问题 编译器/处理器 擅⾃ 优化 ( Java代码在编译后会变成 Java 字节码, 字节码被 类加载器加载到 JVM ⾥, JVM 执⾏字节码, 最终需要转化为汇编指令在 CPU 上执⾏) ,导致有序性问题
有序性可见性,happens-before
1. 对于会改变程序执⾏结果的重排序,JMM要求编译器和处理器必须禁⽌这种重 排序。
2. 对于不会改变程序执⾏结果的重排序, JMM对编译器和处理器不做要求 (JMM 允许这种重排序)。