JAVA虚拟机学习总结——Java内存模型与线程

时间:2022-12-26 17:56:29

JAVA内存模型

Java内存模型规定了所有的变量都存储在主内存中。
每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量所有的操作(读取,赋值)都必须在工作内存中进行,而不能直接读写主内存的变量。不同的线程之间也无法直接访问其他工作内存的变量,线程间变量值传递均需要通过主内存来完成。

内存间相互操作

Lock

作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

Unlock

作用于主内存的变量,把一个变量解锁,方便其他线程使用。

Read

作用于主内存的变量,把一个变量从主内存传输到工作内存,以便load。

Load

作用于工作内存的变量,把read操作从主内存得到的变量值放入到工作内存的变量副本中。

Use

作用于工作内存的变量,把工作内存的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。

Assign

作用于工作内存的变量,把从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

Store

作用于工作内存的变量,把工作内存的变量值传送给主内存,以便于write。

Write

作用于主内存的变量,它把store操作从工作内存得到的变量值放入到主内存中。

Volatile

  • 保证此变量对所有线程的可见性
  • 禁止指令重排序优化

使用时要注意符合以下场景:

  • 运算结果并不依赖变量的当前值,或者能够确保只有单-的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与变量约束。
当线程T操作一个volatile变量V,有以下规则:
  • T对V的load和read动作必须连续一起出现,这要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看到其他线程对变量v所作的修改。
  • T对V的store和write动作必须连续一起出现,这要求在工作内存中,每次修改V后都必须立刻同步回主内存,用于保证其他线程可以看到自己对变量V所做的修改。
  • 如果一个线程A对变量V的 use 或 assign 操作优先于另一个线程B对变量V的 use 或 assign 操作,那么线程A对变量V的 read 或 write 操作优先于线程B对变量V的 read 或 write 操作。这导致volatile修饰的变量不会被重排序优化,保证代码的执行顺序与程序相同。

并发过程的三个特性

  • 原子性
  • 可见性
  • 有序性

先行发生原则 happens before

这些原则是判断数据是否存在竞争,线程是否安全的主要依据:

程序次序规则

在一个线程内,书写在前面的代码的操作先行发生于后面代码的操作。

管程锁定规则

一个unlock操作先行发生于后面(时间上的顺序)对同一个锁的lock操作。

Volatile 变量规则

对一个变量volatile变量的写操作先行发生于后面(时间上的顺序)对这个变量的读操作。

线程启动规则

Thread对象的是start()方法先行发生于此线程的每一个动作。

线程终止规则

线程的所有操作都先行发生于对此线程的终止检测 (Thread.isAlive())。

线程中断规则

对线程nterrupt()方法的调用先行发生于被中断线程的代码(Threadinterrupted()检测到中断事件的发生。

对象终结规则

一个对象的初始化(构造方法)完成,先行发生于它的fnalize()方法。

传递性

如果动作A先行发生于B,B先行发生于C,则A先行发生于C

JAVA与线程

线程是CPU调度的基本单位

JAVA线程的调度

协同式调度

线程的执行时间由线程本身控制,线程把自己的工作执行完成以后,会主动通知系统切换到另一个线程上

抢占式调度

每个线程将由系统来分配时间,线程的切换不由线程本身来决定

线程的六种状态

新建

创建后尚未启动的线程

运行

这时的线程可能在运行,也有可能在等待着CPU为它分配时间

无限期等待

不会被CPU分配时间,他们要等待其他线程显示地唤醒
+ 没有设置Timeout参数的object.wait()和Threadjoin()方法
+ LockSupport.park()方法

限期等待

不会被CPU分配时间,不过无需被其他线程唤醒,在一定时间后它们会被系统自动唤醒
+ Thread.sleep()
+ 设置了Timeout参数的Object.wait()Thread.join()
+ Locksupport.parknanos()
+ Locksupport.parkUntil()

阻塞

线程在等待着获取一个排它锁,等着另外一个线程放弃这个锁。

结束

线程已经完成

线程安全

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

线程安全的实现方法

不可变

对于final修饰的对象,在构造函数完成以后,它是不可变的

互斥同步(阻塞同步)

多线程访问共享数据的时候,保证共享数据在同一个时刻只被一个线程使用。如Synchronized和ReetranLock。

非阻塞同步

基于冲突检测的乐观并发策略,就是先进行操作,如果没有其他线程争用共享数据,那操作就完成了,如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(例如不断重试,直到成功),如CAS

无同步方案
  • 可重入代码:这种代码可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的。如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就能返回相同的结果,那就满足可重入性。
  • 线程本地存储: ThreadLocal,将变量存在在线程中

CAS

CAS指令需要三个操作数:内存位置V,旧的预期值A, 和新值B。Cas指令执行时,当且仅当v的值符合A时,处理器用B更新V,否则它就不更新,但是无论是否更新了V,都会返回V的旧值。
CAS的ABA问题:如果一个变量v初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那我们就能说它的值没有被其他线程改变过了吗?如果这段时间它的值曾经被改为了B,后来又被改回了A,那么CAS操作就会误认为它从来没有被改变过,这就是ABA。
解决办法:使用带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。

锁优化

挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力

自旋锁与自适应

当共享数据的状态只会持续很短一段时间时,为了这段时间去挂起和恢复线程并不值得,这时可以让线程执行一个忙循环(自旋)

锁消除

如果一段代码中,堆上的数据都不会逃逸出去而被其他线程访问到,那就可以把它当作栈上数据对待,认为他们是线程私有的,同步加锁就可以消除。

锁粗化

如果虚拟机探测到有一串零碎的操作频繁地对同一个对象加锁解锁,将会把加锁同步的范围拓展到整个操作序列的外部。

轻量级锁

如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统锁更慢。

偏向锁

消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。