【JUC并发编程】synchronized原理分析(下)(ObjectMonitor 源码解读 Hotspot源码解读 synchronized底层实现原理总结 轻量锁、偏向锁、重量锁原理分析)

【JUC并发编程】synchronized原理分析(下)(ObjectMonitor 源码解读 Hotspot源码解读 synchronized底层实现原理总结 轻量锁、偏向锁、重量锁原理分析),第1张

目录
  • 一、ObjectMonitor 源码解读
    • 1. 锁池
    • 2. 等待池
    • 3. wait与notify原理分析
  • 二、Hotspot源码解读
    • 1. synchronized底层实现原理总结
    • 2. 轻量锁原理分析
    • 3. 偏向锁原理分析
      • 3.1 偏向锁原理
      • 3.2 偏向锁撤销
      • 3.3 批量重偏向
      • 3.4 批量撤销
    • 4. 重量锁原理分析
    • 5. 锁粗化
    • 6. 锁消除
    • 7. JDK15 默认关闭偏向锁优化原因

一、ObjectMonitor 源码解读

Java底层使用 C++ hotspot虚拟机
http://hg.openjdk.java.net/jdk8 下载hotspot虚拟机
Objectmonitor 底层基于C++实现。
Hotspot 源码位置:
hotspot\src\share\vm\runtime\objectMonitor.hpp

ObjectMonitor() {
  _header       = NULL;
  _count        = 0;  // 记录个数
  _waiters      = 0,
  _recursions   = 0;   // 递归次数/重入次数
  _object       = NULL;  // 存储Monitor关联对象
  _owner        = NULL; // 记录当前持有锁的线程ID
  _WaitSet      = NULL;  // 等待池:处于wait状态的线程,会被加入到_WaitSet
  _WaitSetLock  = 0 ;
  _Responsible  = NULL ;
  _succ         = NULL ;
  _cxq          = NULL ; // 多线程竞争锁时的单向链表
   FreeNext      = NULL ;
  _EntryList    = NULL ;  // 锁池:处于等待锁block状态的线程,会被加入到该列表
  _SpinFreq     = 0 ;
  _SpinClock    = 0 ;
  OwnerIsThread = 0 ;
  _previous_owner_tid = 0;
}

1. 锁池

锁池: 假设线程A已经拥有了某个对象的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),
由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,
所以这些线程就进入了该对象的锁池中。

EntryList (锁池) 当前的线程获取锁失败,阻塞 链表数据结构存放

2. 等待池

WaitSet----主动释放锁 阻塞等待-----wait方法 等待池中
等待池: 假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,
这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程
调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。
如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

1.如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
2.当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
3.优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

3. wait与notify原理分析


调用wait方法,即可进入WaitSet变为WAITING状态
BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
BLOCKED线程会在Owner线程释放锁的时候被唤醒
WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争

锁池: 没有获取到锁的线程
等待池:调用wait 方法
相同点: 都会阻塞

等待池的线程被唤醒之后 等待池转移到锁池----从新竞争锁的资源。
Notify()----只会唤醒等待中一个线程
NotifyAll()-----唤醒所有的线程


二、Hotspot源码解读 1. synchronized底层实现原理总结
  1. Synchronized 偏向锁(101)、轻量锁(000)、重量级(010)

  2. Synchronized 锁的升级状态存放在 java对象头中markword中,64位存放

  3. 偏向锁: 当前线程从对象头中markword获取是否是为偏向锁,如果是为偏向锁,则判断线程的id===当前线程id

    • 如果等于当前的线程id,则不会重复的执行CAS *** 作,而是直接进入到
      我们的同步代码快
    • 如果不等于当前的线程id 如果是为无锁的情况,没有其他的线程
      与我竞争的话,直接使用CAS修改markword中锁的标识位状态为101
      同时也存放当前线程的id在markword中。
  4. 其他的线程与偏向锁线程开始竞争,撤销偏向锁次数达到了20次,则后面
    开始直接批量重偏向T2线程(注意事项:没有其他的线程与t2做竞争),如果撤销
    偏向锁次数达到了40次,则后面开始批量撤销

  5. 撤销偏向锁需要在一个全局的安全点 停止我们偏向锁线程,在修改我们markword
    中为轻量级锁,在唤醒偏向锁的线程
    轻量级锁获取锁与释放锁 (用户态中 一直自旋的形式 消耗cpu的资源)

  6. 多个线程同时竞争同一把锁,则升级到轻量锁 使用CAS(修改markword 锁的状态=00)
    如果成功,则与markword 替换 将HashCode值等 直接存放在我们的栈帧中,而当前markword 中存放锁记录地址。

  7. 当我们使用轻量级锁释放锁时,则还原markword 值内容。
    重量级

  8. 当前我们的线程重试了多次还是没有获取到锁,则当前锁会升级为重量级锁,

  9. 没有获取到锁的线程 会存放在C++Monitor EntryList 集合中 同时当前线程会直接阻塞释放了cpu执行权,在后期唤醒从新进入竞争锁的流程成本是非常高的,因为需要发生cpu上下文切换 用户态到内核切换 改我们对象头中markword 值为C++Monitor 内存地址指针
    Java对象与C++Monitor关联起来。

2. 轻量锁原理分析

引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。

注意:
轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

演示代码:

private static Object objectLock = new Object();
public static void main(String[] args) {
    new Thread(() -> {
        synchronized (objectLock) {
            System.out.println("线程1代码");
        }
    }).start();
    new Thread(() -> {
        synchronized (objectLock) {
            System.out.println("线程2代码");
        }
    }).start();
}

  1. 创建锁记录(Lock Record)对象,每个线程的栈帧(方法)都会包含一个锁记录的结构,内部可以储存锁定关联对象的Mark Word
  2. 锁记录中Object reference (对象引用)指向锁对象,采用CAS算法 替换Object锁对象 的Mark Word,将Mark Word 的值存入锁记录
  3. 如果CAS执行成功,则对象头中存储了锁记录地址和状态00,表示该线程获取到锁

演示:

  1. 如果是其它线程已经持有了该Object对象的轻量级锁,表示多个线程开始竞争,进入锁
    升级过程(膨胀/膨化)
  2. 如果当前线程已经获取到了锁,则在新增一条Lock Record 作为重入次数。
  3. 当退出synchronized代码块(解锁时)Lock Record 地址指向为null,代表锁记录有重入,这时重置记录,表示重入计数减一。
  4. 当退出synchronized代码块(解锁时)Lock Record 地址指向锁值不为null,这时使用cas将Mark Word的值恢复给对象头
    如果成功,则解锁成功

演示代码:

public class Test1000 {
    public synchronized static void main(String[] args) {
        DemoLock demoLock = new DemoLock();
//        //调用hashCode
        System.out.println(Integer.toHexString(demoLock.hashCode()));
        synchronized (demoLock) {
            System.out.println(ClassLayout.parseInstance(demoLock).toPrintable());
        }
        try {
            Thread.sleep(4000);
            System.out.println(ClassLayout.parseInstance(demoLock).toPrintable());
        } catch (Exception e) {

        }
    }

    static class DemoLock {
        int i = 2028; // 4个字节 4+开启指针压缩对象头12个字节
        boolean b = true; //  1个字节   16+1=17
    }
}

3. 偏向锁原理分析

偏向锁在没有竞争时,(就自己这个线程),每次重入仍然执行CAS *** 作.
Java6中引入了偏向锁来做进一步优化;只有第一次使用CAS将线程ID设置到对象的Mark Word,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有。

3.1 偏向锁原理

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS *** 作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则,使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

3.2 偏向锁撤销

由于偏向锁使用了一种直到竞争发生时才会释放的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会去释放锁。

3.3 批量重偏向

批量重偏向:当一个线程创建了大量对象并执行了初始的同步 *** 作,后来另一个线程也来将这些对象作为锁对象进行 *** 作,会导偏向锁重偏向的 *** 作。

批量撤销:在多线程竞争剧烈的情况下,使用偏向锁将会降低效率,于是乎产生了批量撤销机制。

1.启动设置参数:
通过JVM的默认参数值,批量重偏向和批量撤销的阈值。
设置JVM参数-XX:+PrintFlagsFinal,在项目启动时即可输出JVM的默认参数值
intx BiasedLockingBulkRebiasThreshold = 20 默认偏向锁批量重偏向阈值
intx BiasedLockingBulkRevokeThreshold = 40 默认偏向锁批量撤销阈值
当然我们可以通过-XX:BiasedLockingBulkRebiasThreshold
-XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值

2.以class为单位,为每个class维护一个偏向锁撤销计数器。每一次该class的对象发生偏向撤销 *** 作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象也会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时class中的epoch值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的站,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获取锁时,发现当前对象的epoch值和class不相等,那就算当前已经偏向了其他线程,也不会执行撤销 *** 作,而是直接通过CAS *** 作将其mark word的Thread Id改为当前线程ID

相关演示代码:

public class Thread02 {
    static class A {

    }

    public static void main(String[] args) throws InterruptedException {
        //延时产生可偏向对象  演示 批量偏向锁
        Thread.sleep(5000);

        //创造100个偏向线程t1的偏向锁
        List<A> listA = new ArrayList<>();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                A a = new A();
                synchronized (a) {
                    listA.add(a);
                }
            }
            try {
                //为了防止JVM线程复用,在创建完对象后,保持线程t1状态为存活
                Thread.sleep(100000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        //睡眠3s钟保证线程t1创建对象完成
        Thread.sleep(3000);
        // 对象头中 19个对象--  偏向锁 指向T1  101
        System.out.println("打印t1线程,list中第20个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(19)).toPrintable()));
        System.out.println((ClassLayout.parseInstance(listA.get(21)).toPrintable()));
        //创建线程t2竞争线程t1中已经退出同步块的锁
        Thread t2 = new Thread(() -> {
            //这里面只循环了30次
            for (int i = 0; i < 24; i++) {
                A a = listA.get(i);
                synchronized (a) {
                    //分别打印第19次和第20次偏向锁重偏向结果 -323248123 升级轻量级锁
                    if (i == 18 || i == 19) {
                        System.out.println("第" + (i + 1) + "次偏向结果");
                        System.out.println((ClassLayout.parseInstance(a).toPrintable()));
                    }
                }
            }
            try {
                Thread.sleep(10000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t2.start();

        Thread.sleep(3000);
        System.out.println("打印list中第21个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(20)).toPrintable()));
        System.out.println("打印list中第29个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(29)).toPrintable()));
        System.out.println((ClassLayout.parseInstance(listA.get(30)).toPrintable()));
    }
}

3.4 批量撤销

当一个偏向锁如果撤销次数到达40的时候就认为这个对象设计的有问题;那么JVM会把这个对象所对应的类所有的对象都撤销偏向锁;并且新实例化的对象也是不可偏向的。

public class Thread03 {
    static class A {

    }

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        List<A> listA = new ArrayList<>();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                A a = new A();
                synchronized (a) {
                    listA.add(a);
                }
            }
            try {
                Thread.sleep(100000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        Thread.sleep(5000);

        Thread t2 = new Thread(() -> {
            //这里循环了40次。达到了批量撤销的阈值
            for (int i = 0; i < 40; i++) {
                A a = listA.get(i);
                synchronized (a) {
                }
            }
            try {
                Thread.sleep(10000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t2.start();

        //———————————分割线,前面代码不再赘述——————————————————————————————————————————
        Thread.sleep(5000);
//        System.out.println("打印list中第21个对象的对象头:");
//        System.out.println((ClassLayout.parseInstance(listA.get(20)).toPrintable()));


        Thread t3 = new Thread(() -> {
            for (int i = 20; i < 40; i++) {
                A a = listA.get(i);
                synchronized (a) {
                    if (i == 20 || i == 22) {
                        System.out.println("thread3 第" + i + "次");
                        System.out.println((ClassLayout.parseInstance(a).toPrintable()));
                    }
                }
            }
        });
        t3.start();


        Thread.sleep(10000);
        System.out.println("重新输出新实例A");
        System.out.println((ClassLayout.parseInstance(new A()).toPrintable()));
    }
}


4. 重量锁原理分析

如果其他的线程尝试轻量级的过程中,CAS多次还是失败,则轻量级会升级为重量级锁。
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层 *** 作系统的Mutex Lock实现, *** 作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
源码相关:ObjectMonitor::enter

5. 锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的请求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步 *** 作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

private static Object lock = new Object();
private static int count = 0;
private static int j = 0;

public static void a() {
    synchronized (lock) {
        b();
    }
    synchronized (lock) {
        c();
    }
}

public static void b() {
    count++;
}

public static void c() {
    j++;
}

public static void a1() {
    synchronized (lock) {
        b();
        c();
    }
}

/**
 * 改成:
 *
 * @param args
 */

public static void main(String[] args) {
    a();
}

6. 锁消除

锁消除是发生在编译器级别的一种锁优化方式。
有时候我们写的代码完全不需要加锁,却执行了加锁 *** 作。
比如,StringBuffer类的append *** 作:

public static void main(String[] args) {
    long start = System.currentTimeMillis();
    int size = 10000;
    for (int i = 0; i < size; i++) {
        createStringBuffer("demo", "demo01");
    }
    long timeCost = System.currentTimeMillis() - start;
    System.out.println("createStringBuffer:" + timeCost + " ms");
}

public static String createStringBuffer(String str1, String str2) {
    StringBuffer sBuf = new StringBuffer();
    sBuf.append(str1);// append方法是同步 *** 作
    sBuf.append(str2);
    return sBuf.toString();
}

代码中createStringBuffer方法中的局部对象sBuf,就只在该方法内的作用域有效,不同线程同时调用createStringBuffer()方法时,都会创建不同的sBuf对象,因此此时的append *** 作若是使用同步 *** 作,就是白白浪费的系统资源。

7. JDK15 默认关闭偏向锁优化原因

JDK15默认关闭偏向锁优化,如果要开启可以使用XX:+UseBiasedLocking,但使用偏向锁相关的参数都会触发deprecate警告
原因
1 偏向锁导致synchronization子系统的代码复杂度过高,并且影响到了其他子系统,导致难以维护、升级
2 在现在的jdk中,偏向锁带来的加锁时性能提升从整体上看并没有带来过多收益(撤销锁的成本过高 需要等待全局安全点,再暂停线程做锁撤销)
3 官方说明中有这么一段话: since the introduction of biased locking into HotSpot also change the amount of uncontended operations needed for that relation to remain true.,原子指令成本变化(我理解是降低),导致自旋锁需要的原子指令次数变少(或者cas *** 作变少 个人理解),所以自旋锁成本下降,故偏向锁的带来的优势就更小了。
维持偏向锁的机会成本(opportunity cost)过高,所以不如废弃

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/800542.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-07
下一篇 2022-05-07

发表评论

登录后才能评论

评论列表(0条)

保存