JMM
概述
内存模型
- java[内存模型](Java Memory Model) 和 [内存结构]
- JMM规定了在多线程下对共享数据的读写时,对数据的原子性 有序性 可见性的规则和保障。
原子性
- 原子性问题:
i++和i--不是原子性操作! 所以一个i++指令会在执行过程中被另一个线程执行!
- 问题分析:
(1)共享的变量信息是放在主内存中的,线程呢是在工作内存中
多线程情况下指令交错产生的问题:
- 问题解决
java中通过synchronized来保证了原子性
EntryList: 排队等候区:阻塞住
Owner: 有一个一个线程可以进入成为owner
monitorExit: 表示owner中线程执行完毕,会通知EntryList中等待的线程。然后就可以拥有抢夺owner的机会,当等待的线程成为owner后就会执行monitorenter 锁住monitor区域
WaitSet: 是线程wait() notify()的时候需要用到的区域
优化:
Java锁机制
64位开启指针压缩为例:
- java中锁的实现:每个对象都拥有一把锁,该锁存放在对象头中。锁中记录了当前对象被哪个线程所占用。
-
对象的结构
- 对象头 :(存放了一些对象本身的运行时信息)
- Mark Word:存储了很多和当前对象运行时状态有关的数据。
- Class Pointer: 指向了堆中当前类class对象。
- 实例数据:属性 + 方法
- 对齐填充字节: 为了满足对象的大小为8个字节的倍数,无实际意义。
- 对象头 :(存放了一些对象本身的运行时信息)
-
锁状态:对象头的Mark Word中"锁标志位"分别对应四种锁状态:无锁、偏向锁、轻量级锁、重量级锁
-
synchronized(重量级锁)实现线程同步:
- 通过生成的monitorenter和monitorexit来实现同步。结合monitor区域来实现线程同步。
synchronized 缺点:本质:通过线程状态的切换。用户态<-->内核态,所以很慢。
synchronized 编译后是通过jvm指令monitorenter和monitorexit来实现同步,而该jvm指令实际上依赖操作系统的mutex lock来实现的。java线程实际上是对操作系统线程的映射,所以每当挂起/唤醒一个线程,都要从"用户态"切换。操作系统的"内核态",这种操作是比较重量级的!一些情况下甚至切换时间本身会超过线程执行任务的时间!所以使用synchronized会对性能产生严重的影响!
jdk 6后,对synchronized进行了优化,引入了无锁、偏向锁、轻量级锁
- 对应了对象头Mark Word中的四种状态[无锁 偏向锁 轻量级锁 重量级锁]
注意锁只能升级不能降级!
synchronized 优化
synchronized是如何优化的?四种状态如何变化的?
-
无锁:线程都可以来访问该资源
- 无竞争
- 存在竞争.可以通过非锁方式,同步线程。失败重试(乐观锁思想)
- CAS: Compare And Swap:CAS在操作系统中通过一条指令来实现,所以能够保证原子性!
且在标价和交换的过程中,必须是原子性。不然两个线程就会抢到一个资源! - cas和锁的区别:
- 锁: 通过锁定资源的方式,保证线程同步。给对象加锁。
- cas: 通过别的方式,不锁定资源。
- CAS: Compare And Swap:CAS在操作系统中通过一条指令来实现,所以能够保证原子性!
-
偏向锁:
- 对象能够认识这个线程,这个线程来了直接把锁交出去,认为对象偏向这个线程。
- 在偏向锁状态时通过线程ID来确定当前想要获得对象锁的线程。
- 当有多个线程来竞争这把锁,偏向锁会升级为轻量级锁。
- 使用场景:只有一个线程来获取资源,单线程。
-
轻量级锁:这时线程会在虚拟机栈中开辟一块被称为Lock Record的空间,线程通过cas获取锁,一旦获取到锁,会复制该对象的mark Word,并且将Lock Record中的owner指针指向该对象,该对象的对象头的Mark Word的前30个bit也会指向栈中锁记录的指针,这就实现了对象与线程的绑定。
这时候也有其他线程想要获取该对象该怎么办?
- 自旋等待:线程自己在不断的轮询循环,尝试着看一下目标对象的锁有没有被释放。
- 这种方式区别于被操作系统挂起,因为对象的锁很快被释放的话,自旋就不需要系统中断和现场恢复。所以效率高一些。
- 自旋相当于cpu在空转,如果时间过长会占用资源。
- 如果自旋等待的线程数超过1个,那么轻量级锁会升级为重量级锁。
-
如果对象头中锁状态被标记为重量级锁,那么需要通过monitor来对线程进行控制。
此时将会完全锁定资源,此时的管控也最为严格.
轻量级锁和重量级锁的区别:
- 重量级锁:通过monitor来控制线程同步,等待的线程会进入阻塞状态。涉及用户态到内核态的转变,这属于比较重量级的操作。
- 轻量级锁:通过等待的线程进行自旋的方式进行等待,不会阻塞。
CAS
- 不锁定资源,也能同步线程
- 预期原值与新值
- 预期原值: 之前读到的资源对象的状态值
- 新值: 想要将预期原值更新后的值
- 内存位置: 资源本身当前的值(资源的状态值)
- 比较
- 比较预期原值和内存位置(资源的状态值),一致的话将内存位置改为新值。这样别的线程的预期原值和内存位置就不一样了,就不操作了。
- 核心问题
- "比较数值,并进行值的更新"这个操作必须是原子的!不然会出问题!!会有两个线程同时抢到...同时只能有一条线程进行操作!!!
- 如何实现CAS的原子性?
- CAS在操作系统中就是一条指令,所以是原子性的。
CAS: 乐观锁。本质是一种无锁的同步机制。Java底层是通过Unsafe的方法实现CAS操作。
可见性
volatile: 一个线程写,多个线程读的场景,无法保证原子性。
一. 可见性问题
二. 可见性问题分析
线程t1对于t2修改后的值不清楚的原因是,t1线程频繁的读取boolean这个变量。然后即时编译器就会视为热点代码。将boolean的值缓存到高速缓存中。所以t1每次读取都是从自己的工作内存中读取。主内存中改了值,其实t1线程是感知不到的。
synchronized 和 volatile 比较
- synchronized 关键字用于给代码块上锁,volatile用于修饰成员变量/静态变量
- synchronized 既可以保证原子性,也可以保证可见性。但是属于重量级的操作,是一种重量级锁,线程的用户态->内核态的转变。
- volatile可以保证可见性和有序性,但是不能保证原子性。通过读写屏障保证线程之间的
可见性和禁止编译器/处理器对指令进行指令重排序!
有序性
volatile保证了禁止指令重排序。
有序性的理解
- 单线程情况下jvm对其指令重排是不太影响结果的。(指令重排是一种优化)
- 多线程下指令重排就会产生一些问题!
单例模式下双重锁机制的变量需要加volatile,否则会指令重排。使得返回的对象不完整。
happens-before
规定了哪些写操作可以对其他线程的读操作可见,是可见性与有序性的一套规则:
CAS
一. 概述
CAS是与volatile配合使用的一项技术。体现的是一种乐观锁的思想,是一种无锁并发技术。
compareAndSwap
:如果旧值和共享变量相同,则进行swap,把共享变量改为结果。
CAS适用场景
- 竞争不激烈,多核CPU的情况下。因为等待的线程并不是进入阻塞状态,而是一种在尝试尝试。其他线程也需要占用CPU资源。如果竞争激烈,会影响效率!
- 获取"主内存中的值时",为了保证变量的可见性,需要使用
volatile
来修饰!结合CAS和volatile可以实现无锁并发! - 与
synchronized
的比较:- 该种并发技术并不会使等待的线程进入阻塞状态,而是通过不断尝试的方式来操作。但是当竞争激烈时,等待的线程会占用过多CPU资源,导致效率下降!
- CAS操作底层依赖于一个
Unsafe
类直接调用操作系统底层的CAS指令。
二. 底层实现
CAS操作底层依赖于一个Unsafe
类直接调用操作系统底层的CAS指令。
三. 原子操作类
- java中的悲观锁:
synchronized
- java中的乐观锁:CAS
锁膨胀
自旋锁:自旋是一种在重量级锁上的优化,并不会让等待的线程进入阻塞状态,而是处于一种
自旋重试的操作!
其他优化
- 缩少锁的粒度:
- ConcurrentHashMap: 原来HashTable是锁住了整个数组,ConcurrentHashMap是给数组(每个链表头)进行加锁,也就是分段的机制,当前段的上锁,不影响其他段的读写操作!
JMM总结
- Java中锁的实现
每个对象都拥有一把锁,该锁存放在对象头中。锁中记录了当前对象被哪个线程所占用。
- 对象的结构
- 对象头:存放了一些对象本身的运行时信息,包括两部分:
- Mark Word: 存储了很多和当前对象运行时状态有关的数据。
- Class Pointer: 指向了堆中当前类class对象。
- 实例数据: 属性 + 方法
- 对齐填充字节: 为了满足对象的大小为8个字节的倍数,无实际意义。
- 锁状态
对象头的Mark Word中"锁标志位"分别对应四种锁状态:
无锁01、偏向锁01、轻量级锁00、重量级锁10
synchronized
(重量级锁) 实现线程同步
通过生成的monitorenter
和monitorexit
来实现同步。
synchronized
的缺点:本质上是通过线程状态的切换,用户态<-->内核态,所以很慢。synchronized
编译后是通过JVM指令monitorenter
和monitorexit
来实现同步,而这些JVM指令实际上依赖操作系统的mutex lock来实现的。Java线程实际上是对操作系统线程的映射,所以每当挂起/唤醒一个线程,都要从"用户态"切换操作系统的"内核态",这种操作是比较重量级的!一些情况下甚至切换时间本身会超过线程执行任务的时间!所以使用synchronized
会对性能产生严重的影响!
monitor区域...
从JDK 6后,对synchronized
进行了优化,引入了无锁,偏向锁,轻量级锁。
注意,锁只能升级不能降级!
synchronized 优化
synchronized是如何优化的?四种状态如何变化的?
-
无锁:线程都可以来访问该资源
-
无竞争
-
存在竞争.可以通过非锁方式,同步线程。失败重试(乐观锁思想)
-
CAS: Compare And Swap:CAS在操作系统中通过一条指令来实现,所以能够保证原子性!
且在标价和交换的过程中,必须是原子性。不然两个线程就会抢到一个资源!
-
cas和锁的区别:
- 锁: 通过锁定资源的方式,保证线程同步。给对象加锁。
- cas: 通过乐观锁思想方式,不锁定资源。
-
-
-
偏向锁
- 对象能够认识这个线程,这个线程来了直接把锁交出去,认为对象偏向这个线程。称为偏向锁。
- 在偏向锁状态时通过线程ID来确定当前想要获得对象锁的线程。
- 当有多个线程来竞争这把锁,偏向锁会升级为轻量级锁。
- 使用场景: 只有一个线程来获取资源。单线程。
-
轻量级锁
- 这时线程会在虚拟机栈中开辟一块被称为Lock Record的空间。
- 线程通过CAS获取锁,一旦获取到锁,会复制该对象的Mark Word,并且将Lock Record中的owner指针指向该对象,该对象的对象头的Mark Word的前30个bit也会指向栈中锁记录的指针,这就实现了对象与线程的绑定。
- 这时候也有其他线程想要获取该对象该怎么办?
- 自旋等待: 线程自己在不断的轮询循环,尝试着看一下目标对象的锁有没有被释放。
- 如果自旋等待的线程数超过1个,那么轻量级锁会升级为重量级锁。
- 锁膨胀: 竞争的线程想要通过CAS操作获取共享对象的锁的时候,获取不到。就会进行锁膨胀,转化为重量级锁!
-
重量级锁
- 如果对象头中锁状态被标记为重量级锁,那么需要通过monitor来对线程进行控制...
- 此时将会完全锁定资源,此时的管控也最为严格。
- 如果对象头中锁状态被标记为重量级锁,那么需要通过monitor来对线程进行控制...
轻量级锁和重量级锁的区别:
- 重量级锁:通过monitor来控制线程同步,等待的线程会进入阻塞状态。涉及用户态到内核态的转变,这属于比较重量级的操作。
- 轻量级锁:通过等待的线程进行自旋的方式进行等待,不会阻塞。
CAS
- 不锁定资源,也能同步线程
- 预期原值与新值
- 预期原值: 之前读到的资源对象的状态值
- 新值: 想要将预期原值更新后的值
- 内存位置: 资源本身当前的值(资源的状态值)
- 比较
- 比较预期原值和内存位置(资源的状态值),一致的话将内存位置改为新值。这样别的线程的预期原值和内存位置就不一样了,就不操作了。
- 核心问题
- "比较数值,并进行值的更新"这个操作必须是原子的!不然会出问题!会有两个线程同时抢到。同时只能有一条线程进行操作!
- 如何实现CAS的原子性?
- CAS在操作系统中就是一条指令,所以是原子性的。
CAS: 乐观锁。本质是一种无锁的同步机制 + volatile。Java底层是通过Unsafe的方法实现CAS操作。