java知识点 垃圾回收,集合,多线程
2022-08-12 18:13:40 23 举报
AI智能生成
java知识点
作者其他创作
大纲/内容
jvm
垃圾回收机制
垃圾在哪?
程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),所以不需要GC
如何确定垃圾
引用计数法
给对象中添加一个引用计数器,每当有 一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0 的对象就是不可能再被使用的。
缺点:很难解决对象之间相互循环引用的问题
可达性分析
首先从GC Roots的对象作为起点开始,然后向下搜索,搜索走过的路径称为引用链,如果一个对象没有任何引用链相连,则判断为对象不可用。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。当对象不可达(可回收)时,当发生GC时,会先判断对象是否执行了finalize方法,如果未执行,则会先执行 finalize方法,我们可以在此方法里将当前对象与GC Roots 关联,这样执行finalize方法之后,GC 会再次判断对象是否可达,如果不可达,则会被回收,如果可达,则不回收!
finalize方法只会被执行一次,如果第一次执行finalize方法此对象变成了可达确实不会回收,但如果对象再次被GC,则会忽略finalize 方法,对象会被回收!
finalize方法只会被执行一次,如果第一次执行finalize方法此对象变成了可达确实不会回收,但如果对象再次被GC,则会忽略finalize 方法,对象会被回收!
哪些对象可以作为GC Roots?
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
垃圾回收算法
标记清除算法(Mark-Sweep)
最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清
除阶段回收被标记的对象所占用的空间。
除阶段回收被标记的对象所占用的空间。
缺点:内存碎片化严重
复制算法(copying)
按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉
缺点:是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。
标记整理算法(Mark-Compact)
标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
分代收集算法
根据不同区域选择不同的算法
新生代 GC采用复制算法(Minor GC)。当JVM 无法为新建对象分配内存空间的时候 (Eden 满了),Minor GC 被触发。因此新生代空间占用率越高,Minor GC 越频繁。
老年代因为每次只回收少量对象,因而采用标记整理算法(MajorGC)
四中引用类型
强引用
把一个对象赋给一个引用变量,这个引用变量就是一个强引用。
即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
软引用
软引用需要用 SoftReference 类来实现,软引用通常用在对内存敏感的程序中。
当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收
弱引用
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短
只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用
虚引用的主要作用是跟踪对象被垃圾回收的状态。
GC 垃圾收集器
Serial 垃圾收集器
单线程、复制算法、 Client 模式、新生代
ParNew 垃圾收集器
Serial+多线程、复制算法、新生代、 Server 模式
Parallel Scavenge 收集器
新生代、复制算法、多线程、重点关注的是程序达到一个可控制的吞吐量、自适应调节策略
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
Serial Old 收集器
单线程、标记整理算法、Client模式、老年代
Parallel Old 收集器
老年代、多线程、标记整理算法、Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器
CMS 收集器
多线程、老年代、标记清除算法、最主要目标是获取最短垃圾回收停顿时间(SWT)
初始标记—并发标记—重新标记—并发清除
G1 收集器
标记-整理算法,不产生内存碎片;可以准确的控制停顿时间,在不牺牲吞吐的情况下实现低停顿的垃圾回收。(SWT)
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。
追求STW短:若ParNew/CMS用的挺好,就用这个;若不符合,用G1(CMS作为老年代收集器的时候,只能使用 ParNew 作为新生代收集器)
追求吞吐量:用Parallel Scavenge/Parallel Old,而G1在吞吐量方面没有优势
目前绝大数的生产环境都是使用的JDK8,若没有进行过调优,默认使用的是PS+PO的收集器;但是要进行调优时,会在CMS和G1中来进行选择,如果内存比较大(10G以上),最好使用G1,内存较小(几个G)可以考虑CMS。
追求吞吐量:用Parallel Scavenge/Parallel Old,而G1在吞吐量方面没有优势
目前绝大数的生产环境都是使用的JDK8,若没有进行过调优,默认使用的是PS+PO的收集器;但是要进行调优时,会在CMS和G1中来进行选择,如果内存比较大(10G以上),最好使用G1,内存较小(几个G)可以考虑CMS。
串行:Serial、Serial Old
并行:ParNew、Parallel Scavenge、Parallel Old
并发:CMS、G1
并行:ParNew、Parallel Scavenge、Parallel Old
并发:CMS、G1
并行:多条垃圾回收线程同时操作 并发:垃圾回收线程与用户线程一起操作
IO/NIO
一个字符表示一个汉字或英文字母,具体字符与字节之间的大小比例视编码情况而定。有时候读取的数据是乱码,就是因为编码方式不一致,需要进行转换,然后再按照unicode进行编码。
集合
Collection
List
有序集合
有索引
元素可重复
有索引
元素可重复
ArrayList
底层原理:数组 Object[] element
扩容机制:初始化如果不指定大小,则数组的大小为0,当大小为0时,第一次添加,则大小扩容至10,后续,再次扩容,则增加1.5倍的方式增加
查询速度快,增删慢
线程不安全
Vector
底层原理:数组
线程同步,即某一时刻只有一个线程能够写 Vector,线程安全,效率慢
查询速度快,增删慢
扩容机制:初始化大小为10,后续扩容默认为2倍。
也可通过构造方法Vector(int initialCapacity, int capacityIncrement)自定义初始化,以及扩容大小
也可通过构造方法Vector(int initialCapacity, int capacityIncrement)自定义初始化,以及扩容大小
LinkedList
底层原理:双向链表
扩容机制:由于是双向链表,所以不存在容量问题,不需要扩容
线程不安全
查询速度慢,增删快
Set
无序
不可重复
不可重复
HashSet
底层原理:HashMap 可见HashSet中的元素,只是存放在了底层HashMap的key上, value使用一个static final的Object对象标识
线程不安全
允许有 null 值,但是只能有一个
TreeSet
底层实现:treeMap
LinkHashSet
LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承于 HashSet,其所有的方法
操作上又与 HashSet 相同
操作上又与 HashSet 相同
Map
HashMap
数组+链表+红黑树
HashMap 最多只允许一条记录的键为 null,允许多条记
录的值为 nul
非线程安全,即任一时刻可以有多个线程同时写 HashMap
如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使
HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap
初始容量大小是创建时给数组分配的容量大小,默认值为16,负载因子默认0.75f
扩容:2n
用数组容量大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用rehash方法将数组容量增加到原来的两倍
在做扩容的时候会生成一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能.
map.put(k,v)实现原理
第一步首先将k,v封装到Node对象当中
第二步它的底层会调用K的hashCode()方法得出hash值
第三步通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上
ConcurrentHashMap
数组+Segment+分段锁
JDK1.7
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)
将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问
ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部
JDK1.8
数组+链表+红黑树
1.HashMap是 非线程安全的,而HashTabl e和ConcurrentHashmap都
是线程安全的
2.HashMap的key 和value均可以为null;而HashTable和Concur rentHashMap的key和value均不可以为null
3.HashTable 和ConcurrentHashMap的区别:保证线程安全的方式不同:
3.1.HashTable是通过给整张散列表加锁的方式来保证线程安全,这种方式保证了线程安全,但是并发执行效率低下。
3.2.ConcurrentHashMap在JDK1.8之前,采用分段锁机制来保证线程安全的,这种方式可以在保证线程安全的同时,一定程度上提高并发执行效率(当多线程并发访问不同的segment时,多线程就是完全并发的,并发执行效率会提高)
3.3.从JDK1.8开始, ConcurrentHashMap数据结构与1.8中的HashMap保持一致,均为数组+链表+红黑树,是通过乐观锁+Synchroni zed来保证线程安全的.当多线程并发向同一个散列桶添加元素时。若散列桶为空,此时触发乐观锁机制,线程会获取到桶中的版本号,在添加节点之前,判断线程中获取的版本号与桶中实际存在的版本号是否一致,若一致,则添加成功,若不一致,则让线程自旋。
若散列桶不为空,此时使用Synchronized来保证线程安全,先访问到的线程会给桶中的头节点加锁,从而保证线程安全。
是线程安全的
2.HashMap的key 和value均可以为null;而HashTable和Concur rentHashMap的key和value均不可以为null
3.HashTable 和ConcurrentHashMap的区别:保证线程安全的方式不同:
3.1.HashTable是通过给整张散列表加锁的方式来保证线程安全,这种方式保证了线程安全,但是并发执行效率低下。
3.2.ConcurrentHashMap在JDK1.8之前,采用分段锁机制来保证线程安全的,这种方式可以在保证线程安全的同时,一定程度上提高并发执行效率(当多线程并发访问不同的segment时,多线程就是完全并发的,并发执行效率会提高)
3.3.从JDK1.8开始, ConcurrentHashMap数据结构与1.8中的HashMap保持一致,均为数组+链表+红黑树,是通过乐观锁+Synchroni zed来保证线程安全的.当多线程并发向同一个散列桶添加元素时。若散列桶为空,此时触发乐观锁机制,线程会获取到桶中的版本号,在添加节点之前,判断线程中获取的版本号与桶中实际存在的版本号是否一致,若一致,则添加成功,若不一致,则让线程自旋。
若散列桶不为空,此时使用Synchronized来保证线程安全,先访问到的线程会给桶中的头节点加锁,从而保证线程安全。
HashTable
数组+链表
线程安全
通过synchronized实现
初始容量:11,负载因子:0.75,扩容:2n+1
Hashtable 是不允许键或值为 null 的
TreeMap
有序的key-value集合,通过红黑树实现
key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的
Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常
Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常
LinkHashMap
继承了HashMap
LinkedHashMap通过在HashMap的基础上增加一条双向链表,实现了插入顺序和访问顺序一致
线程不安全
LinkedHashMap支持两种缓存策略,FIFO(先进先出,淘汰最早被缓存的对象)和LRU(淘汰最长时间未被使用的数据,以时间作为参考)。accessOrder默认为false,就是FIFO,设置为true时就是LRU。
并发
进程和线程
进程
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
进程就可以视为程序的一个实例,大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器
等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
线程
一个进程之内可以分为一到多个线程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作
为线程的容器
为线程的容器
区别
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
进程拥有共享的资源,如内存空间等,供其内部的线程共享
进程间通信较为复杂,线程通信相对简单,因为它们共享进程内的内存
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
并行和并发
并发(concurrent)是同一时间应对多件事情的能力,单核 cpu 下,线程实际还是串行执行的,任务调度器将 cpu 的时间片分给不同的程序使用,只是由于 cpu 在线程间的切换非常快,人类感觉是同时运行的。
并行(parallel)是同一时间动手做多件事情的能力,多核 cpu下,每个核都可以调度运行线程,这时候线程可以是并行的。
创建和运行线程
直接使用 Thread
使用 Runnable 配合 Thread,Thread 代表线程,Runnable 可运行的任务
用 Runnable 更容易与线程池等高级 API 配合,用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
用 Runnable 更容易与线程池等高级 API 配合,用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
线程上下文切换
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
线程的 cpu 时间片用完
垃圾回收
有更高优先级的线程需要运行
线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
常用方法
start 与 run
直接调用 run 是在主线程中执行了 run,没有启动新的线程
使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
sleep和yield区别
sleep()方法暂停当前线程后,会给其他线程执行机会,不区分其他线程的优先级;但yield()方法只会给优先级相同,或优先级更高的线程执行机会
sleep()方法会将线程转入阻塞状态,而yield()强制当前线程进入就绪状态。
sleep()方法声明抛出InterruptedException异常,而yield()方法没有声明抛出任何异常
Thread.sleep()
使当前所在线程进入阻塞
只是让出CPU,并没有释放锁
子主题
Object.wait()
让出CPU,释放对象锁
在调用前需要先拥有对象锁,所以一般在synchronized中同步块使用
使该线程进入该对象的监视器的等待队列
Thread.join()
join=synchronized+Object.wait()
线程合并,调用线程会进入阻塞状态,需要等待被调用线程结束后才可以执行
interrupt()
如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记(打断标记设为false)
如果打断的正在运行的线程,则会设置打断标记 (打断标记为true)
park 的线程被打断,也会设置打断标记(打断标记为true)
如果打断的正在运行的线程,则会设置打断标记 (打断标记为true)
park 的线程被打断,也会设置打断标记(打断标记为true)
isInterrupted()
判断当前线程是否被打断
判断之后不会清除打断标记,打断标记不变
是一个实例方法
interrupted()
判断当前线程是否被打断
判断之后会清除打断标记,将打断标记改为false
是一个是静态方法
LockSupport.park()
阻塞当前线程
如果中断状态为true,那么park无法阻塞
守护线程
只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束
t1.setDaemon(true);
垃圾回收线程就是典型的守护线程
线程状态
五种状态
新建状态(New):新创建了一个线程对象。
就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权
运行状态(Running):就绪状态的线程获取了CPU,执行程序代码
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中
其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时, 或者I/O处理完毕时,线程重新转入就绪状态
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期
六种状态
New 新建状态(线程刚被创建,start方法之前的状态)
Runnable 运行状态(得到时间片运行中状态)(Ready就绪,未得到时间片就绪状态)
Blocked 阻塞状态(如果遇到锁,线程就会变为阻塞状态等待另一个线程释放锁)
Waiting 等待状态(无限期等待)
Time_Waiting 超时等待状态(有明确结束时间的等待状态)
Terminated 终止状态(当线程结束完成之后就会变成此状态)
并发之共享模型
共享模型之管程
临界区
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
为了避免临界区的竞态条件发生
阻塞式的解决方案:synchronized,Lock
非阻塞式的解决方案:原子变量
互斥和同步
互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法控制对资源的访问顺序
同步是指在互斥的基础上实现对资源的有序访问
synchronized
synchronized即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断
注:原子性操作是指这些操作是不可中断的,要做一定做完,要么就没有执行
用法
对象锁:包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)
类锁:指synchronized修饰静态的方法或指定锁为Class对象
成员变量和静态变量是否线程安全?
1、如果它们没有被共享,则线程安全。2、如果它们被共享了,根据它们的状态是否能够改变,⼜分两种情况:(1)如果只有读操作,则线程安全
如果有读写操作,需要考虑线程安全问题(读时可能会读到中间结果,所以有读写时,读也要考虑线程安全
如果有读写操作,需要考虑线程安全问题(读时可能会读到中间结果,所以有读写时,读也要考虑线程安全
局部变量是否线程安全?
1、基本数据类型的局部变量是线程安全的。
2、引用类型的局部变量,要看该对象有没有逃离方法的作用范围,
(1)如果对象仅在方法内创建、使用、消亡,则是线程安全的;
(2)如果一个对象由外部传入,或者传出外部,则需要考虑线程安全问题〈外部仅读,线程安全;外部有读写--如果不考虑同步机
制的话,会存在线程安全问题)
2、引用类型的局部变量,要看该对象有没有逃离方法的作用范围,
(1)如果对象仅在方法内创建、使用、消亡,则是线程安全的;
(2)如果一个对象由外部传入,或者传出外部,则需要考虑线程安全问题〈外部仅读,线程安全;外部有读写--如果不考虑同步机
制的话,会存在线程安全问题)
常见线程安全类
String
包装类
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类
包装类
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类
多个线程调用它们同一个实例的某个方法时,是线程安全的
它们的每个方法是原子的
但注意它们多个方法的组合不是原子的
synchronized原理
对象
对象头
Mark Word(标记字段)
Mark Word用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。
Class Pointer(类型指针)
Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
若为数组对象,还应有记录数组长度的数据
实例数据
存放类的属性数据信息,包括父类的属性信息;
对齐填充(非必须)
由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
偏向锁
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
首先获取锁 对象的 Markword,判断是否处于可偏向状态。(biased_lock=1、且 ThreadId 为空)
如果是可偏向状态,则通过 CAS 操作,把当前线程的 ID 写入到 MarkWord
a) 如果 cas 成功,那么 markword 就会变成这样。 表示已经获得了锁对象的偏向锁,接着执行同步代码块
b) 如果 cas 失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行
a) 如果 cas 成功,那么 markword 就会变成这样。 表示已经获得了锁对象的偏向锁,接着执行同步代码块
b) 如果 cas 失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行
如果是已偏向状态,需要检查 markword 中存储的ThreadID 是否等于当前线程的 ThreadID
a) 如果相等,不需要再次获得锁,可直接执行同步代码块
b) 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁
a) 如果相等,不需要再次获得锁,可直接执行同步代码块
b) 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
撤销
调用对象 hashCode
调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被
撤销
轻量级锁会在锁记录中记录 hashCode
重量级锁会在 Monitor 中记录 hashCode
其它线程使用对象
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
调用 wait/notify
1、偏向锁的撤销需要等待全局安全点,到达全局安全点后,持有偏向锁的线程B也被暂停了。
2、检查持有偏向锁的线程B的状态(会遍历当前JVM的所有线程,如果能找到线程B,则说明偏向的线程B还存活着):
如果线程还存活,则检查线程是否还在执行同步代码块中的代码:
如果是,则把该偏向锁升级为轻量级锁,且原持有偏向锁的线程B继续获得该轻量级锁。
如果线程未存活,或线程未在执行同步代码块中的代码,则进行校验是否允许重偏向:
如果不允许重偏向,则将Mark Word设置为无锁状态(未锁定不可偏向状态),然后升级为轻量级锁,进行CAS竞争锁。
如果允许重偏向,设置为匿名偏向锁状态(即线程B释放偏向锁)。当唤醒线程后,进行CAS将偏向锁重新指向线程A(在对象头和线程栈帧的锁记录中存储当前线程ID)。
3、唤醒暂停的线程,从安全点继续执行代码
2、检查持有偏向锁的线程B的状态(会遍历当前JVM的所有线程,如果能找到线程B,则说明偏向的线程B还存活着):
如果线程还存活,则检查线程是否还在执行同步代码块中的代码:
如果是,则把该偏向锁升级为轻量级锁,且原持有偏向锁的线程B继续获得该轻量级锁。
如果线程未存活,或线程未在执行同步代码块中的代码,则进行校验是否允许重偏向:
如果不允许重偏向,则将Mark Word设置为无锁状态(未锁定不可偏向状态),然后升级为轻量级锁,进行CAS竞争锁。
如果允许重偏向,设置为匿名偏向锁状态(即线程B释放偏向锁)。当唤醒线程后,进行CAS将偏向锁重新指向线程A(在对象头和线程栈帧的锁记录中存储当前线程ID)。
3、唤醒暂停的线程,从安全点继续执行代码
批量重偏向
当撤销偏向锁阈值超过 20 次后,于是会在给这些对象加锁时重新偏向至加锁线程
批量撤销
当撤销偏向锁阈值超过 40 次后,整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
轻量级锁
轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。
创建锁记录(Lock Record)每个线程的栈帧都会包含一个锁记录的结构
让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存
入锁记录
如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁
如果 cas 失败,有两种情况
如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重
入计数减一
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
成功,则解锁成功
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
成功,则解锁成功
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
假设当前Object对象,已经被Thread1所持有,当Thread2前来竞争这把锁,满足上述条件后,会发生锁膨胀
此时Thread2来获取轻量级锁肯定失败的,所以会进入锁膨胀的流程
1)为Object对象申请Monitor锁,Object的Mark Word指向Monitor地址;Monitor的Owner指向Thread1的锁记录。
2)Thread2进入Monitor的EntryList当中,状态变成BLOCKED。
3)当Thread1执行完代码块的内容后,开始释放锁,使用CAS去重置Object的Mark Word,此时会失败。因为当前对象头存储的是Monitor的地址。
4)会进入重量级锁的解锁过程。将Monitor的Owner设置为null,同时唤醒EntryList中的Thread-2。
重量级锁
重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的
Mark Word 中就被设置指向 Monitor 对象的指针
自旋优化
当发生锁膨胀后,没持有锁的线程会进入Monitor的EntryList当中进行阻塞,实际情况是,会通过自适应自旋锁进行一定次数的自旋,如果获取到锁了,就避免进入阻塞状态(会进行上下文切换)。如果没获取到锁,此时在进入阻塞状态。
自旋是占用CPU的,只有在多核CPU中才能发挥优势。
在 Java 6 之后自旋锁是自适应的,自适应自旋锁会动态调整自旋次数,获取锁成功的次数多,就会多自旋几次;如果一次都没有成功,则会可能会直接进行阻塞。
Java 7 之后不能控制是否开启自旋功能
收藏
收藏
0 条评论
下一页