自己如何做公司网站视频快速优化官网
基本概念
- 并发编程可以抽象成三个核心问题: 分工、同步/协作、互斥
分工
将当前 Sprint 的 Story 拆分成「合适」大小的 Task,并且安排给「合适」的 Team Member 去完成
拆分的粒度太粗,导致这个任务完成难度变高,耗时长,不易与其他人配合;拆分的粒度太细,又导致任务太多,不好管理与追踪,浪费精力和资源。
关于分工,常见的 Executor,生产者-消费者模式,Fork/Join 等,这都是分工思想的体现
同步/协作
一个线程执行完任务,如何通知后续线程执行?
Java SDK 中 CountDownLatch 和 CyclicBarrier 就是用来解决线程协作问题的
互斥
互斥:同一时刻,只允许一个线程访问共享变量。分工和同步强调的是性能,但是互斥是强调正确性
当多个线程同时访问一个共享变量/成员变量时,就可能发生不确定性,造成不确定性主要是有可见性、原子性、有序性这三大问题,而解决这些问题的核心就是互斥
三大问题
可见性
定义: 一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性
Java 内存模型规定,将所有的变量都存放在 主内存中,当线程使用变量时,会把主内存里面的变量 复制 到自己的工作空间或者叫作 私有内存
刷新的场景:
- 主内存中有变量 x,初始值为 0
- 线程 A 要将 x 加 1,先将 x=0 拷贝到自己的私有内存中,然后更新 x 的值
- 线程 A 将更新后的 x 值回刷到主内存的时间是不固定的
- 刚好在线程 A 没有回刷 x 到主内存时,线程 B 同样从主内存中读取 x,此时为 0,和线程 A 一样的操作,最后期盼的 x=2 就会编程 x=1
原子性
原子性:原子(atom)指化学反应不可再分的基本微粒,原子性操作你应该能感受到其含义:
count++ 分解为四步,解释一下字节码的指令,
16 : 获取当前 count 值,并且放入栈顶
19 : 将常量 1 放入栈顶
20 : 将当前栈顶中两个值相加,并把结果放入栈顶
21 : 把栈顶的结果再赋值给 count
- JDK 的 rt.jar 包中的 Unsafe 类提供了 硬件级别 的原子性操作
有序性
对于编译期可能对语句的执行进行了优化。
- 如双重加锁检查的
instance = new Singleton()
这 1 行代码转换成了 CPU 指令后又变成了 3 个,我们理解 new 对象应该是这样的:
1. 分配一块内存 M
2. 在内存 M 上初始化 Singleton 对象
3. 后 M 的地址赋值给 instance 变量但编译器擅自优化后可能就变成了这样:
1. 分配一块内存 M
2. 然后将 M 的地址赋值给 instance 变量
3. 在内存 M 上初始化 Singleton 对象
用户态与内核态
操作系统对程序的执行权限进行分级,分别为用户态和内核态。
- 内核态: cpu可以访问计算机所有的软硬件资源
- 用户态: cpu权限受限,只能访问到自己内存中的数据,无法访问其他资源
为什么要有用户态和内核态
系统需要限制不同的程序之间的访问能力,防止程序获取不相同程序的内存数据,或者外围设备的数据,并发送到网络,所有cpu划分出两个权限等级用户态和内核态
内核态:用户态如果要做一些比较危险的操作直接访问硬件,很容易把硬件搞死(格式化,访问网卡,访问内存)
用户态与内核态的性能开销
当发生用户态到内核态的切换时,会发生如下过程:
- 设置处理器至内核态。
- 保存当前寄存器(栈指针、程序计数器、通用寄存器)。
- 将栈指针设置指向内核栈地址。
- 将程序计数器设置为一个事先约定的地址上,该地址上存放的是系统调用处理程序的起始地址。
- 而之后从内核态返回用户态时,又会进行类似的工作。
用户态和内核态之间的切换有一定的开销,如果频繁发生切换势必会带来很大的开销,所以要想尽一切办法来减少切换
避免频繁切换
因为线程的切换会导致用户态和内核态之间的切换,所以减少线程切换也会减少用户态和内核态之间的切换。那么如何减少线程切换呢?
- 无锁并发编程。多线程竞争锁时,加锁、释放锁会导致比较多的上下文切换
- CAS算法。使用CAS避免加锁,避免阻塞线程
- 使用最少的线程。避免创建不需要的线程协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
Java内存模型JMM
java内存模型是关注在虚拟机中把变量值存储到内存和从内存取出变量这样的底层细节。
此处的变量包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数。
如果局部变量是一个reference类型,引用的对象在java堆中,但是reference本身在java栈的局部变量表中是线程私有。
java内存模型规定了所有变量都存储在主内存,每条线程还有自己的工作内存,线程的工作内存保存了被该线程使用的变量的主内存副本。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存的数据。不同线程的内存数据无法直接访问,均得通过主内存。
关于线程内存的复制,如果主内存有一个10MB的对象,线程会把对这个对象的引用、对象中在线程中的字段进行复制,但不会整个复制。
如果要把java的内存强行与Java内存区域做对应的话:
- 主内存 -> java 堆
- 工作内存 -> 虚拟机栈
- 从更基础层次上,主内存直接对应物理硬件的内存。
volatile
性能:volatile变量的读操作性能与普通变量几乎没有差别,但是写操作可能会慢些,因为需要插入内存屏障指令来保证处理器不乱序执行。
当读一个 volatile 变量时, JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
线程在【读取】共享变量时,会先清空本地内存变量值,再从主内存获取最新值