synchronized

  • jdk早期的synchronized为什么是重量级的

    因为我们的jvm是运行在操作系统上的,即用户态,而我们的操作系统是运行在内核态的,用户态中的程序是不能直接调用系统指令的,所以synchronized必须经过老大操作系统,所以synchronized是重量级锁

  • synchronized的升级过程

锁升级

  • 偏向锁:

    引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

  • 偏向锁的获取:

    (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

    (2)当第一次第一个线程来的时候将Mark Word中线程ID设置为当前线程ID

  • 偏向锁的释放

    偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

  • 偏向锁的使用

    偏向锁 - markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用, 只需要判断线程指针是否同一个,所以,偏向 锁, 偏向加锁的第一个线程 。hashCode备份在线程栈上 线程销毁,锁降级为无锁

    有争用 - 锁升级为轻量级锁 - 每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁

  • 匿名偏向锁

    默认情况 偏向锁有个时延,默认是4秒

    打开偏向锁刚 new 出来的对象就是匿名偏向锁

  • 注:

    在 JDK 15 中,默认情况下禁用偏向锁(Biased Locking),并弃用所有相关的命令行选项。

  • 自旋锁

    自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

    自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

  • 什么情况下升级为重量级锁

    锁的时间长,或者自旋线程多

  • new –> 偏向锁 –> 轻量级锁 (无锁, 自旋锁,自适应自旋)–> 重量级锁

    1. Object o = new Object()
      锁 = 0 01 无锁态
      注意:如果偏向锁打开,默认是匿名偏向状态,不偏向任何线程

    2. o.hashCode()
      001 + hashcode

      1
      2
      00000001 10101101 00110100 00110110
      01011001 00000000 00000000 00000000

      little endian big endian

      00000000 00000000 00000000 01011001 00110110 00110100 10101101 00000000

    3. 默认synchronized(o)
      00 -> 轻量级锁
      默认情况 偏向锁有个时延,默认是4秒
      why? 因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。

      1
      -XX:BiasedLockingStartupDelay=0
    4. 如果设定上述参数
      new Object () - > 101 偏向锁 ->线程ID为0 -> Anonymous BiasedLock
      打开偏向锁,new出来的对象,默认就是一个可偏向匿名对象101

    5. 如果有线程上锁
      上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程
      偏向锁不可重偏向 批量偏向 批量撤销

    6. 如果有线程竞争
      撤销偏向锁,升级轻量级锁
      线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁

    7. 如果竞争加剧
      竞争加剧:有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半, 1.6之后,加入自适应自旋 Adapative Self Spinning , JVM自己控制
      升级重量级锁:-> 向操作系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间

    (以上实验环境是JDK11,打开就是偏向锁,而JDK8默认对象头是无锁)

    偏向锁默认是打开的,但是有一个时延,如果要观察到偏向锁,应该设定参数

    如果计算过对象的hashCode,则对象无法进入偏向状态!

    轻量级锁重量级锁的hashCode存在与什么地方?

    答案:线程栈中,轻量级锁的LR中,或是代表重量级锁的ObjectMonitor的成员中

    为什么有自旋锁还需要重量级锁?

    自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗

    重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源

    偏向锁是否一定比自旋锁效率高?

    不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁

    JVM启动过程,会有很多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开

  • 锁重入

    sychronized是可重入锁

    重入次数必须记录,因为要解锁几次必须得对应

    偏向锁 自旋锁 -> 线程栈 -> LR + 1

  • 锁消除 lock eliminate

1
2
3
4
public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}

我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。

  • 锁粗化 lock coarsening
1
2
3
4
5
6
7
8
9
10
public String test(String str){

int i = 0;
StringBuffer sb = new StringBuffer():
while(i < 100){
sb.append(str);
i++;
}
return sb.toString():
}

JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。

  • 锁降级

https://www.zhihu.com/question/63859501

  • 超线程

一个ALU + 两组Registers + PC

参考:
https://cloud.tencent.com/developer/article/1480590

https://cloud.tencent.com/developer/article/1480590
https://cloud.tencent.com/developer/article/1484167

https://cloud.tencent.com/developer/article/1485795

https://cloud.tencent.com/developer/article/1482500