并发编程(一)

102 阅读6分钟

这是我参与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读操作不会和之后的读操作,写操作重排序。