Synchronized底层实现,锁升级的具体过程

Synchronized底层实现,锁升级的具体过程,第1张

Synchronized底层实现,锁升级的具体过程 锁的各种概念
  1. 自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能够成功获取,直到获取到锁才会退出循环。

  2. 乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,重试修改。

  3. 悲观锁:假定会发生并发冲突,同步所有对数据的相关 *** 作,从读数据就开始上锁。

  4. 独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再加锁;(单写)

  5. 共享锁(读):给资源加上读锁后只能读不能修改,其他线程也只能加读锁,不能加写锁;(多读)

  6. 可重入锁、不可重入锁:线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码。

  7. 公平锁、非公平锁:争抢锁的顺序,如果是按先来的先抢到,则为公平。

synchronized 为何jdk1.6后synchronized效率高

在Java早期版本中,synchronized属于重量级锁,效率低下,因为 *** 作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

庆幸的是在jdk1.6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Jdk1.6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁。

特性

可重入、独占、悲观

锁消除

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

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

从源码中可以看出,append方法用了synchronized关键词,它是线程安全的。但我们可能仅在线程内部把StringBuffer当作局部变量使用:

public class Demo {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int size = 10000;
        for (int i = 0; i < size; i++) {
            createStringBuffer("Hyes", "为分享技术而生");
        }
        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 *** 作若是使用同步 *** 作,就是白白浪费的系统资源。这时我们可以通过编译器将其优化,将锁消除,前提是java必须运行在server模式(server模式会比client模式作更多的优化),同时必须开启逃逸分析:-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks 其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。

锁粗化

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

一种极端的情况如下:

public void doSomethingMethod(){
    synchronized(lock){
        //do some thing
    }
    //这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
    synchronized(lock){
        //do other thing
    }
}

上面的代码是有两块需要同步 *** 作的,但在这两块需要同步 *** 作的代码之间,需要做一些其它的工作,而这些工作只会花费很少的时间,那么我们就可以把这些工作代码放入锁内,将两个同步代码块合并成一个,以降低多次锁请求、同步、释放带来的系统性能消耗,合并后的代码如下:

public void doSomethingMethod(){
    //进行锁粗化:整合成一次锁请求、同步、释放
    synchronized(lock){
        //do some thing
        //做其它不需要同步但能很快执行完的工作
        //do other thing
    }
}

另一种需要锁粗化的极端的情况是:

for(int i=0;i 

上面代码每次循环都会进行锁的请求、同步与释放,看起来貌似没什么问题,且在jdk内部会对这类代码锁的请求做一些优化,但是还不如把加锁代码写在循环体的外面,这样一次锁的请求就可以达到我们的要求,除非有特殊的需要:循环需要花很长时间,但其它线程等不起,要给它们执行的机会。

锁粗化后的代码如下:

synchronized(lock){ for(int i=0;i Java 对象在内存中的布局

对象和他的属性存储在堆中,对象中的String和其他对象类型字段通过引用方式,比如对象Teachaer中student字段指向Student实例,每个对象通过对象头指向所属类型

对象头

如上图所示,对象头分为三部分:

  1. Mark Word :对象加锁(synchronized(this))状态信息记录在Mark Word中,包括右侧锁相关信息,State代表锁所处的状态,如State值 Unlocked未锁定状态、Biased/biasable偏向锁、Leavy-weight locked重量级锁,Light-weight locked 轻量级锁

  2. Class meta address 指向方法区的类对象,表明当前对象的类信息

  3. Array lengh 对象中数组字段的长度,有的对象有数组字段。

锁升级过程 -不同状态
  1. 几种锁的区别

  2. 偏向锁因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。

    偏向锁原理和升级过程

    当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

    3.轻量级锁

    为什么要引入轻量级锁?

    轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

    如上图所示:两个或多个线程在方法栈中开辟一个空间命名为 Lock Record,将锁对象头的信息(Hashcode Age 0 01,未锁定状态值,并非偏向锁)拷贝到Lock Record,然后通过CAS进行争抢锁。

    当某一线程争抢成功,则锁对象头的mark word值变为 Lock Record Adress 0 0,mark word指向持有锁线程的栈空间Lock Record地址,持有锁的线程方法栈会有一个ower变量指向锁的对象头,表明当前的锁的持有者。

    争抢锁失败的线程进入自旋,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

    4.重量级锁

    紧接着上面轻量级锁步骤,重量级锁对象头值变为Monitor adress 1 0。此时对象的Monitor(应该升级到重量级锁才有的,具体不进行讨论)监视器含有三个要属性:entryList(锁池,抢锁未成功的锁进入)、WaitSet(等待池,调用wait方法进入)、owner(指向线程持有者)

    轻量级锁中未争抢到锁的线程(t2),自悬到一定次数,进入到entryList,ower属性的值是持有锁的线程值(t1),此时如果有其他线程(t3)来抢锁,因为锁被t1持有,(t3根据ower不为空),直接进入等待队列entryList;

    如果持有锁的线程(t1)执行了wait()则释放锁进入WaitSet中,此时owner为null,entryList中的线程被唤醒进行争抢锁,entryList线程出栈规则是先进先出,t2先出栈进行抢锁,此时如果有其他新的线程(t4)进入抢锁,则有可能新进入的线程(t4)抢锁成功,这就说明synronized是非公平的锁.

    当t1被唤醒(其他线程唤醒或者wait到一定时间自己唤醒),此时线程已经被其他线程持有,会进入锁池entryList阻塞排队。

    持有锁的线程,执行完毕,会调用monitorExit指令结束。

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

原文地址: https://outofmemory.cn/zaji/5693641.html

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

发表评论

登录后才能评论

评论列表(0条)

保存