Java并发编程实战-第二章-《线程安全性》

77 阅读4分钟

1. 为什么会有线程安全性?

大家考虑一下下面的场景,是否存在线程安全性:

  • 单线程环境
  • 多线程操作final
  • 多线程 读/写 操作 线程内变量 ThreadLocal
  • 多线程顺序 读写 任意变量
  • 多线程 只读 任意变量
  • 多线程 只写 任意变量
  • 多线程 读写 任意变量
  • 多线程下,为什么32位CPU执行long型变量写,可能读出来是别的值?
  • CPU不同:单核下/多核下/多处理器下

特点: 但凡有所谓线程不安全的场景时,都满足:数据共享 + 数据可变

原因: 竞态条件,不确定的顺序处理,导致数据的正确性无法保证。

2. 为什么是多线程下存在线程安全问题?

image.png

2.1 基本概念

  • 进程

程序启动后,会被OS加载到内存中,并分配相应资源(内存空间、文件描述符、端口)。只要资源分配不冲突,进程可以被加载多次,例如多个tomcat不同端口下同时启动(镜像和容器)

  • 进程是操作系统对一个正在运行的程序的一种抽象结构。进程是资源分配的基本单位。进程的调度涉及到的内容比较多(存储空间,CPU,I/O资源等,进程现场保护),调度开销较大,在并发的切换过程效率较低。

进程:操作系统进行资源分配的基本单位

  • 线程

进程加载后,是以线程为单位进行执行。Java中就是我们常说的main线程

  • 简单理解,一个程序的执行路径,就可以叫做一个线程。如果多个路径同时执行,就是多线程

  • 线程:操作系统进行调度管理的基本单位。可以共享进程的资源。 

  • 线程能独立运行,独立调度,拥有资源(一般是CPU资源,程序计数器等)

    • 更高效的进行调度,比进程更轻量的独立运行和调度的基本单位

    • 同一个进程的多个线程共享进程的资源(省去了资源调度现场保护的很多工作)

线程的切换:如果一个进程中存在多个线程,通过OS进行CPU的调度。

  • 协程 是用户模式下的轻量级线程(在用户态执行),操作系统内核对协程一无所知。一个线程可以包含一个或多个协程

2.2 既然有共享资源,就可能存在同时操作的情况。如何保证安全?

  • 不可中断、不可拆分
  • 临界资源的互斥访问

管理共享变量以及对共享变量的操作过程,让他们支持并发

临界资源的访问需要同步操作,比如信号量就是一种方便有效的进程同步机制。资源的请求和释放过程request和release。
确保每次仅有一个进程使用该共享资源,这样就可以统一管理对共享资源的所有访问,实现临界资源互斥访问。  

Java里的管程就是synchronized

这里可以结合一下 内核空间/用户空间、CPU缓存模型(总线、中断...),以及java内存模型(happen-before)去理解

3. 什么是原子性

操作系统的原语:

  • 由若干条指令组成
  • 用来完成某个特定功能
  • 执行过程不会被中断,原子性。

原语运行在内核空间。是不可分割的指令

这里一个典型的反例是:

简单的一行++ count,底层是“读取 → 修改 → 写入"的过程,允许跳步【Java编译器优化导致的指令重排】

参考:

www.cnblogs.com/panlei3707/…

zhuanlan.zhihu.com/p/62373826


4. 竞态条件

某个计算的正确性取决于多个线程的交替执行时序,不保证始终的正确性,这就是竞态条件。

5. 加锁机制

加锁,本质上其实就是对资源的保护。

互斥保证正确的顺序性

  • 互斥,就是同一时刻,只允许同一个线程访问共享变量。

看加锁是否正确,其实就是看保护的资源是不是同一个,比如 lock(this) 和 lock(A.class)不是同一个资源