今天依旧在拜读周志强老师的<深入理解JVM虚拟机> ,结合了自己的能力,有了一定的理解,总结一下
并发处理的广泛应用是Aamdahl定律代替摩尔定律成为计算机性能发展源动力的根本原因,也是人类压榨计算机运算能力的最有利武器
衡量一个服务性能的高低好坏,每秒事务处理数是重要指标之一
先了解一下计算机的硬件: 计算机的存储设备与处理器的运算速度有这几个数量级的差距,所有有一个一层或者多层读写速度尽可能接近处理器运算的 告诉缓存来作为内存与处理器之间的缓冲(也就是我们了解的Cache):将运算需要使用的数据复制到缓存中,让运算能快速运行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了.
它也引入了一个新的问题: 缓存的一致性,在多路处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主内存,这种系统也被称为 共享内存多核系统
解决一致性问题:需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,这些协议 MSI MESI MOSI Synapse Firfly及Dragon Protocol等
Java的内存模型的主要目的是定义程序中各种变量的访问规则,也就是关注虚拟机中把变量的值存储到内存中,或者取出内存中的变量值的底层细节.
java内存模型规定了所有变量都存储在主内存中,每条线程都有自己的工作内存,线程对变量的所有操作(读取,赋值) 都必须在工作内存中进行,而不能直接读写主内存中能够的数据,不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成.
java内存模型定义了8种操作来完成主内存和工作内存数据的交互
java内存模型也定义了在这8中基本操作必须满足以下规则
volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制 也可以用synchronized关键字来进行同步
volatile有两个主要的特性 第一:保持变量对所有线程的可见性 第二:禁止了指令的重排序
可见性:这里的可见性可以理解为:当一条线程修改了这个变量的值,新值对于其他线程来说可以是立即得知的,而普通变量得不到这一点,普通变量的值在线程间传递均需要通过主内存来保证 线程之间的变量可见性
**指令重排序:**处理器采用允许 将多条指令不按照程序规定的顺序分开发送给各个相应的电路元件进行处理,处理器必须能够正确处理指令依赖情况保障城程序能够得出正确的执行结果
volatile变量读操作与普通变量的性能差不多,但是写操作可能会比较低一点,因为它需要在本地代码中插入很多的内存屏障指令来保证处理器不发生乱序执行,但是volatile还是比加锁的性能高很多
Java内存模型对volatile变量定义的特殊规则的定义
java内存模型都要求lock,unlock,read,load,user,assign,store,write这八种操作都具有原子性,但对于long和double的64位数据的读写操作划分为两次的32位的操作来进行,即允许虚拟机实现自行选择是否要保证64位数据类型的read load store write这四种操作的原子性
在实际开发中,除非明确指出被long和double修饰的变量存在线程竞争,否则一般不用修饰为volatile
原子性: java内存模型直接保证原子性变量的从操作包括:read,load,user,assign,store,write这六种操作,也就是说 基本内存类型的访问,读写都是具有原子性的 当然这里要除 long和double两种基本数据类型
如果需求中要求更大范围的原子性保证,java内存模型也给我们提供了 lock和unlock操作,虽然JVM并没开放给用户但是它提供了跟高级的字节码指令:monitorenter和monitorexit也就是我们所熟知的synchro-nized关键字
可见性:就是指当一个线程修改了共享变量的值时,其他线程会立马得知这个修改 也就是我们上面提到的volatile关键字,当然java还有两个关键则:synchronized和final
首先synchronized是保证当一个变量执行 unlock操作之前,必须把此变量同步给主内存中
final关键字是 被其修改的字段在构造器中一旦被初始化完成,并且构造器中没有把 this的引用传递出去(this逃逸)
有序性: volatile关键字,有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,那么所有操作都是无序的 前半句是指:线程内的串行,后半句是指指令的重排序以及主内存和工作内存的同步延迟
Java语言提供了两个关键字:synchronize和volatile关键字; 前者:是由一个变量在同一时刻只允许一个线程对其lock操作获得的, 后者是在本地代码中加入了很多的内存屏障来保证有序的
是Java 内存模型中定义的两项操作之间的偏序关系, 比如说操作A先行发生于操作B 其实就是说发生操作B之前,操作A产生的影响能被操作B观察到,这里的影响包括内存修改了共享变量的值,发送了消息,调用了方法等等
下面是Java内存模型下一些天然的先行发生关系
线程的实现主要有一下三种方式: 内核线程的实现(1:1) 用户线程的实现(1:N) 使用用户线程和 轻量级的进程的混合实现(N:M)
也被称为1:1 的实现 内核线程是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器 对线程进行调度 并负责将线程的任务映射到各个处理器上,每个内核线程可以视为一个内核的分身,这样操作系统就有能力同时处理多件事情,这样能处理多线程的内核 就被称为多线程内核
程序一般不会直接使用内核线程,而是内核线程的一种高级接口-------轻量级进程
轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程, 才能有轻量级进程 这样的轻量级进程 与内核线程之间的1:1关系被称为一对一的线程模型
由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使其中一个轻量级进程在系统调用中被阻塞到了,也不会影响整个进程继续工作 轻量级进程也具有他的局限性: 首先它是基于内核线程实现的,所有的线程操作,如创建, 析构以及同步 都需要进行系统调用,而系统调用的 代价相对较高,需要用户态向内核态的来回切换,其次,每个轻量级进程都需要一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的
用户线程的实现 方式被称为 1:N的实现
广义上来讲 一个线程只要不是内核线程就是 用户线程,因此从这个定义上来说轻量级进程也是用户线程的一种,但是轻量级进程要建立在内核之上的,许多操作都要进行系统调用,因此效率会受到限制,并不具备通常意义上的用户线程的优点
狭义上来讲,用户线程是指完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现. 用户线程的建立, 同步, 销毁 , 和调度完全是在用户态中完成的 不需要内核的帮助. 如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是 非常快速且低功耗的 也能够支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的
优势: 不需要系统内核支援 劣势: 没有系统内核的支援 所有的线程操作 都系要由用户线程自己处理. 线程的创建, 销毁, 切换, 调度, 都是用户必须考虑的问题 而且由于操作系统只能把处理器资源分配到进程, 那诸如 "阻塞如何处理 ",“多处理器系统如何将线程应声道其他处理器上” 这类的问题解决起来 异常的困难
线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种内核线程与用户线程结合使用的实现方式,即N:M 实现
在这种混合是线下,即存在 用户线程,也存在轻量级进程.
用户线程还是完全建立在用户空间中,因此用户线程的创建,析构等操作依然廉价,并且 可以支持大规模的用户线程并发. 而操作系统支持的轻量级进程则作为 用户线程和 内核线程之间的桥梁,这样可以使用 内核提供的线程调度功能以及处理器映射,并且用户线程的 系统调用要通过轻量级进程来完成,这大大降低了整个进程被完全阻塞的风险
分为两种:协同式线程调度,抢占式线程调度
协同式线程调度: 线程执行的时间由线程本身来控制,线程把自己工作执行完之后,要主动通知系统切换到理另一个线程上, 最大的好处就是实现简单, 而且由于线程要把自己的事情做完之后才会进行线程切换,切换线程对线程来说是可知的,所以一般没有什么线程同步问题, 坏处: 线程执行的时间控制,甚至如果一个线程的代码编写有问题,一直不告知系统进行线程切换,那么线程就一直会阻塞在那里.
抢占式线程调度: 每个线程将有系统来分配执行时间,线程的切换不由线程本身决定,. 在这种线程调度的方实现,线程的执行时间是系统可控的,也不会有一个线程导致进程甚至整个系统阻塞的问题
虽然说Java的线程调度是系统自动完成的,但我们仍然可以建议操作系统来给某个线程多分配一些时间,另一些线程少分配一些时间----------这项操作就是通过设置线程的优先级来完成
概念: 当多线程同时访问一个对象时,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要做一些额外的同步, 或者在调用方不用做其他的协调操作,调用这个对象的行为都可以获取正确的结果,那么就称这个对象是线程安全的
可以将各种操作共享的数据分为五类: 不可变 , 绝对线程安全 相对线程安全 线程兼容 线程对立
不可变 用final修饰的对象 只要一个不可变的对象被构建出来(没有发生this引用逃逸) 那么在外部的可见状态永远不会改变,永远都不会看到它在多个线程之中处于不一致的状态
如果是一个基本数据类型,在定义时使用final关键字修饰它就可以保证它时不可变的,如果共享数据是一个对象就需要对象自行保证其行为不会为其状态产生任何影响 比如 String对象实例,用户调用他的substring() replace() concat() 这些方法都不会影响它原来的值,只会返回一个新构建的字符串对象
除了String类型 还有 Number的部分子类 Long Double等数值的包装类型. BigInteger BigDecimal等大数据类型
**绝对线程安全: ** 在Java API中标注自己是线程安全的类都不是绝对的线程安全 例如:Vector是一个线程安全的类,具备了原子性.可见性 有序性 ,但是并不标志着 调用他的时候就永远不需要在同步手段了
如果说Vector一定要做到绝对的线程安全,那就必须在他内部维护一组一致性的快照访问才行,每次对元素进行改动之后,都要产生新的快照,这样付出的时间和 空间成本都是非常大的
相对线程安全: 就是我们通常意义上的线程安全,我们在调用的时候不需要进行额外的保护措施,但是对应一些特殊的连续调用,就需要在调用端使用额外的同步手段来保证调用的正确性
线程兼容: 是指对象本事不是线程安全的,但是通过调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用,平时所说的一个类不是线程安全的就是指这种情况
线程对立: 是指不管调用端是否采用同步措施,都无法在多线程并发使用代码 例子: Thread类的suspend()和resume()方法 中断线程和恢复线程
互斥同步: 同步是指在多线程并发的访问共享数据时, 保证共享数据在同一时刻只能被一条线程所访问
互斥是指 同步的一种手段, 临界值, 互斥量, 信号量都是常见的互斥实现方式 所有有一句换总结互斥同步: 互斥是因,同步是果,互斥是方法,同步是目的
在Java中最基本的互斥同步就是synchronized关键字:synchronizod 关键宇经过 Javac 编译之后,会在同步块的前后分别形成 monitorenter 和monitorexit 这两个宇节码指令。这两个字节码指令都需要个reference 类型的参数来指明要锁定和解锁的对象。如果 Java 源码中的 synchronized 明确指定了对象参数,那就以这个对象的引用作为 reference;如果没有明确指定,那将根据synchronized 修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的 Class 对象来作为线程要持有的锁
在执行 monitorenter 指令时,首先 要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行 monitorexit 指令时会将锁计数器的值减一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止
JUC包中的ReentrantLock: 不过,ReentrantLock 与synchronized 相比增加了一些高级功能,主要有以下三项:等待可中断、可实现公平锁及锁可以鄉定多个条件。
满足以下条件,仍然推荐在 synchronized 与 ReentrantLock 都可满足需要时优先使用 synchronized
基于冲突检测的乐观并发策略,通俗的来说就是不管风险,先进行操作,如果没有其他线程争用共享数据,就直接操作成功;如果共享的数据被争用,产生了冲突,在再进行其他的补偿措施,最常用的补偿措施就是 不断的重试,直到出现没有竞争的共享数据为止,这种乐观并发策略的实现不再需要把线程的阻塞挂起, 因此被称为 非阻塞同步 也被称为无锁编程
我们必须要求操作和冲突检测这两个步骤具有原子性,只有靠硬件来实现这件事情,硬件保证某些从语义上看起需要多次操作的行为可以只通过一条指令就能完成,这类指令介绍一下 CAS
CAS 指令需要有三个操作数,分别是内存位置(在Java 中可以简单地理解为变量的内存地址,用V表示)、旧的预期值(用A 表示)和准备设置的新值(用B 表示)。CAS 指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不执行更新。但是,不管是否更新了 V的值,都会返回 V的旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断。
在JDK 5 之后,Java 类库中才开始使用 CAS 操作,该操作由 sun.misc.Unsafe 类里面的 compar-eAndSwapInt0 和compareAndSwapLong0 等几个方法包装提供。HotSpot 虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是—条平合相关的处理器 CAS 指令,没有方法调用的过程,或者可以认为是无条件内联进去了。不过由于 Unsafe 类计上就不是提供给用户程序调用的类 (Unsafe:getUnsafe) 的代码中限制了只有启动类加载器 ( Bootstrap ClassLoader)加载的 Class 才能访问它),因此在 JDK 9之前只有 Java 类库可以使用 CAS,臂如J.U.C包里面的整数原子类,其中的 compareAndSet0 和 getAndincre-mento()等方法都使用了 Unsafe 类的CAS 操作来实现。而如果用户程序也有使用 CAS操作的要求,那要么就采用反射手段突破 Unsafe 的访问限制,要么就只能通过 Java 类库 AFI来间接使用它。直到JDK 9之后,Java 类库才在 VarHandle 类里开放了面向用户程序使用的CAS 操作
尽管 CAS 看起来很美好,既简单又高效,但显然这种操作无法涵盖五斥同步的所有使用场景,并且CAS 从语义上来说并不是真正完美的,它存在一个逻辑漏洞:如果一个变量V初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然为 A值,那就能说明它的值没有被其他线程改变过了吗?这是不能的,因为如果在这段期间它的值曾经被改成B,后来又被改回为 A,那CAS 操作就会误认为它从来没有被改变过。这个漏洞称为 CAS操作的“ABA 问题”。J.U.C包为了解决这个问题,提供了一个带有标记的原子引用类
AtomicStampedReference,它可以通过控制变量值的版本来保证CAS 的正确性。不过目前来说这个类处于相当鸡助的位置,大部分情況下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更为高效。
可重入代码: 这类代码被称为纯代码,是指可以在代码执行的任何时刻中断他,转而去执行另一段代码(包括调动他的本身),而控制权返回后,原来的程序不会出现任何错误,也不会对结果有所影响. 可重入代码是线程安全代码的一个真子集
我们可以根据一个比较简单的原则判断代码是否是可重入性de: 如果一个方法的返回结果时可以预测的,只要输入了相同的数据,就能返回相同的结果,那它就满足可重入性的要求,当然也是线程安全的
线程本地存储: 如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行
当一个线程想要获取到共享数据的锁的时候,发现这个锁被另一个线程占用,就会被阻塞,
我们可以让这个线程"稍等一忽儿" 但不放弃处理器的执行时间,看看持有锁的线程是否很快就会被释放,为了让线程等待,我们只需让线程执行一个忙循环(自旋) 这就是所谓的自旋锁
在JDK6中对自旋锁的优化,引人了自适应的自旋。自适应意味着自旋的时间不再是固定的了,而是由的一次在同一个锁上的自流时间双锁的姐有者的达态来决定的,如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋等待持续相对更长的时间比如持续 100 次忙循环。另一方面,如果对于某个锁,自旋很少成功获得过锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,
随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越精准,虚拟机就会变得越来越“聪明”了。
锁消除是指 虚拟机及时编译器在运行时检测到某段需要同步的代码根本不可能存在共享数据竞争而实施的一种多锁进行消除的优化策略
如果一段代码中,在对上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当做栈数据对待,认为它们是线程私有的,同步加锁自然就无须再进行
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能消耗 就会将锁粗化
它的设计初衷是在没有多线程竞争前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
Hotspot 虚拟机的对象头 分为两部分 一部分用户存储对象自身的运行时数据,如 HashCode GC的分代年龄等等这类数据将长度在 32位 和 64位的Java 虚拟机中分别占用32个和64个比特 官方将其成为 Mark Word 另一部分用于存储指向方法区对象类型数据的指针
Mark Word被设计成一个非固定动态数据结构,以便在极小的空间内存储更多的信息
工作过程: 加锁:在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 01 状态),虚拟机首先将在当前线程的栈帧建立一个名为锁记录(Lock Record)的空间,用于存储锁对象的目前的Mark Word的拷贝(官方为这份拷贝加入了一个Displaced前缀,即 Displaced Mark Word). 然后虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针 如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个比特)将转换成 00 表示此对象处于轻量级锁定状态 如果这个更新动作失败了,那就以为着至少存在一个线程与当前线程竞争获取该对象的锁. 虚拟机首先检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已拥有这个对象的锁, 那就直接进入同步块 继续执行就可以了,否则就说明这个锁对象被其他线程抢占了, 如果出现两条以上的线程争用同一个锁的情况,那 轻量级锁就不在有效,必须膨胀为 重量级锁 ,锁标志的状态位 10 此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待所得线程必须进入阻塞状态
解锁: 他的解锁过程 同样是通过CAS操作来执行的,如果对象的Mark Word仍然指向线程的锁记录 , 那就用CAS操作把对象当前的 Mark Word 和线程中复制的Displaced Mark Word 替换回来,如果替换成功,那整个同步过程就顺利完成了, 如果替换失败了,则说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程
轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞年的”这一经验法则。如果没有竞争,轻量级锁便通过 CAS 操作成功避免了使用互斥量的开销;但如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了 CAS操作的开销。因此在有竞手的情况下,轻量级锁反而会比传统的重量级锁更慢。
目的是为了消除数据在无竞争的情况下的同步原语.进一步提高程序性能
如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS都不需要做了
它的意思是这个锁会偏向于第一个获得它的线程,如果接下来的执行过程中,该锁一直没有被其他线程获取,则持有偏向锁的线程则永远不需要在进行同步
过程: 当锁对象第一次被线程获取的时候,虚拟机会把对象头的标志位设置为 01 把偏向模式设置为 1 表示进入偏向模式, 并使用CAS操作把获取到的这个所得线程的ID 记录在对象的Mark Word之中, 如果CAS操作成功, 持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何的同步操作(例如 加锁,解锁以及对Mark Word更新操作)
一旦出现另一个线程去尝试获取这个锁的情况,偏向模式就立马宣告结束 根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为 0 ) ,撤销后标志位恢复到未锁定(标志位 为 01 ) 或轻量级锁定(标志位 为 00 ) 的状态,后续的同步操作就按照情况及锁那样去执行
如果程序中大多数的锁总是被多个不同线程访问,那么偏向模式就是多余的 在具体问题具体分析的前提下,有时候可以使用 参数 -XX:UseBiasedLocking来禁止偏向锁优化反而会提升性能
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/qq_47109099/article/details/126264674
内容来源于网络,如有侵权,请联系作者删除!