Java Memory Model(JMM)java内存模型,区别与java内存结构。JMM定义了一套在多线程读写共享数据(变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。
原子性
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
多线程情况下,对同一个对象进行操作时,会导致字节码指令交错执行,从而产生原子性问题,可以通过synchronize关键字解决
可见性
可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改
有序性
如果在本线程内观察,所有的操作都是有序的;(线程内表现为串行的语义)如果在一个线程中观察另外一个线程,所有的操作都是无序的。
多线程情况下,jvm会进行指令重排,会影响有序性。
happens-before
happens-before规定了哪些写操作对其他读操作可见,即前一个操作的结果可以被后续的操作获取。它是可见性与有序性的一套规则总结。
程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
传递规则:这个简单的,就是happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。
对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
synchronize和volatile对比
1、volatile是线程同步的轻量级实现,性能比synchronize好
2、volatile只能修饰变量,而synchronize可以修饰方法、代码块和变量
3、volatile多线程时不会发生阻塞,而synchronize会阻塞线程
4、volatile可以保证可见性和有序性(禁止指令重排),无法保证原子性,而synchronize都可以保证
总结:volatile就是保证变量对其他线程的可见性和防止指令重排序
而synchronize解决多个线程访问资源的同步性
锁状态
锁的状态总共有四种:
无锁状态(01)、偏向锁(01)、轻量级锁(00)和重量级锁(10)。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)
CAS和原子类
CAS即Compare and Swap,是一种乐观锁的思想。为了保证该变量的可见性,需要使用volatile修饰,结合CAS和volatile可以实现无锁并发,适用于竞争不激烈、多核CPU的场景。
原子操作类:AtomicInteger、AtomicBoolean,底层采用CAS+volatile实现。
Monitor
每一个java对象都会关联一个monitor对象,monitor对象的主要组成部分:waitSet(之前获得过锁的线程,条件不满足后会进入),EntryList(线程等待队列),Owner(锁持有者)。
synchronized重量级锁
基于悲观锁思想,加锁后关联monitor对象,线程进入后成为monitor的owner(即owner指向该线程),当其他线程访问时会先检查monitor对象是否空闲,若monitor对象已经被持有,则进入entryList等待,从而实现阻塞。
synchronized轻量级锁
如果多线程访问一个对象的时间是错开的,则可以使用轻量级锁来优化。
加锁过程:如果加锁对象为无锁状态(01)时,线程首先会在栈帧中创建一个锁记录空间(Lock Record),用于存储锁对象的Mark Work,并将使用CAS操作锁对象的Mark Work更新为指向锁记录的指针且将锁记录里的owner指针指向锁对象的Mark Work,此时锁对象的锁标志位是00(轻量级锁),如果更新失败,jvm首先会检查锁对象的Mark work是否指向当前线程的栈帧,是的话就进行锁重入,继续执行同步代码,否则说明多线程竞争锁,轻量级锁就要升级为重量级锁。
解锁过程:通过CAS操作尝试把线程中的Mark word 替换当前对象的Mark word,若操作成功则解锁完成,否则说明锁已经膨胀为重量级锁,那就要在释放锁的同时唤醒被挂起的线程。
锁膨胀
锁膨胀是指轻量级锁在出现竞争的情况下,当线程1持有轻量锁时,线程2过来竞争的时候,轻量锁会膨胀为重量锁,线程2会进入等待队列(EntryList)。
锁自旋
重量级锁竞争的时候,不一定马上进入阻塞,还可以使用自旋来进行优化,如果当前线程自旋成功,就避免了阻塞。
- java6之后自旋锁是自适应的,若自旋成功过,下次就会多自旋几次;反之,就少自旋或不自旋
- 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU才能发挥优势
- java7之后不能控制是否开启自旋功能
偏向锁
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径(即减少锁重入),因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。
偏向锁:只有第一次使用CAS时将线程ID设置到对象的Mark Work头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。
一个对象创建时默认开启偏向锁,但该默认是延迟的(可通过VM参数BiasedLockingStartupDelay=0来禁用延迟),不会在程序启动时立即生效。
- 轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
偏向锁撤销
- 对象调用hashCode()方法会禁用该对象的偏向锁,原因就是调用了hashCode()方法,对象头就没有地方存放线程id了,只能禁用该对象的偏向锁。重量级锁在monitor对象中存储hashCode。
- 当两个及以上线程使用同一个对象时,偏向锁将会升级为轻量级锁,如果这些线程会产生资源竞争,则进一步升级为重量级锁。
- 对象调用wait/notify,也会撤销对象的偏向状态,原因是只有重量级锁才会有wait/notify机制
- 连续撤销偏向超过40 次(超过阈值),jvm会认为确实偏向错了,于是所有类都不可偏向,新建的对象也不可以偏向
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID;当撤销偏向锁达到阈值 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至t2。因为前19次是轻量,释放之后为无锁不可偏向,但是20次后面的是偏向t2,释放之后依然是偏向t2。
锁消除
JIT即时编译器对字节码做的优化,当判断加锁对象线程安全时,会进行锁消除。
锁粗化
JIT即时编译器对字节码做的优化,当发现相邻的synchronize块使用的是同一个锁对象,那么就会把这几个synchronize块合并为一个加大的同步块,避免频繁申请和释放锁。
网友评论