volatile 原理
- 对 volatile 变量的写指令后会加入写屏障
- 写屏障(sfence)保证在这个屏障之前的,对共享变量的改动,都同步到主存当中(可见性)
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后(有序性)
- 对 volatile 变量的读指令前会加入读屏障
- 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据(可见性)
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前(有序性)
monitor原理
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。
加锁流程:
- 刚开始monitor 的owner是null,这时候没人持有锁
- 当有线程执行synchronized(obj) ,首先去obj的mark down找到monitor对象,解这就会将 Monitor 的所有者 Owner 从null置为自己。
- 有其他线程过来之后执行相同的操作但是发现monitor对象的owner不是null,这时候他们就会进入阻塞队列
- 当前owner线程执行完之后,其他阻塞线程看到owner为空了,就会进行非公平的竞争,直到有一个人获取了锁
- monitor还有一个waitset,里面是已经获得了锁但是被wait了,会被放到waitset里面,等着别人来notify
synchronized底层原理
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。执行到monitorenter 时候要查看当前计数器值,为0才能继续执行,同时执行完monitorexit 要把计数器归零
轻量级锁原理
轻量级锁的使用场景:
如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化
加锁流程
- 每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的mark word
- 加锁的时候,让锁记录中的object reference指向锁对象,并尝试用cas替换object的mark word
- 如果cas替换成功,对象头中就存储了锁记录地址和状态00,就表示该对象代表的锁被 锁记录地址所在的线程持有了
- cas失败有两种情况
- 如果其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 发现原来就是自己持有了这个锁,那就再加一条锁记录,做为重入的计数
释放锁流程
- 释放锁时发现有取值为null的锁记录,表示有重入,删除这条锁记录,表示重入计数减一
- 释放锁发现取值不为null的锁记录,就使用cas将mark word的值恢复给对象头
- 成功,解锁成功
- 失败,说明轻量级锁已经进行了锁膨胀变成了重量级锁,进入重量级锁的解锁流程
锁膨胀
- 将轻量级锁膨胀为重量级锁
- 轻量级锁产生竞争后,后来的线程会将该所进行锁膨胀
- 给该对象申请monitor对象,设置monitor的owner为持有锁的线程
- 锁膨胀后解锁,cas失败后会查看monitor,将owner设为null,并唤醒blockentry的线程
- 轻量级锁产生竞争后,后来的线程会将该所进行锁膨胀
- 将偏向锁膨胀为轻量级锁
锁自旋优化
自选就是在那while(true)循环一会,如果自旋的时候突然有人让出锁了,自己就可以上位了,从而避免上下文切换
自适应自旋,如果前面有自旋成功的案例比较多,就判定当前情况适合自旋,那就自旋时间长一点,否则就自旋时间短一点
偏向锁
轻量级锁在没有竞争,也就是只有自己持有并且申请持有锁的时候,重入还需要执行cas操作。是对资源的一种浪费
偏向锁进一步优化,只有第一次使用cas将线程id设置到对象的mark word投,下次申请锁的时候发现这个线程id是自己就表示没有竞争,不用重新cas
偏向锁开启之后对象头会有变化,因为要存threadid,所以会把hashcode挤掉。
什么时候偏向锁被撤销
1. 加锁对象调用了hashcode
1. 轻量级锁会在锁记录中记录 hashCode
1. 重量级锁会在 Monitor 中记录 hashCode
2. 其他线程锁竞争--->锁膨胀
2. 调用了wait/notify--->因为只有重量级锁有wait和notify,用了这两个会把原来的锁升级为重量级锁
2. 批量重偏向:锁没有竞争,但thradid总是改,且总是改向另外一个线程,这时会重偏向到另外一个线程
2. 批量撤销:撤销偏向次数很多之后,就不加偏向锁了。
锁消除
虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
锁粗化
我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小。只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。
比如我把while循环里面的内容整体加锁,这时候锁粗化会帮我把锁加在while循环外面,省得来回加锁解锁了。
join():
调用者轮询检查对应线程状态,当对应线程执行完了,调用者线程才会继续执行。
park、unpark
他们是locksupport类里面的方法。
每个线程都有自己的parker对象,park对象保存了一个counter变量,如果在线程里面调用了park方法,会给counter - 1,然后检查一下counter是否为0
- counter 为0当前线程就park住,否则就没问题
如果调用了unpark()方法,会给counter + 1,如果当前线程是被park住的,那就唤醒让他继续前进,如果当前线程正在运行,就是正常给counter + 1
aqs原理
aqs是一个抽象类,很多类使用aqs框架来实现。我理解他是一种框架也是一种思想。
1. Exclusive(独占):只有一个线程能执行,如:ReentrantLock,又可分为公平锁和非公平锁。
2. Share(共享):多个线程可同时执行,如:CountDownLatch、Semaphore、 CyclicBarrier、ReadWriteLock。
AQS的核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并
且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤
醒时锁分配的机制,这个机制AQS使用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
aqs加锁过程:
- 当前线程用cas操作将state从0置为1,并将当前加锁线程设置为自己。
- 当另一个线程请求该资源时,想通过cas操作将state从0置为1时,发现state已经是1了,所以就执行失败
- 执行失败后在检查当前加锁的线程是不是自己
- 当前加锁是自己没关系
- 不是自己,就把自己放到clh队列中等待
释放锁流程:将state - 1,如果state为0,表示彻底释放锁,并且将加锁线程置为null。
clh队列:
本质就是一个双向链表,该链表包含的node节点共有5条属性:分别是waitStatus 、prev、next、thread、
nextWaiter
。
waitStatus :表示当前节点的等待状态标志位,表示该节点当前处于何种状态
- 值为1,表示同步队列中等待的线程等待超时或被中断,应当从同步队列取消,被取消的节点不会参与到竞争中。他会一直保持取消状态不转变为其他状态。
- 值为-1,处于唤醒状态,只要前继结点释放锁,就会通知标识为-1状态的后继结点的线程执行。
- 值为-2,节点在等待队列中,节点线程等待在condition上,只有其他线程对condition调用了signal()后,该节点将会从等待队列中转移到同步队列中,才有可能获取同步状态
- 值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
- 值为0,代表处于初始化状态
prev、next:前驱和后继节点
thread:保存了当前线程信息
nextWaiter:标记了当前锁处于什么状态:该节点唤醒后依据该节点的状态判断是否依据条件唤醒下一个节点,包括共享模式,独占模式,其他非空值
- 共享模式:直接唤醒下一个节点
- 独占模式:等待当前线程执行完成后再唤醒
- 其他非空值:依据条件决定怎么唤醒下一个线程
以ReentrantLock 加锁流程说明阻塞队列如何运行的:
- 一开始thread0获取了锁,将state用cas置为了1,并将owner置为了自己
- thread1来了之后,尝试将state由0改为1,失败
- 再次尝试还是失败
- 将自己放入队列,因为是首次,所以要新建一个clh队列,他是首个线程,所以要先创建一个哨兵节点,用来占位不关联线程
- 一开始哨兵和thread1的状态都是0,初始化状态,thread1一看自己是第一个节点,离获取到锁只有一步之遥,还会再尝试获取锁,也获取失败
- 这时哨兵的状态改为了-1,目的就是提醒thread1,不应该再争抢锁了,我状态是-1了,一旦有锁释放的消息,我就提醒你,让你来抢
- 所以thread1 park了自己,进入了阻塞状态
- 后面再来的线程也都陆续park了自己
- 直到thread0释放了锁,这时state变成了0,哨兵提醒thread1赶紧去抢,unpark了thread1
- thread1去获取锁,但这时候如果正好有其他线程来和他一起抢,他有可能抢到,也有可能抢不到,如果抢不到就重新进入park阻塞状态
设计得话就用interrupt和lock还有unlock
重入锁,比如可以对一个ReentrantLock对象进行多次lock()和unlock()操作,也就相当于可以对一个锁加多次。本质上就是把state进行累加。