一:内存与线程
1. Java内存模型的概念
定义程序中各个变量(实例字段、静态字段、数组元素)的访问规则
- 所有变量存储在主内存
- 每条线程还有自己的工作内存,保存了使用到的变量在主内存的副本拷贝。(不同线程之间无法访问彼此的变量,线程也无法访问主内存的变量)
2. Java内存模型之间的交互
(1)主要操作
- lock锁定(主内存):把主内存的变量标志为一条线程独占的状态
- unlock解锁(主内存):主内存的变量可被其他线程锁定
- read读取(主内存):把变量值从主内存传输到线程的工作内存中
- load载入(工作内存):把读取的变量值放入工作内存的副本中
- use使用(工作内存):把工作内存的变量值传递给执行引擎
- assign赋值(工作内存):把执行引擎得到的变量值传递给工作内存的变量
- store存储(工作内存):把变量值从线程的工作内存传输到主内存中
- write写入(主内存):把得到的变量值放入主内存的变量中
(2)过程
- 主内存→工作内存:read+load
- 工作内存→主内存:store+write
- 使用变量:lock+unlock
(3)一般的规则
- 必须读写:不允许变量从主内存读取了,工作内存不接受。或者变量从工作内存发起回写了,主内存不接受
- 变化同步:工作变量被赋值后,必须同步回主内存。一个线程没有赋值操作,不能把数据同步回主内存
- 先初始化:数据需要先从主内存载入和赋值,才可以使用和存储
- 唯一加锁:一个变量在同一时刻只能被一条线程加锁,可以加多次锁
- 锁即清空:在加锁前需要先执行载入或赋值操作,初始化变量
- 自己解锁:只能解锁被自己锁住的变量
- 解锁同步:在解锁前需要先同步回主内存
(4)Volatile的规则
特点
1:保证变量对所有线程的可见性,无需通过主内存完成传递
无需加锁:
- 运算结果并不依赖变量的当前值,确保只有单一线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
需要加锁:
- Volatile变量在各个工作线程中不存在一致性问题,但是Volatile变量的运算在并发下依然存在一致性问题
2:禁止指令重排序优化
避免变量被重排序优化后提前执行
规则
- 先刷新(load+use):在工作内存中,每次使用volatile变量需要先在主内存刷新,保证得到最新值
- 快同步(assign+store):在工作内存中,每次修改volatile变量需要立刻同步回到主内存,保证主内存的变量是最新值
- 同顺序:被volatile修饰的变量,代码的执行顺序与程序的顺序相同
(5)long和double的非原子协定
允许虚拟机将没有被volatile修饰的64位数据的读写操作分为两次32位的操作进行,不保证read、load、store、write的原子性
3. Java内存模型的特征
(1)原子性
- Java内存模型保证了read、load、assign、use、store、write操作的原子性
- 通过lock和unlock获得更大范围的原子性(synchronized同步块)
(2)可见性
变量修改后同步写回主内存,变量读取前从主内存刷新变量值。
- volatile变量会保证立即同步和立即刷新
- synchronized同步块
- final关键字
(3)有序性
- 本线程的操作有序:线程内表现为串行
- 其他线程的操作无序:指令重排序现象、工作内存与主内存同步延迟现象
- volatile变量禁止重排序
- synchronized同步块保证同一时刻只允许一条线程加锁
4. 先行发生原则
如果操作A先行发生于操作B,则操作A产生的影响能被操作B观察到。时间的先后顺序与先行发生原则没有必然联系
- 同一线程之间
- 程序次序原则:线程内按照控制流的顺序,前面的操作先行发生于后面的操作
- synchronized同步块内
- 管程锁定原则:一个unlock操作先行发生于同一个锁的lock操作
- volatile变量
- volatile变量原则:对一个volatile变量的写操作先行发生于读操作
- 线程
- 线程启动原则:Thread对象的start()方法先行发生于线程内的操作
- 线程终止原则:线程内的操作都先行发生于对此线程的终止检测
- 线程中断原则:对线程的interrup()方法先行发生于线程代码内对此线程的中断事件的检测
- 对象
- 对象终止原则:一个对象的初始化完成先行发生于finalize()
- 传递性原则
5. Java与线程
(1)线程的实现
- 内核实现(1:1):直接由操作系统的内核支持的线程,内核完成线程的切换和调度。程序通常使用轻量级进程(一个轻量级进程由一个内核线程支持)
- 轻量级进程是一个独立的调度单元
- 系统调用代价高
- 内核资源消耗大
- 用户线程实现(1:N):线程的创建、切换、调度和同步由用户自己完成
- 操作快速
- 资源消耗小
- 程序过于复杂
- 用户线程+轻量级进程实现(N:M)
- 用户线程的创建、切换依旧由用户完成
- 轻量级进程作为桥梁,使用内核提供的线程调度和处理器映射功能
Java在JDK1.2之前使用基于“绿色线程”的用户线程实现,在之后使用基于操作系统原生线程模型实现。
(2)线程的调度
协同式
线程的执行时间由线程本身控制,线程工作后主动通知系统切换其他线程
- 实现简单
- 无法控制线程执行时间
抢占式(JAVA)
线程的执行时间由系统分配,由系统调度线程。
- 通过设置优先级分配时间
- 不同平台的优先级不一致,优先级可能被系统改变
(2)线程的状态
- 新建 New:创建完成,还没有启动
- 运行 Runable:就绪或正在执行
- 无限等待 Waiting:等待被其他线程唤醒
- Object.wait()
- Thread.join()
- LockSupport.park()
- 限期等待 Timed Waiting:等待一段时间后自动唤醒
- Thread.sleep()
- Thread.join(x)
- LockSupport.parkNanos()
- LockSupport.parkUtil()
- 阻塞 Blocked:等待获取排他锁
- 结束 Terminated:线程结束执行
二:线程安全
1. 线程安全的概念
(1)定义
当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要额外的同步,或者在调用方进行任何其他的协调操作,单次调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
(2)Java中的线程安全
- 不可变对象:一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要额外的线程安全保障措施
- 基本数据类型:final
- 对象类型:对象的任何行为都不会改变自己的状态
- 绝对线程安全对象:完全满足线程安全的定义,调用对象不需要额外的同步手段
- 相对线程安全对象:对该对象的单次操作时线程安全的,如果是连续调用,则需要额外的同步手段
- 线程兼容对象:对象本身不线程安全,可以通过在调用端使用同步手段保证对象线程安全
- 线程对立对象:无论是否采取同步手段,都无法保证对象线程安全
2. 线程安全的实现
(1)互斥+同步
定义
同步:多个线程并发访问共享数据,共享数据在同一时刻只被一条或一部分线程使用
互斥:实现同步的一种方法
特点
- 悲观并发策略
- 重量级,系统调用的开销大
方法
synchronized关键字
形成monitorenter和monitorexit字节码,需要一个reference类型的参数指明锁定和解锁的对象
- synchronized指定对象:该对象作为reference类型的对象
- synchronized未指定:修饰方法的实例对象或Class对象作为reference类型的对象
过程:
- 在执行monitorenter指令时,获取对象的锁
- 如果对象没有锁定,或已经被该线程拥有(可重入),则锁的计数器+1
- 否则,阻塞等待
- 在执行 monitorexit指令时,锁的计数器-1
- 锁的计数器=0,释放对象的锁
java.util.concurrent的重入锁ReentrantLock
- 等待可中断:持有锁的线程长期不释放锁的时候,正在等待的线程放弃等待,改为处理其他事情
- 可实现公平锁:多个线程等待时,按先来先服务获取锁
- 锁可以绑定多个条件
(2)非阻塞+同步
先进行操作,如果没有其他线程使用共享数据,操作成功。否则,再进行补偿措施(重试)
特点
- 基于冲突检测的乐观并发策略
- 需要保证操作和冲突检测具备原子性(硬件保证通过一条指令完成多次操作)
- 测试并设置(Test-and-Set)
- 获取并增加(Fetch-and-Increment)
- 交换(Swap)
- 比较和交换(Compare-and-Swap)
- 加载链接(Load-Linked)
- 条件存储(Store-Conditional)
方法
通过J.U.C包里面的原子类或者使用反射获取Unsafe.getUnsafe()使用
(3)无同步方案
如果一个方法不涉及共享数据,无需使用同步措施
- 可重入代码:不依赖存储在堆的数据和公共资源、不调用非可重入方法、状态由参数传人
- 线程本地存储:一个线程保存共享数据的操作过程
3. 锁优化
(1)自旋锁
多个处理器可以让多个线程并行执行,让后面请求锁的线程等待(执行一个忙循环),如果等待一段时间后没有成功获得锁,则挂起线程
- 避免线程切换的开销
- 消耗处理器资源
自适应的自旋锁
线程等待的时间由上一次在该锁上的自旋时间以及锁的拥有者决定
- 容易获得锁:等待
- 不易获得锁:直接挂起线程
(2)锁消除
虚拟机即时编译器在运行时,对一些代码要求同步,但是会消除被检测到的不可能存在共享数据竞争的锁。
如果判断到一段代码中,在堆上的数据不会逃逸出去被其他线程访问,则可以将其作为栈上数据对待,也就无需加锁
(3)锁粗化
- 一般来说,同步块的范围应该尽量小,只在共享数据的实际作用域中进行同步,方便锁的释放
- 如果一系列操作都对同一个对象反复加锁和释放锁,则应该扩大同步范围
(4)轻量级锁
HotSpot虚拟机的对象的内存布局
对象头
- Mark World:存储对象自身的运行时数据,如哈希值,GC分代年龄等
- 存储指向方法区对象类型数据的指针(和数组的长度)
轻量级锁的特点
- 对于大部分锁,在整个同步周期内不存在竞争
- 如果存在锁竞争,则轻量级锁比重量级锁慢(需要额外的CAS操作)
轻量级锁的实现
加锁:
- 如果同步对象没有被锁定(标志位=01),虚拟机首先会在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Displaced Mark World拷贝
- 虚拟机使用CAS操作尝试将对象的Mark World拷贝更新为指向Lock Record的指针
- 更新成功,线程拥有对象的锁(标志位=00)
- 更新失败,虚拟机检查Mark World是否指向当前线程的栈帧
- 是:证明线程已经拥有了锁
- 否:证明锁已经被其他线程抢占,如果有多个线程争夺锁,则锁为重量级锁(标志位=10),Mark World存储指向重量级锁的指针,等待的线程阻塞
解锁:
- 如果对象的Mark World仍然指向线程的锁记录,则用CAS操作把当前对象的Mark World和线程中复制的Displaced Mark World替换
- 替换成功:同步完成
- 替换失败:在释放锁的同时,唤醒其他被挂起的线程
(5)偏向锁
消除数据在无竞争下的同步原语,进一步提高程序的运行性能
加锁
- 如果线程第一次获取锁,则对象头启用偏向模式(标志位=01)
- 使用CAS操作,把线程ID记录在对象头的Mark World中
- 操作成功:持有锁的线程以后每次进入同步块时,无需同步操作
- 线程结束操作,重偏向
撤销偏向
另外一个线程请求获取锁,偏向模式结束
- 锁被其他线程锁定:恢复到轻量级锁状态(标志位=00)
- 锁未被锁定:恢复到未锁定状态(标志位=01)
特点
- 提高带有同步、无竞争的程序性能
- 如果同一个锁有多个线程竞争,则偏向锁是多余的