volatile 原理

  1. 对 volatile 变量的写指令后会加入写屏障
    1. 写屏障(sfence)保证在这个屏障之前的,对共享变量的改动,都同步到主存当中(可见性)
    2. 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后(有序性)
  2. 对 volatile 变量的读指令前会加入读屏障
    1. 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据(可见性)
    2. 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前(有序性)

monitor原理

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。
image.png
加锁流程:

  1. 刚开始monitor 的owner是null,这时候没人持有锁
  2. 当有线程执行synchronized(obj) ,首先去obj的mark down找到monitor对象,解这就会将 Monitor 的所有者 Owner 从null置为自己。
  3. 有其他线程过来之后执行相同的操作但是发现monitor对象的owner不是null,这时候他们就会进入阻塞队列
  4. 当前owner线程执行完之后,其他阻塞线程看到owner为空了,就会进行非公平的竞争,直到有一个人获取了锁
  5. monitor还有一个waitset,里面是已经获得了锁但是被wait了,会被放到waitset里面,等着别人来notify

synchronized底层原理
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。执行到monitorenter 时候要查看当前计数器值,为0才能继续执行,同时执行完monitorexit 要把计数器归零

轻量级锁原理

轻量级锁的使用场景:
如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化

加锁流程

  1. 每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的mark word
  2. 加锁的时候,让锁记录中的object reference指向锁对象,并尝试用cas替换object的mark word
    1. 如果cas替换成功,对象头中就存储了锁记录地址和状态00,就表示该对象代表的锁被 锁记录地址所在的线程持有了
    2. cas失败有两种情况
      1. 如果其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
      2. 发现原来就是自己持有了这个锁,那就再加一条锁记录,做为重入的计数

释放锁流程

  1. 释放锁时发现有取值为null的锁记录,表示有重入,删除这条锁记录,表示重入计数减一
  2. 释放锁发现取值不为null的锁记录,就使用cas将mark word的值恢复给对象头
    1. 成功,解锁成功
    2. 失败,说明轻量级锁已经进行了锁膨胀变成了重量级锁,进入重量级锁的解锁流程

锁膨胀

  1. 将轻量级锁膨胀为重量级锁
    1. 轻量级锁产生竞争后,后来的线程会将该所进行锁膨胀
      1. 给该对象申请monitor对象,设置monitor的owner为持有锁的线程
    2. 锁膨胀后解锁,cas失败后会查看monitor,将owner设为null,并唤醒blockentry的线程
  2. 将偏向锁膨胀为轻量级锁

锁自旋优化

自选就是在那while(true)循环一会,如果自旋的时候突然有人让出锁了,自己就可以上位了,从而避免上下文切换

自适应自旋,如果前面有自旋成功的案例比较多,就判定当前情况适合自旋,那就自旋时间长一点,否则就自旋时间短一点

偏向锁

轻量级锁在没有竞争,也就是只有自己持有并且申请持有锁的时候,重入还需要执行cas操作。是对资源的一种浪费
偏向锁进一步优化,只有第一次使用cas将线程id设置到对象的mark word投,下次申请锁的时候发现这个线程id是自己就表示没有竞争,不用重新cas
偏向锁开启之后对象头会有变化,因为要存threadid,所以会把hashcode挤掉。

什么时候偏向锁被撤销

  1. 1. 加锁对象调用了hashcode
  2. 1. 轻量级锁会在锁记录中记录 hashCode
  3. 1. 重量级锁会在 Monitor 中记录 hashCode
  4. 2. 其他线程锁竞争--->锁膨胀
  5. 2. 调用了wait/notify--->因为只有重量级锁有waitnotify,用了这两个会把原来的锁升级为重量级锁
  6. 2. 批量重偏向:锁没有竞争,但thradid总是改,且总是改向另外一个线程,这时会重偏向到另外一个线程
  7. 2. 批量撤销:撤销偏向次数很多之后,就不加偏向锁了。

锁消除

虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。

锁粗化

我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小。只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。
比如我把while循环里面的内容整体加锁,这时候锁粗化会帮我把锁加在while循环外面,省得来回加锁解锁了。

join():

调用者轮询检查对应线程状态,当对应线程执行完了,调用者线程才会继续执行。
image.png
image.png

park、unpark

他们是locksupport类里面的方法。
每个线程都有自己的parker对象,park对象保存了一个counter变量,如果在线程里面调用了park方法,会给counter - 1,然后检查一下counter是否为0

  1. 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加锁过程:

  1. 当前线程用cas操作将state从0置为1,并将当前加锁线程设置为自己。
  2. 当另一个线程请求该资源时,想通过cas操作将state从0置为1时,发现state已经是1了,所以就执行失败
  3. 执行失败后在检查当前加锁的线程是不是自己
    1. 当前加锁是自己没关系
    2. 不是自己,就把自己放到clh队列中等待

释放锁流程:将state - 1,如果state为0,表示彻底释放锁,并且将加锁线程置为null。
clh队列:
本质就是一个双向链表,该链表包含的node节点共有5条属性:分别是waitStatus 、prev、next、thread、
nextWaiter

waitStatus :表示当前节点的等待状态标志位,表示该节点当前处于何种状态

  1. 值为1,表示同步队列中等待的线程等待超时或被中断,应当从同步队列取消,被取消的节点不会参与到竞争中。他会一直保持取消状态不转变为其他状态。
  2. 值为-1,处于唤醒状态,只要前继结点释放锁,就会通知标识为-1状态的后继结点的线程执行。
  3. 值为-2,节点在等待队列中,节点线程等待在condition上,只有其他线程对condition调用了signal()后,该节点将会从等待队列中转移到同步队列中,才有可能获取同步状态
  4. 值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
  5. 值为0,代表处于初始化状态

prev、next:前驱和后继节点
thread:保存了当前线程信息
nextWaiter:标记了当前锁处于什么状态:该节点唤醒后依据该节点的状态判断是否依据条件唤醒下一个节点,包括共享模式,独占模式,其他非空值

  1. 共享模式:直接唤醒下一个节点
  2. 独占模式:等待当前线程执行完成后再唤醒
  3. 其他非空值:依据条件决定怎么唤醒下一个线程

以ReentrantLock 加锁流程说明阻塞队列如何运行的:

  1. 一开始thread0获取了锁,将state用cas置为了1,并将owner置为了自己
  2. thread1来了之后,尝试将state由0改为1,失败
  3. 再次尝试还是失败
  4. 将自己放入队列,因为是首次,所以要新建一个clh队列,他是首个线程,所以要先创建一个哨兵节点,用来占位不关联线程
  5. 一开始哨兵和thread1的状态都是0,初始化状态,thread1一看自己是第一个节点,离获取到锁只有一步之遥,还会再尝试获取锁,也获取失败
  6. 这时哨兵的状态改为了-1,目的就是提醒thread1,不应该再争抢锁了,我状态是-1了,一旦有锁释放的消息,我就提醒你,让你来抢
  7. 所以thread1 park了自己,进入了阻塞状态
  8. 后面再来的线程也都陆续park了自己
  9. 直到thread0释放了锁,这时state变成了0,哨兵提醒thread1赶紧去抢,unpark了thread1
  10. thread1去获取锁,但这时候如果正好有其他线程来和他一起抢,他有可能抢到,也有可能抢不到,如果抢不到就重新进入park阻塞状态

设计得话就用interrupt和lock还有unlock

重入锁,比如可以对一个ReentrantLock对象进行多次lock()和unlock()操作,也就相当于可以对一个锁加多次。本质上就是把state进行累加。