JVM原理学习:线程与内存模型篇
标题定义时,我寻思着”Java高并发编程和线程安全“作为标题会不会更好一点,日常开发中线程安全主要是同步锁实现,属于Java语言级别基础用法,但Java并不是JVM的独生子,JVM支持Scala等其他语言,jVM只认识字节码。实质上,JVM提供原语支持保证资源访问原子性。本文主要记录JVM为并发编程做了什么努力,大部分思想来源于《深入理解Java虚拟机第二版》。
一、什么是线程安全?
在《深入理解Java虚拟机第二版》386页中有段较恰当的描述”当多个线程访问同一个对象时,如果不用考虑这些线程运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,单次调用可以获得正确的结果,那这个对象就是线程安全的“。这个定义很全面,关注了高并发下线程安全实质上是资源访问安全,注意到”线程“、”同一个对象“、”运行时环境“、”同步操作“等关键词,用于理解JVM如何解决这些问题。
二、如何保证资源访问安全?
1、局部变量
运行时环境,JVM将字节码文件描述的方法参数和局部变量保存在虚拟机栈中,虚拟机栈是线程私有的。
2、线程本地变量
运行时环境,Thread持有的ThreadLocalMap对象保存了键值对,其中key为该线程位置标志,value是需要保存的值的拷贝,是线程私有的。
3、共享资源锁
当多线程访问同一个资源时,由JVM内存模型规定进行资源抢占,谁占有资源宏观上是随机的。Java代码中ReentrenLock、Synchronized提供资源锁定功能。JVM底层提供了monitorenter和moniterexit等字节码指令以支持同步锁功能,同时物理机器提供了CompareAndSwap等机器指令集为乐观锁提供支持,说到这还难以理解,以下通过JVM、物理机器等逐步解释同步锁和乐观锁。
三、JVM如何保证资源可见以及锁?
JVM通过monitorenter锁定同步代码块,完成后通过monitorexit释放锁。同一个线程多次获取同一个锁时,表现为锁的计数,也叫可重入,synchronized便是可重入锁。
1、锁类型和标识
JDK1.6实现了很多锁优化类型。存放于Java堆中的对象中,对象头存放了运行时数据(Mark Word)和方法区引用指针,其中运行时数据首部的对象哈希码存放了对象锁,分为无锁、轻量级锁、重量级锁、偏向锁等,这些锁分类的目的在于提供锁的性能,因为锁定资源会引起线程阻塞,导致物理机器内核调用进行线程切换,造成巨大开销。这些锁作用如下:
轻量级锁:只有一个线程,避免切换
重量级锁:多线程下,需要切换线程
偏向锁:获取锁成功的线程,提高下次获取成功的概率
无锁:不加锁,或通过CAS乐观点
2、内存模型JMM
JVM在宏观上将数据分别存储于Java堆、方法区、虚拟机栈、本地方法栈、程序计数器等;微观上,执行时的访问变量建议存放于高速缓存,未访问变量存放于主内存。这是为了解决CPU和主内存速度差异实现的,但高速缓存中的数据同一时刻却不一定等于主内存,数据一致性以及同步锁应该如何作用?为了解决数据一致性问题,JVM约定了一套内存模型JMM。
JMM是实现主内存、高速缓存间数据安全迁移功能的。主要分为8个原子指令,lock/unlock、read/write、load/store、use/assign等。数据迁移指令的关系如下:
主内存:read/write <----> 高速缓存:load/store <----> 执行时高速缓存:use/assign
访问变量时,一般都是从线程私有的高速缓存中读取(load),此时不同线程访问资源皆为高速缓存的副本,主内存才是实际值,此时数据很可能是过时。为了解决数据过时问题,Java提供了volatile修饰符,让线程直接从主内存读取数据(read/load组合),从而保证数据可见性。
lock/unlock是解决主内存资源唯一访问的保障,其中lock锁定主内存时会将所有高速缓存相关值清空,从而保证数据直接从主内存读取最新值。同时也提供了同步锁支持。
四、物理机器如何保证资源安全
物理机器指令集中,还提供了各种原子指令如CAS(CompareAndSwap),保证同时只有一个资源可以写入,从而实现只有一个线程可以获取锁资源。因此,线程安全是需要机器支持的,然而大多数机器都支持。在Java中,sun.misc.Unsafe包封装了访问底层物理机指令集,因此实现了一些资源访问原子性(如AtomicInteger)。
本文来自博客园,作者:SArtOnline,转载请注明原文链接:https://www.cnblogs.com/sartonline/p/14605629.html