个人学习记录,如有错误,希望大佬指出纠正
1. 锁的分类
从 并发 的宏观角度来讲分为乐观锁和悲观锁。
乐观锁:
读多写少,认为每次修改数据的时候其他线程恰好不会修改数据。因此在取数据的时候会先获取一个版本号但不加锁,其他线程可以直接读取数据,但在最终要修改数据的时候会加锁或执行原子操作比较当前时刻的版本号与之前获取的版本号是否一致。若一致直接更新否则说明数据已被其他线程修改,本次数据更新失败需要进行重试。适用于读多写少场景。很多乐观锁都是通过CAS实现。
悲观锁:
与乐观锁恰好相反,认为每次修改数据的同时会有其他线程来修改该数据,所以每次取数据的时候加锁,其他线程来操作数据会被阻塞,指导本次数据修改完成释放锁,其他线程才能操作该条数据。例如synchronized关键字。
2.线程阻塞
- java的线程是映射在操作系统原生线程之上的,因此唤醒或阻塞需要操作系统的介入,需要在用户态和内核态之间做切换。用户态和内核态是操作系统的两种运行级别。
当在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成一些用户态自己没有特权和能力完成的操作时就会切换到内核态。 - 当一个进程在执行用户自己的代码时处于用户运行态(用户态),此时特权级最低,为3级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态。Ring3状态不能访问Ring0的地址空间,包括代码和数据;当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),此时特权级最高,为0级。执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈。
- 高频率的用户态、内核态切换会导致消耗大量系统资源
- 简单且执行速度快的同步代码获取锁和阻塞挂起消耗的时间比代码执行的时间还要长是不合适的。
- synchronized会使竞争锁失败的线程进入阻塞状态,属于重量级锁,在jdk1.5以后,引入偏向锁、轻量级锁并默认启用自旋锁,它们属于乐观锁。
3.markword
-
在java虚拟机内存中对象是由对象头、实例数据、对齐填充组成。其中,可以简单的理解为java对象头是由class类指针和markword组成(数组对象多了一个数组长度)。
-
markword的长度在64位虚拟机和32位虚拟机中长度分别是64bit和32bit,最后2bit是存储锁状态位的,他决定了markword的存储内容。如果是无锁或偏向锁状态,会有1bit存储偏向是否偏向锁。
-
synchronized重量级锁属于悲观锁的一种,偏向锁、轻量级锁、自旋锁都属于乐观锁,根据场景不同适用不用的锁。
java的几种锁
- synchronized 重量级锁,可以使用任何非null对象作为锁。可以用在类的静态上、方法上、代码块上。放在静态方法上是类的全局锁锁的是类字节码文件对该类所有实例起作用。放在普通方法上锁的是当前的this对象,仅对当前实例起作用。放在代码块上如果参数是对象,仅对当前实例起作用,如果参数是类字节码文件,对该类所有实例起作用。
- Lock 轻量级锁,需要手动加锁、解锁
- 关于synchronized 和 Lock,点击这里
自旋锁
- 如果正在持有锁的线程可以在很短的时间内执行完成释放锁,那么当前等待竞争锁的线程可以不必要去进行用户态和内核态的切换,进入挂起阻塞的状态,而是通过不断地进行自旋尝试,直至竞争到锁资源,减少系统资源的消耗。
- 自旋锁可以减少线程的阻塞挂起,这对于锁资源竞争不高且加锁资源能快速执行释放锁资源的代码块来说可以大幅度提升性能,因为短时间内的自旋操作对cpu的资源消耗要低于挂起阻塞再次唤醒的操作,因为后者会发生两次上下文切换。但是如果锁资源竞争激烈,代码运行占用锁资源时间长,那么自旋对CPU的消耗会大于线程阻塞再唤醒额操作,自旋会长时间占用CPU自旋,但是却执行不了任何任务,白白耗费CPU资源。
- jdk1.6中引入了自适应自旋锁,对旧版自旋锁长时间自旋占用系统资源做了一定的优化。自适应意味着自旋时间不再固定,而是根据前一次同一个锁的自旋时间和持有状态来进行调整,通常认为一次切换上下文的时间是一个最佳时间。
- 在JDK1.6中,Java虚拟机提供-XX:+UseSpinning参数来开启自旋锁,使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。在JDK1.7开始,自旋锁的参数被取消,虚拟机不再支持由用户配置自旋锁,自旋锁总是会执行,自旋锁次数也由虚拟机自动调整。
偏向锁
- 偏向锁是为了在无锁资源竞争的情况下尽量减少不必要的获取释放轻量级锁的执行路径。偏向锁的初始化只需要一次CAS操作,将markword中初始化偏向锁标识。只有当前线程运行中时遭遇其他线程竞争抢占锁资源,才会将偏向锁挂起,JVM会消除偏向锁,转换为轻量级锁。
-
获取偏向锁
- 第一步确认当前锁标识位是否为01,偏向锁标识是否为1。
- if如果是无锁状态,如果不支持偏向锁,获取轻量级锁。否则一次CAS操作将线程ID放入mark word中,当前线程成为偏向所有者。如果CAS成功,走第7,否则走第3.
- else if检测当前mark word中的偏向线程ID是否是当前线程的ID并且epoch等于当前class的epoch,如果是走第7(一个线程在执行完同步代码块以后, 并不会尝试将 Mark Word 中的 thread ID 赋回原值 。如果该线程需要再次加锁时, 会发现之前已经获得偏向锁, 无须修改对象头的任何内容, 最小化开销。),不是走第4。
- else if如果epoch不相同,需要重新偏向,利用CAS指令将锁对象中的mark word替换为一个偏向当前线程且epoch为class类的epoch的新的mark word。CAS失败,锁升级。
- else 偏向其他线程或匿名偏向(没有偏向任何线程);CAS将偏向线程改为当前线程,如果当前是匿名偏向则能修改成功,否则进入锁升级的逻辑6。
- 竞争失败说明存在多个线程竞争锁资源,到达安全点后挂起持有偏向锁的线程,jvm撤销偏向锁。撤销偏向锁需要等待全局安全点(此时间点,没有线程在执行字节码)
- 执行同步代码
- 偏向锁的撤销
- 查看偏向线程是否存活,如果不存活直接撤销偏向锁。JVM维护了一个集合存放所有存活的线程,通过遍历集合即可判断。
- 偏向线程是否退出同步代码块,如果退出,撤销。(偏向锁加锁流程中,每次进入同步代码块会找到内存地址最高的可用Lock Record,为obj字段赋值,指向锁对象,在解锁时会移除该Lock Record,因此可以遍历线程栈中的Lock Record来判断是否退出同步代码块)
- 如果未退出,将偏向线程所有相关的Lock Record的Displaced mark word 设置为null,然后将地址最高的Lock Recode的Displaced mark word设置为无锁状态,然后锁对象头指向地址最高的Lock Record,升级为轻量级锁。(在safe point不需要CAS原子操作)
- 偏向锁的释放其实应该是Lock Record的释放,与此不同的是轻量级锁的释放,是将Displaced Mark Word 替换会锁对象头。
轻量级锁
- 如果锁对象没有开启偏向模式等原因或已经偏向其他线程,这时候会构建一个无锁状态的mark word 设置到线程栈中的Lock Record中,称为Displaced mark word(这是因为在解锁时CAS可以直接将它替换回索对象头)。
- CAS将锁对象头的mark word替换为指向当前Lock Record的指针,CAS成功获取到轻量级锁
- CAS失败,判断是否为锁重入,如果是将当前Lock Record的Displaced mark word设为null,起到锁重入计数(计算Lock Record数)。
- 如果非重入,进行自旋等待,超时会进行锁膨胀升级为重量级锁。
- 轻量级锁的释放,就是将当前线程的Lock Record的Displace mark word进行CAS替换回锁对象头中。
- 如果CAS失败,锁膨胀升级为重量级锁
重量级锁
-
已经是重量级状态,说明膨胀已经完成,直接返回
-
如果是轻量级锁则需要进行膨胀操作
-
如果是膨胀中状态,则进行忙等待
-
如果是无锁状态则需要进行膨胀操作
- 轻量级锁的膨胀
- 先分配一个ObjectMonitor对象,初始化值
- 将锁对象的mark word 设置为膨胀中状态
- 设置monitor对象的header为线程栈中的Displaced mark word
- 设置monitor对象的owner为指向的Lock Record指针
- 设置monitor的obj字段为锁对象
- 将锁对象的对象头设置为重量级锁状态,将锁对象头设置为重量级锁状态,指向monitor对象
- 无锁膨胀为重量级锁
- 先分配一个ObjectMonitor对象,初始化值
- 设置monitor对象的header为mark word
- 设置monitor对象的owner为NULL
- 设置monitor的obj字段为锁对象
- 将锁对象头设置为重量级锁状态,指向monitor对象
- 进入获取锁代码
- owner为null代表无锁状态,CAS成功当前线程直接获取锁。
- 如果是重入,计数器+1。
- 如果是由轻量级锁膨胀得到,当前线程为之前持有轻量级锁的线程,将owner字段的Lock Record替换为当前线程。
- 在调用同步代码前,尝试自旋获取锁,减少系统开销。
- 否则调用系统同步操作获得锁或者阻塞
- 重量级锁获取流程
-
Contention List 和 Entry List:竞争队列,所有请求锁的线程首先被放在Contention List这个竞争队列中,Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
-
Wait Set:调用Object的wait方法被阻塞的线程被放置在这里;
-
OnDeck:在所释放后,会在竞争队列中选择一个线程作为下一个获取锁的继承人,但不会直接将锁交给当前线程,因此它不一定能竞争到锁资源。Synchronized为非公平锁,其他线程可能会在同时竞争到锁资源。竞争不到锁资源的线程会再次进入竞争队列调用park进入阻塞状态。
-
Owner:当前已经获取到所资源的线程被称为Owner;
-
如果线程获取到锁以后,调用wait方法,将会将当前线程加入WaitSet队列中,当被notify方法唤醒后会进入竞争队列。
-
!Owner:当前释放锁的线程。
-
调用Object的wait和notify方法,如果当前锁状态为轻量级锁或偏向锁,会导致锁升级先膨胀为重量级锁。
-