synchronized原理

使用

我们知道synchronized可以使用在代码块中和方法上;

使用在普通方法上,锁对象为当前对象实例;

使用在静态方法上,锁对象为当前类class对象;

使用在代码块上时,如果为this,则是当前类对象;如果为某个对象,则为该对象;如果为某个class,则锁对象为类class对象;

monitor(管程/监视器)

管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。

MESA管程模型

synchronized是jvm层面使用monitor机制实现的管程(很重)

底层Monitor对象的主要属性描述:锁的重入次数、存储锁对象,owner标识拥有monitor的线程,WaitSet等待线程(调用wait)组成的双向链表,cxq多线程竞争锁存在单向链表中(FILO栈结构),EntryList存放在进入或者重新进入时被阻塞的线程,也就是竞争锁失败的线程。synchronized是非公平锁,竞争失败的线程首先会存储在cxq中,而如果获得锁的线程调用wait(),会进入WaitSet链表被唤醒以后会进入EntryList中,此时再竞争锁EntryList中线程会优先获得锁,如果EntryList中为null,则将cxq中线程赋值到entryList中。

对象的内存布局

对象在内存中可以分为三个部分:对象头,实例数据,对齐填充位

对象头

Mark Word:HashCode、分代年龄、线程id、epoch、是否偏向锁、锁标志位;以下为64位jvm对象MarkWrod结构图 

Klass Pointer

对象通过该指针指向所属的类元数据。jdk1.8默认开启指针压缩后为4字节,当关闭指针压缩后为8字节。

数组长度(只有数组对象才有)

只有该对象为数组对象时才会有值,用来记录数组的长度,4个字节

synchronized锁状态

无锁(001),偏向锁(101),轻量级锁(00),重量级锁(10)

jol工具验证对象内存布局和锁的状态

对象内存布局

编写如下测试类,使用JOL工具提供的API查看Object对象的内存布局大小 

public class ObjectTest {
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

 执行结果:

前两个代表MarkWord共8个字节,第三个代表klass pointer 4个字节,最后一个为4字节的填充对齐位;共16个字节。

锁状态

偏向锁

Hotspot虚拟机在启动后默认会有4秒的延迟才会对每个新建的对象开启偏向锁模式。可以通过设置XX:BiasedLockingStartupDelay=0 参数关闭默认延迟偏向锁,也可以在启动时睡眠4-5s中等偏向锁加载完成;此时新建的对象处于可偏向但未偏向任何线程,也叫作匿名偏向状态。

1、偏向锁延迟偏向跟踪

public class SynchronizedTest {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
        Thread.sleep(5000);        
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

 从结果看出,确实存在延迟偏向。在没有开启偏向锁状态,通过synchronized获得的锁为轻量级锁。

2、如果调用object对象hashCode()方法会发什么呢,会不会开启偏向锁呢?

public class SynchronizedTest {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
        Thread.sleep(5000);        
        Object o = new Object();
        o.hashCode();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

从验证结果中得知:当调用了对象的hashCode()方法后,不会在开启偏向锁状态。个人理解是因为只有无锁状态会在内存中存储hashCode值,所以会撤销偏向锁不会开启偏向锁。 

3、如果调用在获得偏向锁的状态下调用hashCode()方法会发生什么呢?

public class SynchronizedTest {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        new Thread(()->{
            synchronized (o) {
                o.hashCode();
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();
    }
}

 在获得偏向锁的状态下调用hashCode()方法会导致直接升级为重量级锁

 另外,轻量级锁会在锁记录中记录 hashCode, 重量级锁会在 Monitor 中记录 hashCode

偏向锁调用wait()/notify():偏向锁状态下调用锁对象的wait()会升级为轻量级锁,调用notify会升级为重量级锁。 

4、 synchronized获取偏向锁

public class SynchronizedTest {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        new Thread(()->{
            synchronized (o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();
    }
}

从结果总结:新创建的线程获取偏向锁,并将线程ID参数使用CAS设置为428367877表示指向获得偏向锁的线程ID。

轻量级锁跟踪

在没有开启偏向锁的情况下或者偏向锁失败,通过synchronized获得的锁为轻量级锁,并不是直接升级为重量级锁。轻量级锁适应的场景为线程交替执行同步块的场合。

public class SynchronizedTest {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        new Thread(()->{
            synchronized (o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();
        Thread.sleep(1000);
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

从执行结果可以看出:第一次的Object对象内存布局为001无锁状态,当创建一个线程并通过synchronized获取锁,对象内存布局为00表示升级为轻量级锁,轻量级锁降级直接变为无锁状态,并不会降级为偏向锁。

偏向锁状态下2个线程轻微竞争导致锁升级为轻量级锁

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        new Thread(()->{
            synchronized (o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();
        Thread.sleep(5); //轻微竞争锁资源
        new Thread(()->{
            synchronized (o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();
        Thread.sleep(1000);
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

偏向锁升级为重量级锁

将上面代码Thread.sleep(5);注释,偏向锁状态会升级为重量级锁

 锁升级:偏向锁--轻量级锁--重量级锁

public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        new Thread(()->{
            synchronized (o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();
        Thread.sleep(2);
        new Thread(()->{
            synchronized (o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();
        new Thread(()->{
            synchronized (o) {
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }).start();
    }

偏向锁升级为轻量级锁:将MarkWord拷贝到线程栈中的Lock Record中,修改MarkWord指向Lock Record的地址。

轻量级锁膨胀为重量级锁:jvm会创建monitor对象,修改MarkWord,膨胀为重量级锁以后会自旋尝试获取锁,如果获取失败则将线程挂起。轻量级锁不存在自旋。

偏向锁撤销,分为两种:

1、撤销为无锁状态(当前对象未锁定)

2、偏向锁撤销升级为轻量级锁或者重量级锁(当前对象锁定)

synchronized锁优化

当有一个线程反复进入同步代码块时,偏向锁带来的性能损耗可以忽略不计,当时此时如果有其他线程来竞争锁时,偏向锁会撤销为无锁或者升级为轻量级锁甚至重量级锁,此时偏向锁不仅不能带来优化,反而会更加影响性能,因为偏向锁撤销需要等待safe point才能执行。针对大量偏向锁撤销存在性能问题,进行批量重偏向、批量撤销的优化。

小知识点:什么是safe point呢?

系统在稳定运行的过程中,应用在执行业务时会有大量的线程在不停的对栈和堆进行操作,如果此时jvm需要对栈和堆进行操作怎么办呢?比如GC,此时jvm会对堆进行操作,那么就会导致业务线程出现异常。safe point就是为了解决这个问题的,每个线程在执行到安全点时会去检查是否需要进入safe point操作,如果需要就会等所有线程进入安全点。什么时候会进入安全点呢?线程在竞争锁被阻塞,IO阻塞时,在等待获得监视器锁状态时等操作会导致线程进入safepoint。

偏向锁批量重偏向:

核心代码:其中thread1和thread2进行保活

new Thread(() -> {
    for (int i = 0; i < 50; i++) {
        Object lock = new Object();
        synchronized (lock) {
            list.add(lock);
        }
    }
}, "thead1").start();

new Thread(() -> {
    for (int i = 0; i < 40; i++) {
        Object obj = list.get(i);
        synchronized (obj) {
            if(i>=15&&i<=21||i>=38){
                log.debug("thread2-第" + (i + 1) + "次加锁执行中\t"+
                        ClassLayout.parseInstance(obj).toPrintable());
             }
        }
    }
}, "thead2").start();

new Thread(() -> {
    for (int i = 0; i < 50; i++) {
        Object lock =list.get(i);
        synchronized (lock){
            if(i>=17&&i<=21||i>=35&&i<=41){
                log.debug("thread3-第" + (i + 1) + "次加锁执行中\t"+
                        ClassLayout.parseInstance(lock).toPrintable());
            }
        }
    }
},"thread3").start();

log.debug((ClassLayout.parseInstance(new Object()).toPrintable()));

LockSupport.park();

thread1执行完成后会对50个对象锁进行偏向,并将锁对象的Thread ID指向thread1。接下来执行thread2,0-18次偏向锁撤销升级轻量级锁(解锁后为无锁状态),执行到第19次的时候发现前面所有的锁对象偏向的是thread1,而不是thread2,jvm会认为偏向锁偏向错了,会将接下来所有的锁对象重偏向指向thread2的ID,此为批量重偏向。19-40(解锁后还是偏向锁状态偏向thread2)

批量撤销

thread3执行0-18直接从无锁状态升级为轻量级锁状态,注意并不是从无锁到轻量级锁19-39从偏向thread2状态撤销升级为轻量级锁状态,thread2执行时已经撤销了19次,这里再次撤销21次达到批量撤销的阈值40,thread3继续执行40-49,从偏向thread1升级为轻量级锁状态。

最后当达到批量撤销的阈值40以后,jvm认为该类不可偏向任何线程,当再次使用Object类创建对象时,Object对象的锁状态为无锁状态,并不是偏向锁状态。

重量级锁:自旋优化目的是为了减少线程挂起的次数,尽量避免直接挂起线程,因为挂起操作涉及系统调用,存在用户态和内核态切换,开销比较大

锁粗化:StringBuffer在连续调用append()方法,出现频繁的加锁解锁操作;jvm会检测如果发现有一连串对同一个对象加锁,jvm对此进行优化,第一次执行append方法加锁,最后一个append()进行解锁,此为锁粗化。

锁消除

启动线程for循环重复调用下面方法, 开启锁消除的执行时间为2秒多,当设置jvm参数关闭锁消除时,执行时间大概在7秒左右

    /**
     * 锁消除
     * -XX:+EliminateLocks 开启锁消除(jdk8默认开启)
     * -XX:-EliminateLocks 关闭锁消除
     */
    public void append(String str1, String str2) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(str1).append(str2);
    }

逃逸分析

线程逃逸

创建大量线程,并在线程中创建锁对象,紧接着通过synchronized获取锁对象,此时每个线程的锁对象都是不同的,没有意义,相当于没有加锁。线程没有逃逸。

方法逃逸

连续大量调用某个方法,方法内创建StringBuffer对象并调用append()方法,jvm会进行锁消除进行优化

使用逃逸分析,编译器可以对代码做如下优化:
1.同步省略或锁消除(Synchronization Elimination)。如果一个对象被发现只能从一个线程被访问
到,那么对于这个对象的操作可以不考虑同步。
2.将堆分配转化为栈分配(Stack Allocation)。如果一个对象在子程序中被分配,要使指向该对象
的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
3.分离对象或标量替换(Scalar Replacement)。有的对象可能不需要作为一个连续的内存结构存
在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

个人笔记

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值