这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战
并发编程(一)
基础概念
并发编程三要素
- 原子性: 不可再分割的颗粒
- 有序性: 程序按照代码的先后顺序执行
- 可见性: 当多个线程访问同一个变量,如果一个线程做了修改,其他线程立刻能获取到最新的值
并发&并行
单个处理器采用单核执行多个任务是并发,在不同计算机,处理器,处理器核心上运行多个任务是并行。
同步
常用的同步机制有信号量和监视器。
线程状态
创建NEW 就绪RUNABLE 阻塞BLOCKED 等待WAITING 超时等待TIMED_WAITING 死亡 TERMINATED
悲观锁和乐观锁
悲观锁: 每次操作都会加锁, 线程阻塞
乐观锁: 假设没有冲突去完成操作, 如果失败就重试,不会线程阻塞
synchronized
- 修饰代码块
- 修饰方法
- 修饰一个类
CAS
Compare And Swap, 如果值与预期的相同,就会更新,否则不做操作。存在三个问题: ABA, 循环时间长,开销大,只能保证一个共享变量的原子操作
线程池
通过复用减少频繁创建线程与销毁带来的内存消耗
线程
-
新建线程可以继承Thread类或实现Runnable,Callable接口
-
Java中的线程有优先级,介于Thread.MIN_PRIORITY(1)和Thread.MAX_PRIORITY(10)之间, 默认优先级是Thread.NORM_PRIORITY(5).执行顺序没有保证,只能是尽量让优先级高的线程在优先级低的线程之前执行
-
线程可以分为守护线程和非守护线程, 区别在于如何影响程序的结束。守护线程通常在作为垃圾收集器或缓存收集器中,通过setDaemon()方法设置守护线程。
-
线程状态分为六种,创建NEW 就绪RUNABLE 阻塞BLOCKED 等待WAITING 超时等待TIMED_WAITING 死亡 TERMINATED。通过getState可以获取线程状态。
-
线程常用方法:
- getId(): 唯一标识符,正整数
- getName()/setName: 线程名称
- getPriority/setPriority: 优先级
- isDaemon/setDaemon: 守护线程
- getState: 线程状态
- interrupt: 中断目标线程,给目标线程发送中断信号, 线程被打上中断标记
- interrupted: 判断目标线程是否被中断,同时清除线程的中断标记
- isInterrupted: 判断目标线程是否被中断,不会清除线程的中断标记
- join: 暂停线程执行,直到调用线程执行结束
- SetUncaughtExceptionHandler: 未校验异常控制器
- currentThread: 当前Thread对象。
Synchronized
锁的本质
对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
HotSpot虚拟机的对象头分为两部分信息,第一部分用于存储对象自身运行时数据,如哈希码、GC分代年龄等, 也就是平时说的Mark Word。另一部分用于存储指向对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分存储数组长度。
在JVM中, JDK6引入了偏向锁和轻量级锁。 当没有竞争时,采用的是偏向锁,此时锁ID记录在对象的MarkWord中。当遇到竞争了,虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储对象目前Mark Word的拷贝。然后虚拟机使用CAS操作尝试将对象的Mark Word更新为指向锁记录(Lock Record)的指针。如果成功,说明获取了轻量级锁, 负责升级到重量锁,Mark Word存储指向重量级锁的指针,线程阻塞。
interrupt
当调用wait, sleep, join的时候, 如果被中断会抛出InterruptedException。 抛出后会重置中断标记。这里需要注意的是WAITING、TIMED_WAITING是轻量级阻塞,而Synchronized是重量级阻塞。 所以Interrupt的精确含义是唤醒轻量级阻塞。
可以利用interrupt对线程进行优雅关闭
死锁
死锁条件:
- 互斥
- 占有并等待
- 不可剥夺
- 循环等待
解决:
- 忽略: 直接忽略
- 检测: 系统中设定检测任务,当发生死锁时,结束或强制结束任务。
- 预防
- 规避
JMM & Happen-before
为什么存在 内存可见性问题?
-
CPU层次
缓存一致性协议MESI, 多个CPU缓存不会出现不同步问题,但是损耗很大, 为了解决损耗问题,加上了store buffer和Load buffer, 但是Store Buffer,Load Buffer和L1之间是异步的, 向内存中写入一个变量, 这个变量会保存到Store Buffer中, 稍后才会异步的写入L1, 同时同步写入主内存。
-
操作系统内核层次
多个CPU和多个核,都有自己的缓存, 和主内存不是完全同步
-
Java层次
JVM抽象内存模型,每个线程有线程本地缓存, 还有共享内存。
重排序和内存可见性关系
Store Buffer 就是重排序的一种,也就是内存重排序
类型:
- 编译器重排序
- CPU指令重排序
- CPU内存重排序(主因)
内存屏障
为了禁止编译器重排序和CPU重排序, 在编译器和CPU层面都有对应的指令, 也就是内存屏障。这也是JMM和happen-before规则的底层实现。编译器的内存屏障是为了告诉编译器不要对指令进行重排序,编译完成后,内存屏障消失。CPU内存屏障是CPU指令。
Jdk8在Unsafe类中提供了三个内存屏障,在理论层次可以分为四种: LoadLoad,StoreStore, LoadStore, StoreLoad
as-if-serial
- 单线程的重排序,执行结果不能改变
- 多线程的重排序, 编译器和CPU只能保证每个线程的as-if-serial, 需要上层来告知多线程时什么时候可以重排序,什么时候不能重排序
happen-before
描述两个操作之间的内存可见性。
- 同一个线程中,前面的操作happen-before后面的操作
- 同一个锁的unlock操作happen-before lock操作
- volatile变量的写操作happen-before对此变量的任意操作
- happen-before具有传递性。
- 一个线程的start方法happen-before线程的其他方法
- 线程中断的调用happen-before检测到中断发送的代码
- 线程中所有操作都happen-before线程的终止检测
- 一个对象初始化happen-before它的finalize方法。
volatile
volatile三个作用: 64位写入的原子性,内存可见性和禁止重排序
64位写入的原子性
64位的long或double并不是原子的, 如果要是原子的,可以添加volatile
原理
- 在volatile写操作之前插入StoreStore屏障, 保证Volatile写操作不会和之前的写操作重排序
- 在volatile写操作之后插入StoreLoad屏障,保证Volatile写操作不会和之后的读操作重排序
- 在volatile读操作的后面插入LoadLoad和LoadStore屏障,保障volatile读操作不会和之后的读操作,写操作重排序。