精华内容
下载资源
问答
  • 锁升级过程(偏向锁/轻量级锁/重量级锁
    千次阅读
    2021-04-09 13:00:09

    锁的前置知识

    如果想要透彻的理解java锁的来龙去脉,需要先了解锁的基础知识:锁的类型、java线程阻塞的代价、Markword。

    锁的类型

    锁从宏观上分类,分为悲观锁与乐观锁。

    乐观锁

    乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低。每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

    java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

    悲观锁

    悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高。每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会阻塞(block)直到拿到锁。

    java中的悲观锁就是Synchronize、AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到才会转换为悲观锁,如:ReentrantLock。

    线程阻塞的代价

    java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与核心态之间切换,这种切换会消耗大量的系统资源。因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

    1. 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
    2. 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。

    synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁。为了缓解上述性能问题【synchronized】,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。

    明确java线程切换的代价,是理解java中各种锁的优缺点的基础之一。

    Mark Word

    在介绍java锁之前,先说下什么是markword,markword是java对象数据结构中的一部分,要详细了解java对象的结构可以点击这里,这里只做markword的详细介绍,因为对象的markword和java各种类型的锁密切相关;

    markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,如下表所示:

    状态标志位存储内容
    未锁定01对象哈希码、对象分代年龄
    轻量级锁定00指向锁记录的指针
    膨胀(重量级锁定)10执行重量级锁定的指针
    GC标记11空(不需要记录信息)
    可偏向01偏向线程ID、偏向时间戳、对象分代年龄

    32位虚拟机在不同状态下markword结构如下图所示:

    这里写图片描述

    了解了markword结构,有助于后面了解java锁的加锁解锁过程;

    前面提到了java的4种锁,他们分别是重量级锁、自旋锁、轻量级锁和偏向锁。不同的锁有不同特点,每种锁只有在其特定的场景下,才会有出色的表现,java中没有哪种锁能够在所有情况下都能有出色的效率,引入这么多锁的原因就是为了应对不同的情况;

    Java中的锁

    偏向锁

    Java偏向锁(Biased Locking)是Java6引入的一项多线程优化,它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

    偏向锁,顾名思义它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
    如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

    偏向锁获取过程

    1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
    2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
    3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
    4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the world)
    5. 执行同步代码。

    注意:第四步中到达安全点safepoint会导致stop the world,时间很短。

    偏向锁获取过程

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

    偏向锁的适用场景

    ​ 始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the world操作;
    在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向锁的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用。

    查看停顿–安全点停顿日志

    要查看安全点停顿,可以打开安全点日志,通过设置JVM参数 -XX:+PrintGCApplicationStoppedTime 会打出系统停止的时间,添加 -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 这两个参数会打印出详细信息,可以查看到使用偏向锁导致的停顿,时间非常短暂,但是争用严重的情况下,停顿次数也会非常多;

    注意:安全点日志不能一直打开,只在问题排查时打开:

    1. 安全点日志默认输出到stdout,一是stdout日志的整洁性,二是stdout所重定向的文件如果不在/dev/shm,可能被锁。
    2. 对于一些很短的停顿,比如取消偏向锁,打印的消耗比停顿本身还大。
    3. 安全点日志是在安全点内打印的,本身加大了安全点的停顿时间。

    如果在生产系统上要打开,再再增加下面四个参数:-XX:+UnlockDiagnosticVMOptions -XX: -DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log 打开Diagnostic(只是开放了更多的flag可选,不会主动激活某个flag),关掉输出VM日志到stdout,输出到独立文件,/dev/shm目录(内存文件系统)。

    这里写图片描述
    第一部分是时间戳,VM Operation的类型;

    第二部分是线程概况,被中括号括起来【total: 安全点里的总线程数、initially_running: 安全点开始时正在运行状态的线程数、wait_to_block: 在VM Operation开始前需要等待其暂停的线程数】

    第三部分是到达安全点时的各个阶段以及执行操作所花的时间【spin:等待线程响应safepoint号召的时间、block:暂停所有线程所用的时间、sync:等于spin+block,这是从开始到进入安全点所耗的时间,可用于判断进入安全点耗时、cleanup:清理所用时间、vmop:真正执行VMOperation的时间】其中最重要的是vmop。

    可见,那些很多但又很短的安全点,全都是RevokeBias, 高并发的应用会禁用掉偏向锁。

    Jvm开启/关闭偏向锁

    • 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
    • 关闭偏向锁:-XX:-UseBiasedLocking

    轻量级锁

    轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;

    轻量级锁加锁过程

    1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图所示:

      这里写图片描述
    2. 拷贝对象头中的Mark Word复制到锁记录中;

    3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。

    4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示:

      这里写图片描述
    5. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

    轻量级锁释放过程

    释放锁线程视角:由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的markword,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对markword做了修改,两者比对发现不一致,则切换到重量锁。

    因为重量级锁被修改了,所有display mark word和原来的markword不一样了。怎么补救?就是进入mutex前,compare一下obj的markword状态,确认该markword是否被其他线程持有。此时如果线程已经释放了markword,那么通过CAS后就可以直接进入线程,无需进入mutex,就这个作用。

    尝试获取锁线程视角:如果线程尝试获取锁的时候,轻量锁正被其他线程占有,那么它就会修改markword,修改重量级锁,表示该进入重量锁了。

    还有一个注意点:等待轻量锁的线程不会阻塞,它会一直自旋等待锁,并如上所说修改markword。

    这就是自旋锁,尝试获取锁的线程,在没有获得锁的时候,不被挂起,而转而去执行一个空循环,即自旋。在若干个自旋后,如果还没有获得锁,则才被挂起,获得锁,则执行代码。

    自旋锁

    自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

    但是线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,如果一直获取不到锁,那线程也不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

    自旋锁的优缺点

    :自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!

    :但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着茅坑又不拉屎,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要CPU的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭自旋锁;

    自旋锁时间阈值

    ​ 自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!

    ​ JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化:

    1. 如果平均负载小于CPUs则一直自旋;
    2. 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞;
    3. 如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞;
    4. 如果CPU处于节电模式则停止自旋;
    5. 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差);
    6. 自旋时会适当放弃线程优先级之间的差异;

    自旋锁的开启

    JDK1.6中 -XX:+UseSpinning 开启;-XX:PreBlockSpin=10 为自旋次数;
    JDK1.7后,去掉此参数,由jvm控制;

    重量级锁Synchronized

    在JDK1.5之前都是使用synchronized关键字保证同步的,它可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

    • 普通同步方法,锁是当前实例对象 ;
    • 静态同步方法,锁是当前类的class对象 ;
    • 同步方法块,锁是括号里面的对象;

    Synchronized的实现

    这里写图片描述

    它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

    1. Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
    2. Entry List:候选者队列 ,Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
    3. Wait Set:阻塞队列,哪些调用wait方法被阻塞的线程被放置在这里;
    4. OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
    5. Owner:当前已经获取到所资源的线程被称为Owner;
    6. !Owner:当前释放锁的线程。

    JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。

    OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。

    处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。

    Synchronized是非公平锁,在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。

    Synchronized锁的演变过程

    1. 检测Mark Word里面是不是当前线程的ID,如果是则表示当前线程处于偏向锁;
    2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1;
    3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁;
    4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁;
    5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁;
    6. 如果自旋成功则依然处于轻量级状态;
    7. 如果自旋失败,则升级为重量级锁;

    在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;

    偏向锁是在无锁争用的情况下使用的,也就是同步开在当前线程没有执行完之前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁;

    注意:如果线程争用激烈,那么应该禁用偏向锁。

    关注公众号 ,专注于java大数据领域离线、实时技术干货定期分享!个人网站 www.lllpan.top

    在这里插入图片描述

    更多相关内容
  • 在本文中小编给的大家整理了关于Java锁的升级策略 偏向锁 轻量级锁 重量级锁的相关知识点内容,需要的朋友们参考下。
  • 目录由一个问题引发的思考多线程对于共享变量访问带来的安全性问题线程安全性思考如何保证线程并行的数据安全性synchronized 的基本...monitor重量级锁的加锁回顾线程的竞争机制**偏向锁**轻量级锁重量级锁Synchronized

    由一个问题引发的思考

    线程的合理使用能够提升程序的处理性能,主要有两个方面,

    1. 能够利用多核 cpu 以及超线程技术来实现线程的并行执行;
    2. 线程的异步化执行相比于同步执行来说,异步执行能够很好的优化程序的处理性能提升并发吞吐量

    同时,也带来了很多麻烦,举个简单的例子:

    多线程对于共享变量访问带来的安全性问题

    一个变量 i. 假如一个线程去访问这个变量进行修改,这个时候对于数据的修改和访问没有任何问题。但是如果多个线程对于这同一个变量进行修改,就会存在一个数据安全性问题

    image-20220116153316991

    线程安全性

    本质上是管理对于数据状态的访问,而且这个这个状态通常是共享的、可变的。

    1. 共享,是指这个数据变量可以被多个线程访问;
    2. 可变,指这个变量的值在它的生命周期内是可以改变的。

    一个对象是否是线程安全的,取决于它是否会被多个线程访问,以及程序中是如何去使用这个对象的。所以,如果多个线程访问同一个共享对象,在不需额外的同步以及调用端代码不用做其他协调的情况下,这个共享对象的状态依然是正确的(正确性意味着这个对象的结果与我们预期规定的结果保持一致),那说明这个对象是线程安全的。

    public class SynDemo {
        private static int count = 0;
    
        public static void incr(){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
    
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 1000; i++) {
                new Thread(() -> SynDemo.incr()).start();
            }
            Thread.sleep(2000);
            System.out.println("运行结果"+count);
        }
    }
    
    //运行结果993
    
    

    在 Java 中如何解决由于线程并行导致的数据安全性问题呢?

    思考如何保证线程并行的数据安全性

    问题的本质在于共享数据存在并发访问。如果我们能够有一种方法使得线程的并行变成串行,那是不是就不存在这个问题呢?

    按照大家已有的知识,最先想到的应该就是锁吧。毕竟这个场景并不模式,我们在和数据库打交道的时候,就了解过悲观锁、乐观锁的概念。 什么是锁?它是处理并发的一种同步手段,而如果需要达到前面我们说的一个目的,那么这个锁一定需要实现互斥的特性。

    Java 提供的加锁方法就是 Synchroinzed 关键字。

    synchronized 的基本认识

    在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着 Java SE 1.6 对synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁

    synchronized 的基本语法

    synchronized 有三种方式来加锁,分别是

    1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
    2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
    3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

    不同的修饰类型,代表锁的控制粒度

    synchronized 的应用

    修改前面的案例,使用 synchronized 关键字后,可以达到数据安全的效果

    public class SynDemo {
        private static int count = 0;
    
        public  static void incr(){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
    
        public synchronized static void incr1(){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
    
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 1000; i++) {
                //new Thread(() -> SynDemo.incr()).start();
                new Thread(() -> SynDemo.incr1()).start();
            }
            Thread.sleep(2000);
            System.out.println("运行结果"+count);
        }
    }
    

    思考锁是如何存储的

    可以思考一下,要实现多线程的互斥特性,那这把锁需要哪些因素?

    1. 锁需要有一个东西来表示,比如获得锁是什么状态、无锁状态是什么状态
    2. 这个状态需要对多个线程共享

    那么我们来分析,synchronized 锁是如何存储的呢?

    观察synchronized 的整个语法发现,synchronized(lock)是基于lock 这个对象的生命周期来控制锁粒度的,那是不是锁的存储和这个 lock 对象有关系呢?

    于是我们以对象在 jvm 内存中是如何存储作为切入点,去看看对象里面有什么特性能够实现锁

    对象在内存中的布局

    在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:

    • 对象头(Header)、
    • 实例数据(Instance Data)、
    • 对齐填充(Padding)

    image-20220116154237658

    image-20220116154423740

    为什么任何对象都可以实现锁

    1. Java 中的每个对象都派生自 Object 类,而每个Java Object 在 JVM 内部都有一个 native 的 C++对象oop/oopDesc 进行对应。
    2. 线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带 monitor。在 hotspot 源码的markOop.hpp 文件中,可以看到下面这段代码。
    3. image-20220116154732414
    4. 多个线程访问同步代码块时,相当于去争抢对象监视器修改对象中的锁标识,上面的代码中ObjectMonitor这个对象和线程争抢锁的逻辑有密切的关系

    synchronized 锁的升级

    锁的种类

    jvm 存在偏向锁、轻量级锁、重量级锁。

    在分析这几种锁的区别时,我们先来思考一个问题,使用锁能够实现数据的安全性,但是会带来性能的下降。不使用锁能够基于线程并行提升程序性能,但是却不能保证线程安全性。这两者之间似乎是没有办法达到既能满足性能也能满足安全性的要求。

    hotspot 虚拟机的作者经过调查发现,

    • 大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。

    • 所以基于这样一个概率,是的 synchronized 在JDK1.6 之后做了一些优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。

    • 因此大家会发现在 synchronized 中,锁存在四种状态分别是:无锁、偏向锁、轻量级锁、重量级锁;

    • 锁的状态根据竞争激烈的程度从低到高不断升级。

    偏向锁

    前面说过,大部分情况下,锁不仅仅不存在多线程竞争,而是总是由同一个线程多次获得,为了让线程获取锁的代价更低就引入了偏向锁的概念。怎么理解偏向锁呢?

    当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的 ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了

    偏向锁的获取
    1. 首先获取锁 对象的 Markword,判断是否处于可偏向状态。(biased_lock=1、且 ThreadId 为空)

    2. 如果是可偏向状态,则通过 CAS 操作,把当前线程的 ID写入到 MarkWord

      1. 如果 cas 成功,那么 markword 就会变成这样。表示已经获得了锁对象的偏向锁,接着执行同步代码块
      2. 如果 cas 失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行
    3. 如果是已偏向状态,需要检查 markword 中存储的ThreadID 是否等于当前线程的 ThreadID

      1. 如果相等,不需要再次获得锁,可直接执行同步代码块
      2. 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁
    偏向锁的撤销

    偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为 偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,发现 cas 失败也就是存在线程竞争时,直接把被偏向 的锁对象升级到被加了轻量级锁的状态。对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程,有两种情况:

    1. 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向但前线程

    2. 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块

      在我们的应用开发中,绝大部分情况下一定会存在 2 个以上的线程竞争,那么如果开启偏向锁,反而会提升获取锁的资源消耗。所以可以通过 jvm 参数UseBiasedLocking 来设置开启或关闭偏向锁

    流程图分析

    image-20220116160452699

    轻量级锁

    轻量级锁的加锁

    锁升级为轻量级锁之后,对象的 Markword 也会进行相应的的变化。升级为轻量级锁的过程:

    1. 线程在自己的栈桢中创建锁记录 LockRecord。
    2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。
    3. 将锁记录中的 Owner 指针指向锁对象。
    4. 将锁对象的对象头的 MarkWord替换为指向锁记录的指针。
    image-20220116160840118 image-20220116160856504
    自旋锁

    轻量级锁在加锁过程中,用到了自旋锁

    • 所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。
    • 注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于在执行一个啥也没有的 for 循环。
    • 所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就能够获得锁了。

    自旋锁的使用,其实也是有一定的概率背景,在大部分同步代码块执行的时间都是很短的。

    • 所以通过看似无异议的循环反而能提升锁的性能。
    • 但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。
    • 默认情况下自旋的次数是 10 次,可以通过 preBlockSpin 来修改

    在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。

    • 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。
    • 如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源
    轻量级锁的解锁

    轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的MarkWord 中,

    如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁

    流程图分析

    image-20220116160937683

    重量级锁

    当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待被唤醒了。

    重量级锁的 monitor

    我们写一个java类,对行代码加锁,

    public abstract class User {
    
        public static void testSync() {
            synchronized (User.class) {
                System.out.println("我是锁");
            }
        }
    
        public static void main(String[] args) {
            User.testSync();
        }
    }
    

    使用 javac User.java 编译成class,然后使用javac -v User 反编译

    image-20220116163529560

    加了同步代码块以后,在字节码中会看到一个monitorenter 和 monitorexit。

    • 每一个 JAVA 对象都会与一个监视器 monitor 关联,我们可以把它理解成为一把锁,
    • 当一个线程想要执行一段被synchronized 修饰的同步方法或者代码块时,该线程得先获取到 synchronized 修饰的对象对应的 monitor。
    • monitorenter 表示去获得一个对象监视器。monitorexit 表示释放 monitor 监视器的所有权,使得其他被阻塞的线程可以尝试去获得
    • 这个监视器monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能
    重量级锁的加锁

    任意线程对 Object(Object 由 synchronized 保护)的访问,

    • 首先要获得 Object 的监视器。
    • 如果获取失败,线程进入同步队列,线程变为 BLOCKED。
    • 当访问 Object 的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

    image-20220116163908166

    回顾线程的竞争机制

    再来回顾一下线程的竞争机制对于锁升级这块的一些基本流程。方便大家更好的理解

    加入有这样一个同步代码块,存在 Thread#1、Thread#2 等多个线程

    synchronized (lock) {
      
    // do something*
      
    }
    
    • 情况一:只有 Thread#1 会进入临界区;

    • 情况二:Thread#1 和 Thread#2 交替进入临界区,竞争不激烈;

    • 情况三:Thread#1/Thread#2/Thread3… 同时进入临界区,竞争激烈

    偏向锁

    1. 此时当 Thread#1 进入临界区时,JVM 会将 lockObject 的对象头 Mark Word 的锁标志位设为“01”,同时会用 CAS 操作把 Thread#1 的线程 ID 记录到 Mark Word 中,此时进入偏向模式。

    2. 所谓“偏向”,指的是这个锁会偏向于 Thread#1,若接下来没有其他线程进入临界区,则 Thread#1 再出入临界区无需再执行任何同步操作。

    3. 也就是说,若只有Thread#1 会进入临界区,实际上只有 Thread#1 初次进入临界区时需要执行 CAS 操作,以后再出入临界区都不会有同步操作带来的开销。

    轻量级锁

    1. 偏向锁的场景太过于理想化,更多的时候是 Thread#2 也会尝试进入临界区,
    2. 如果 Thread#2 也进入临界区但是Thread#1 还没有执行完同步代码块时,会暂停 Thread#1并且升级到轻量级锁。
    3. Thread#2 通过自旋再次尝试以轻量级锁的方式来获取锁

    重量级锁

    1. 如果 Thread#1 和 Thread#2 正常交替执行,那么轻量级锁基本能够满足锁的需求。
    2. 但是如果 Thread#1 和 Thread#2同时进入临界区,那么轻量级锁就会膨胀为重量级锁,
    3. 意味着 Thread#1 线程获得了重量级锁的情况下,Thread#2就会被阻塞

    Synchronized 结合 wait,notify,notifyAll

    前面我们在讲 synchronized 的时候,发现被阻塞的线程什么时候被唤醒,取决于获得锁的线程什么时候执行完同步代码块并且释放锁。那怎么做到显示控制呢?我们就需要借 助 一 个 信 号 机 制 : 在 Object 对 象 中 , 提 供 了wait/notify/notifyall,可以用于控制线程的状态

    wait/notify/notifyall 基本概念

    • wait:表示持有对象锁的线程 A 准备释放对象锁权限,释放 cpu 资源并进入等待状态。

    • notify:表示持有对象锁的线程 A 准备释放对象锁权限,通知 jvm 唤 醒 某 个 竞 争 该 对 象 锁 的 线 程 X 。 线 程 A synchronized 代码执行结束并且释放了锁之后,线程 X 直接获得对象锁权限,其他竞争线程继续等待(即使线程 X 同步完毕,释放对象锁,其他竞争线程仍然等待,直至有新的 notify ,notifyAll 被调用)。

    • notifyAll:notifyall 和 notify 的区别在于,notifyAll 会唤醒所有竞争同一个对象锁的所有线程,当已经获得锁的线程A 释放锁之后,所有被唤醒的线程都有可能获得对象锁权

    限需要注意的是:三个方法都必须在 synchronized 同步关键字 所 限 定 的 作 用 域 中 调 用 , 否 则 会 报 错

    java.lang.IllegalMonitorStateException ,意思是因为没有同步,所以线程对对象锁的状态是不确定的,不能调用这些方法。

    另外,通过同步机制来确保线程从 wait 方法返回时能够感知到感知到 notify 线程对变量做出的修改

    image-20220116164554208

    展开全文
  • 重量级锁 1.1 什么是重量级锁?重量级是怎么体现的? 第一:执行同步代码块的时候或者执行同步方法的时候,需要为锁对象创建监视器,监视器用于关联锁对象、以及锁对象的原始头信息、重入次数、竞争失败队列、...

    目录

    一 重量级锁

    1.1 什么是重量级锁?重量级是怎么体现的?

    1.2 重量级锁的监视器工作流程

    1.2.1 线程竞争锁

    1.2.2 线程锁竞争成功

    1.2.3 线程竞争锁失败

    1.2.4 运行中的线程调用wait方法后,会被挂起

    1.2.5 阻塞队列中线程时间到期或者被唤醒

    1.2.6 轻量级锁膨胀之后,会导致竞争的线程加入到竞争失败队列_cxq中,然后挂起线程

    1.3 重量级锁的膨胀流程

    1.4 重量级锁加锁流程

    1.5重量级锁释放锁的流程

    1.6 重量级锁是如何保存hashcode和分代信息的

    1.7 重量级锁是如何实现重入的

    二 轻量级锁

    2.1 什么是轻量级锁?轻量是怎么体现的?

    2.2 轻量级锁的工作流程

    2.2.1 锁对象处于无锁状态

    2.2.2 锁对象处于有锁状态,但是是线程自己(重入)

    2.2.3 锁对象处于有锁状态,但是是别的线程持有锁(非重入)

    2.3 轻量级锁加锁流程

    2.4 轻量级锁释放锁的流程

    2.5 轻量级锁升级为重量级锁的流程

    2.5.1 升级时机

    2.5.2 升级流程

    2.6 轻量级锁如何保存锁对象的分代信息和hashcode等信息

    2.7 轻量级锁重入是怎么实现的

    三 偏向锁

    3.1 什么是偏向锁?

    3.2 偏向锁加锁流程

    3.2.1 没有开启偏向

    3.2.2 无锁状态

    3.2.3 重入状态

    3.2.4 已经偏向其他线程

    3.3 偏向锁释放锁的流程

    3.4 偏向锁升级流程

    3.4.1 升级时机

    3.4.2 升级流程

    3.5 偏向锁如何保存hashcode、分代信息等?

     

    一 重量级锁

    1.1 什么是重量级锁?重量级是怎么体现的?

    第一:执行同步代码块的时候或者执行同步方法的时候,需要为锁对象创建监视器,监视器用于关联锁对象、以及锁对象的原始头信息、重入次数、竞争失败队列、竞争队列和阻塞队列等信息,所以保存的东西占用的内存量很多,尤其是线程并发量大的时候。

    第二:另外线程竞争失败的队列需要进入队列挂起或者线程阻塞也需要挂起,当挂起的时间到期又需要唤醒线程,等待和唤醒是属于系统调用,会涉及到CPU在用户态和内核态切换,线程太多就会频繁切换。

    第三:内核线程需要操作系统进行调度,所以也会耗费时间。

    综上所述:上面就是重量级锁也是体现重量级的地方。

     

    1.2 重量级锁的监视器工作流程

    1.2.1 线程竞争锁

    新到来的线程释放锁唤醒的线程(假定继承人)会同时通过CAS方式竞争锁,即将监视器中的线程持有者_owner置为自己

     

    1.2.2 线程锁竞争成功

    线程竞争锁成功,就可以进入临界区执行代码

     

    1.2.3 线程竞争锁失败

    根据不同的唤醒策略QMode,那么失败后的处理结果不一样:

    情况一:QMode= 2

    失败的线程封装成ObjectWaiter,然后放入到_cxq锁竞争失败队列,并且唤醒队首的线程(此时唤醒的线程就是假定继承者),然后返回。

     

    情况二:QMode = 3

    失败的线程封装成ObjectWaiter,然后放入到_cxq锁竞争失败队列,并且将_cxq队列中所有元素插入到_EntryList队尾,并且唤醒_EntryList队首线程(此时唤醒的线程就是假定继承者)。

     

    情况三:QMode = 4

    失败的线程封装成ObjectWaiter,然后放入到_cxq锁竞争失败队列,并且将_cxq队列中所有元素插入到_EntryList队首,并且唤醒_EntryList队首线程(此时唤醒的线程就是假定继承者)。

     

    1.2.4 运行中的线程调用wait方法后,会被挂起

    运行中的线程调用wait方法后,会被挂起,放入到阻塞队列中_WaitSet中

     

    1.2.5 阻塞队列中线程时间到期或者被唤醒

    阻塞队列_WaitSet中线程时间到期或者被唤醒(notify),不会立刻抢占锁,而是先被放入到_EntryList中或者_cxq队列中(取决于Knob_MoveNotifyee这个值)

     

    1.2.6 轻量级锁膨胀之后,会导致竞争的线程加入到竞争失败队列_cxq中,然后挂起线程

     

    1.3 重量级锁的膨胀流程

     

    第一:获取锁对象的mark word

    第二:如果锁状态已经是重量级锁,返回监视器

    第三:如果当前锁正处于膨胀中,说明有其他线程正在膨胀锁,则当前线程自旋,重复步骤一

    第四:轻量级锁线程膨胀,首先创建监视器和初始化;然后CAS方式将锁状态置为膨胀中,如果失败有其他线程竞争,则释放锁然后重试,重复步骤一;紧接着设置监视器的displace header,将锁的持有者指向轻量级锁的指向的lock record,并且将锁对象的指针指向监视器(此步骤无需CAS,因为处于膨胀中无其他线程打扰),然后返回监视器

    第五:如果是其他情况,则也会创建监视器和初始化,并且将锁对象的指针以CAS方式指向监视器,如果失败则释放锁并进行重试,然后返回监视器。但是锁的持有者是NULL,即没有持有者。

     

    1.4 重量级锁加锁流程

    第一:不管是重量级锁获取锁还是轻量级锁锁竞争锁失败导致的升级或者轻量级锁因为升级而导致重入必须获取重量级锁,都需要先走膨胀流程,再获取锁

    第二:当前锁对象处于无锁状态,则将监视器的_owner字段通过CAS方式设置为当前线程,设置成功,返回执行同步代码块

    第三:如果是锁重入的情况,那么递增_recursions,返回执行同步代码块

    第四:如果当前线程是之前持有轻量锁的线程,可能发生重入。当重入之后,发现已经不是轻量级锁,则会走轻量级锁膨胀流程,然后进入加锁流程,重入次数只为我1,然后将锁的持有者_owner置为当前线程(锁升级的时候_owner是指向BasicLock(Lock Record)的指针),然后返回

    第五:如果当前没有锁,则自旋,自旋次数,默认10次。自旋期间,获取到锁了,则返回;如果没有则再次尝试获取锁,获取到锁了,则返回,如果失败则再次尝试自旋。

    第六:如果自旋成功,返回;如果继续失败,则将当前线程封装成Node节点,然后加入到_cxq等待队列。如果加入成功,则挂起;如果加入失败,则无限制获取锁直到成功,没有次数限制。

     

    1.5重量级锁释放锁的流程

    第一:不管是重量级锁还是轻量级锁因为锁升级的缘故,都需要走膨胀流程,然后再释放锁

    第二:锁如果没有膨胀,则将锁置为膨胀中;如果锁为膨胀中则自旋等待膨胀完成;如果以膨胀则返回

    第三:一般情况下,锁的持有者和当前线程是一致的,但是轻量级锁有可能不一样。

    因为轻量级锁升级的时候,锁的持有者无法设置为锁升级的线程,只能设置为当时持有轻量锁的线程的lock record。所以当持有轻量级锁的线程释放锁的时候,因为锁升级导致释放失败,从而会进入重量级锁的释放流程,所以导致释放锁的线程可能并不是重量级锁的持有者_owner。

    此时的轻量级锁已经是重量级锁,所以释放锁需要修改的是重量级锁的信息。故将重入次数_recursions置为0,将线程所有者_owner置为自己

    第四: 如果是重量级锁重入,那么递减_recursions就行

    第五:锁释放完毕,需要根据QMode唤醒_cxq或者_EntryList队首的线程作为假定继承人,假定继承人需要和新到来的线程竞争锁,由于重量级并不是公平锁,所以并不一定会成功获取锁

    失败的线程封装成ObjectWaiter,然后放入到_cxq锁竞争失败队列,并且唤醒队首的线程(此时唤醒的线程就是假定继承者),然后返回。

     

    情况二:QMode = 3

    失败的线程封装成ObjectWaiter,然后放入到_cxq锁竞争失败队列,并且将_cxq队列中所有元素插入到_EntryList队尾,并且唤醒_EntryList队首线程(此时唤醒的线程就是假定继承者)。

     

    情况三:QMode = 4

    失败的线程封装成ObjectWaiter,然后放入到_cxq锁竞争失败队列,并且将_cxq队列中所有元素插入到_EntryList队首,并且唤醒_EntryList队首线程(此时唤醒的线程就是假定继承者)。

     

    1.6 重量级锁是如何保存hashcode和分代信息的

    重量级锁监视器有一个_header字段,用于保存锁对象原始的hashcode或者分代信息

     

    1.7 重量级锁是如何实现重入的

    重量级锁监视器有一个_recursions字段,用于记录重入次数,如果没有重入了

    _recursions = 0;如果发生重入,_recursions递增

     

    二 轻量级锁

    2.1 什么是轻量级锁?轻量是怎么体现的?

    当只有少量线程的时候,并发程度很低,不存在同时竞争锁的情况,只需要尝试将自己的线程栈帧中的锁记录信息设置到锁对象中就可以,不需要进行等待和唤醒等操作,所以不存在内核态和用户态的切换,只有系统调用才会发生内核态和用户态的切换;而且还少了操作系统调度,提升了性能。所以它是轻量级的。

     

    注意:轻量级锁获取锁的过程并不存在自旋的情况,只是获取锁失败,在膨胀为重量级锁的时候如果有其他线程正在膨胀,则膨胀失败,进行自旋重试,而且这里的自旋也并没有次数限制。重量级锁是有自旋的,但是前提是轻量级锁已经膨胀为重量级锁。

     

    2.2 轻量级锁的工作流程

    2.2.1 锁对象处于无锁状态

    第一:从线程栈帧中找到一个可用的BasicObjectLock,即锁记录lock record,然后将锁记录obj和锁对象关联

    第二:将线程的锁记录中BasicLock锁的displaced header设置为现在无状态的锁对象的mark word,保存锁记录原始信息,比如hashcode、分代信息等

    第三:尝试将锁对象的mark word中的指针指向锁记录的BasicLock的指针,这样线程就持有轻量级锁,如果设置失败走膨胀流程。

     

    2.2.2 锁对象处于有锁状态,但是是线程自己(重入)

    如果持有锁的线程是线程自己,那么就是重入锁,将锁记录的displaced header置为NULL就可以,这里的重入获取的是一个新的BasicObjectLock,之所以不用保存锁对象的mark word,是因为第一个持有锁的记录已经保存了。

     

    2.2.3 锁对象处于有锁状态,但是是别的线程持有锁(非重入)

    如果既不是无锁状态,也不是重入,那么就表示有线程竞争,此时应该升级轻量级锁,走轻量级锁膨胀流程。

     

    2.3 轻量级锁加锁流程

    第一:获取锁对象的mark word

    第二:如果锁对象是无锁状态

    首先,将mark word设置到锁记录BasicLock中的displaced header中;然后通过CAS方式将锁对象的锁记录指针指向当前线程栈帧中的BasicLock锁记录地址;最后获取成功,执行同步块中代码

    第三:如果是重入

    如果是重入,那么当前栈帧会分配一个锁记录BasicObjectLock,然后将锁记录的displaced header置为NULL,设置成功,执行同步块中代码

    第四:锁对象是无锁状态,但是设置锁记录指针失败或者锁对象被其他线程持有,此时需要升级轻量级锁,进入膨胀流程

     

    2.4 轻量级锁释放锁的流程

    第一:获取锁对象的mark word

    第二:遍历线程帧栈中所有锁记录,获取保存有锁对象mark word的锁记录,然后将锁记录关联的锁对象置为NULL,表示已经移除了锁

    第三:如果是轻量级重入锁,和偏向锁一样,那么删除锁记录即可

    第四:如果不是重入锁,那就需要将锁记录关联的displaced header替换回mark word。替换成功,释放锁成功;释放失败,表示锁对象已经被别的线程膨胀了,现在的锁对象指向的是ObjectMonitor的地址,所以此时需要走重量级锁的退出流程

     

    2.5 轻量级锁升级为重量级锁的流程

    2.5.1 升级时机

    只要发生轻量级锁竞争,就会升级为重量级锁

     

    2.5.2 升级流程

    第一:线程竞争轻量级锁失败,表示现在有其他线程持有轻量级锁

    第二:此时会走重量级锁的膨胀流程

    第三:获取锁对象,检查对象状态

    #1 如果锁对象已经有监视器,说明已经是重量级锁,直接返回监视器

    #2 如果锁对象正处于膨胀中,说明有其他线程在膨胀,那么当前线程自旋,直到该锁对象膨胀成功

    #3 如果是轻量级线程触发膨胀,首先,创建所对象监视器;然后初始化监视器再然后将锁对象状态置为膨胀中,如果失败,说明有其他线程已经触发了膨胀流程

    #4 如果锁对象置为膨胀状态成功,则获取锁对象的mark word,然后设置到监视器、将轻量级锁的线程持有者置为_owner(注意不是膨胀线程)、设置监视器锁对象,然后返回监视器

    第四:然后进入到升级后的流程,主要区别为之前触发膨胀的线程和之前持有轻量级锁的线程

    触发膨胀的线程流程:

    #1 获取当前膨胀的线程

    #2 当前线程既不是锁的持有者(锁的持有者是之前持有轻量级锁的线程),也不是重入线程,则尝试自旋,这期间可能持有锁的线程已经释放了,如果自旋成功获取到锁则返回执行同步块代码。默认情况下自旋只有10次。

    #3 如果自旋失败,则尝试再次获取锁,如果成功获取到锁则返回执行同步块代码;如果失败再次尝试自旋,如果自旋成功,返回

    #4 将线程封装成Node节点,然后以CAS方式设置到_cxq队列中,将该线程挂起;如果设置失败,则尝试再次获取锁,如果成功返回,如果失败则自旋,重新将Node节点再次放入到_cxq竞争失败队列中,如此往复,没有限制

     

    膨胀前持有锁的线程的流程:

    #1因为锁对象mark word被改变了导致释放锁失败,从而进入轻量级锁膨胀流程,再进行释放锁

    #2 膨胀完成后,需要在进行重量级锁的释放,因为此时的锁已经是重量级锁。

    #3 如果当前线程之前之前持有轻量级锁的线程,首先将锁的持有者_owner设置为当前线程,不再是之前的该线程的帧栈中的BasicLock指针,然后重入次数_recursions置为0

    #4 然后将锁的_owner置为NULL,表示当前线程释放锁,然后根据具体的唤醒策略,从_cxq或者_EntryList唤醒一个线程去作为假定继承人

     

    2.6 轻量级锁如何保存锁对象的分代信息和hashcode等信息

    轻量级锁是可以将锁对象的分代信息和hashcode保存在线程帧栈中的displaced headere,当释放锁的时候就会替换回去

     

    2.7 轻量级锁重入是怎么实现的

    第一:如果有线程竞争锁,每一个线程都有会分配BasicObjectLock,字段obj关联

    锁对象,lock关联BasicLock,而BasicLock的_displaced_header保存锁对象的mark word

    第二:当线程重入的时候,同样会分配一个BasicObjectLock,也会将obj字段关联锁对象,但是不会设置BasicLock的_displaced_header,因为只需要有一个BasicObjectLock记录就可以了。

    第三:在释放轻量级锁的时候,一般会遍历线程中所有的BasicObjectLock锁记录,然后先从低位开始逐渐遍历和释放。

     

    三 偏向锁

    3.1 什么是偏向锁?

    当经常只有一个线程执行同步代码块的时候,其实复制mark word到锁记录displaced mark word都是没有必要的, 只要在锁对象中设置一个线程标识,每一次校验线程ID是不是持有锁的线程ID, 如果持有则可以执行同步代码块,没有多余的其他操作。这个偏向于某一线程的锁,就叫做偏向锁。

    3.2 偏向锁加锁流程

    3.2.1 没有开启偏向

    直接升级为轻量级锁,走轻量级锁升级流程

    3.2.2 无锁状态

    创建无锁状态的mark word,然后当前线程ID创建偏向锁的mark word,然后用D创建的偏向当前线程ID的偏向锁的mark word更新锁对象,如果个成功表示已经获取偏向锁;如果失败,表示有其他线程竞争偏向锁,需要撤销锁,升级为轻量级锁。

     

    3.2.3 重入状态

    如果已经存在偏向锁,但是偏向的是当前线程,则什么也不用做,可以直接执行同步块代码

     

    3.2.4 已经偏向其他线程

    则首先会走偏向锁撤销流程,至于是否升级为轻量级锁,需要取决于安全安全点的时候,持有偏向锁的线程是否已退出同步代码块

     

    3.3 偏向锁释放锁的流程

    第一:退出同步代码块的时候,需要释放锁

    第二:获取锁对象,一个线程帧栈因为重入可能有多个锁记录BasicObjectLock,所以需要从遍历栈帧中的BasicObjectLock,找到对应的锁记录BasicObjectLock

    第三:首先需要将BasicObjectLock锁记录的obj锁对象字段置为NULL,删除和锁对象的关联,即删除锁记录。这样就完成了锁的释放流程。

     

    3.4 偏向锁升级流程

    3.4.1 升级时机

    只要发生线程竞争,就会撤销锁,然后走升级流程,除非发生重偏向

    3.4.2 升级流程

    第一:当调用锁对象的hashcode或者多线程竞争锁失败,此时都需要将偏向锁撤销,升级为轻量级锁

    第二:首先需要撤销偏向锁,将锁对象置为无锁状态

    第三:判断锁状态是否有锁,如果没有锁,则设置线程栈帧中的锁记录的displaced header为锁对象的mark word, 然后尝试将锁记录的指针更新到锁对象,让锁对象指向自己,如果成功,那么获取锁,执行同步代码块代码;如果失败表示有多个线程在竞争锁,进入轻量级锁膨胀流程

    第四:如果有锁,但是锁对象mark word的指针指向的是自己,则属于重入,只需要将锁记录的displaced header置为NULL就可以

    第五:如果是有锁,但是不是重入或者无锁撞他竞争锁失败,都会进入轻量级锁膨胀流程。

     

    3.5 偏向锁如何保存hashcode、分代信息等?

    偏向锁是无法保存hashcode、分代信息的,如果调用锁对象的hashcode方法,此时会撤销偏向锁,升级为轻量级锁。因为轻量级锁会将锁对象的hashcode和age等信息保存到线程帧栈中的displaced header.

     

    展开全文
  • 轻量级锁与重量级锁

    2021-11-28 16:14:28
    一、轻量级锁 1、使用场景 轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用2者是透明的...

    目录

    一、轻量级锁

    1、使用场景

     2.使用过程

     2.1每次指向到synchronized代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的Mark Word和对象引用reference

     2.2让锁记录中的Object reference指向对象,并且尝试用cas(compare and sweep)替换Object对象的Mark Word ,将Mark Word 的值存入锁记录中

     2.3如果cas替换成功,那么对象的对象头储存的就是锁记录的地址和状态01,如下所示

     2.4如果cas失败,有两种情况

     2.5当线程退出synchronized代码块的时候,如果获取的是取值为 null 的锁记录 ,表示有重入,这时重置锁记录,表示重入计数减1

     2.6当线程退出synchronized代码块的时候,如果获取的锁记录取值不为 null,那么使用cas将Mark Word的值恢复给对象

    二、锁膨胀(重量级锁)

    1.定义

    2.转化过程  

    2.1当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁  

     2.2这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

    三、自旋优化(重量级锁的优化)

    1.定义

     2自旋重试成功的情况

     3自旋重试失败的情况


    一、轻量级锁

    1、使用场景

    轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用2者是透明的,即语法仍然是synchronized,假设有两个方法同步块,利用同一个对象加锁

    static final Object obj = new Object();
    public static void method1() {
         synchronized( obj ) {
             // 同步块 A
             method2();
         }
    }
    public static void method2() {
         synchronized( obj ) {
             // 同步块 B
         }
    }

     2.使用过程

     2.1每次指向到synchronized代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的Mark Word和对象引用reference

    lock record地址中 00轻量级锁 10重量级锁 11GC 再与Object的对象头的Hashcode交换  

     2.2让锁记录中的Object reference指向对象,并且尝试用cas(compare and sweep)替换Object对象的Mark Word ,将Mark Word 的值存入锁记录中

     2.3如果cas替换成功,那么对象的对象头储存的就是锁记录的地址和状态01,如下所示

     2.4如果cas失败,有两种情况

    1. 如果是其它线程已经持有了该Object的轻量级锁,那么表示有竞争,将进入锁膨胀阶段

    2. 如果是自己的线程已经执行了synchronized进行加锁,那么那么再添加一条 Lock Record 作为重入的计数

     如下图为情况2,在添加一条

     2.5当线程退出synchronized代码块的时候,如果获取的是取值为 null 的锁记录 ,表示有重入,这时重置锁记录,表示重入计数减1

     2.6当线程退出synchronized代码块的时候,如果获取的锁记录取值不为 null,那么使用cas将Mark Word的值恢复给对象

    1. 成功则解锁成功

    2. 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

    二、锁膨胀(重量级锁)

    1.定义

    如果在尝试加轻量级锁的过程中,cas操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。

    2.转化过程  

    2.1当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁  

     2.2这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

     2.2.1即为对象申请Monitor锁,让Object指向重量级锁地址,然后自己进入Monitor 的EntryList 变成BLOCKED状态

    2.2.2如图所示

     2.2.3当Thread-0 推出synchronized同步块时,使用cas将Mark Word的值恢复给对象头,失败,那么会进入重量级锁的解锁过程,即按照Monitor的地址找到Monitor对象,将Owner设置为null,唤醒EntryList 中的Thread-1线程

    三、自旋优化(重量级锁的优化)

    1.定义

    重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁

     2自旋重试成功的情况

     3自旋重试失败的情况

    自旋了一定次数还是没有等到持锁的线程释放锁

     自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。Java 7 之后不能控制是否开启自旋功能

    展开全文
  • 上篇文章分析了偏向锁、轻量级锁的演变过程,本篇将分析重头戏:重量级锁的原理。 通过本篇文章,你将了解到: 1、ObjectMonitor 的运用 2、锁的膨胀过程 3、重量级锁的加锁流程 4、重量级锁的解锁流程 5、重量级锁...
  • 一. 概述 ... 重量级锁 3.早期synchronized效率低的原因 Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会...
  • 对于synchronized这个关键字,可能之前大家有听过,他是一个重量级锁,开销很大,建议大家少用点。但到了jdk1.6之后,该关键字被进行了很多的优化,,建议大家多使用。 在现在的版本中,锁的状态总共有四种: 无锁...
  • Java对象头 普通对象 数组对象 其中 Mark Word在64 位虚拟机中的结构为 Mark Word后三位 ...若锁还未释放,另一线程就来竞争锁资源,则偏向锁直接升级为重量级锁。 调用hashcode方法,因为没有空间存储hashcod
  • Java锁事之重量级锁

    2021-03-10 00:43:52
    Java中的重量级锁是通过ObjectMonitor实现的。接下来简单分析下ObjectMonitor的实现逻辑。Objectmonitor中的关键词EntryListWaitListcxq(ContentionList)OwnerOnDeckThreadrecursionscxq(竞争列表)cxq是一个单向链表...
  • 文章目录一、简介二、Java对象头中的Mark Word三、偏向锁四、轻量级锁五、重量级锁六、自旋锁七、锁升级过程 一、简介 在讲解这些锁概念之前,我们要明确的是这些锁不等同于Java API中的ReentratLock这种锁,这些锁...
  • Monitor(重量级锁) 对象头 组成 工作机制 轻量级锁 加锁过程 锁重入 锁膨胀(转为重量级锁) 自旋 Monitor(重量级锁) 对象头 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给...
  • 一、Java中的锁分为两种:轻量级锁,重量级锁 轻量级锁,基z于CAS: 轻量级锁常见的两个问题: 1.ABA问题: 简单解释就是有一个基本类型a = 8,一个线程把他修改为a = 1,另一个线程有把他修改为a = 8,虽然前后...
  • 上篇文章已经分析了Java对象头构成、源码及其对象头的调试,本篇将分析偏向锁、轻量级锁、重量级锁的实现及其演变过程。由于涉及到c++源码,估计不少同学没兴趣看,因此重点多以图+源码辅助分析。 通过本篇文章,你...
  • 在Java SE1.6之前,synchronized一直都是重量级锁,如果某个线程获得了锁,其它获取锁的线程必须阻塞。在高并发的情况下,会有大量线程阻塞,导致系统响应速度急剧下降;同时不断的获取和释放锁也会导致线程不断切换...
  • Java中有着各种锁机制,今天我们要说的就是其中两种状态,轻量级锁与重量级锁,小伙伴们知道它们的区别分别有哪些吗?下面来了解一下吧。首先我们了解一下有哪些锁状态吧锁的状态总共有四种:无锁状态、偏向锁、轻量...
  • 无锁、偏向锁、轻量级锁、重量级锁 1、偏向锁、轻量级锁、重量级锁适用于不同的并发场景 偏向锁:无实际的锁竞争,且将来只有第一个申请锁的线程会使用锁。偏向锁只有初始化时需要一次CAS 轻量级锁:无实际的锁竞争...
  • java中每个对象都可作为锁,锁有四种级别,按照量级从轻到重分为:无锁、偏向锁、轻量级锁、重量级锁。并且锁只能升级不能降级。 在讲这三个锁之前,我先给大家讲清楚自旋和对象头的概念。 自旋 现在假设有这么一...
  • jdk1.2 重量级锁 (个人见解,尤其在重量级锁方面) 重量级锁和轻量级锁的概念 重量级锁:交给 OS 管理锁的争抢,释放 CPU 资源 轻量级锁:JVM 自己管理锁的争抢(无锁,自旋锁),CPU资源不释放 不上锁时的多线程...
  • 一、Java的 1. 悲观和乐观 首先我们需要明白一个概念,资源或者变量如果只是并发读是不会产生冲突的,我们所遇到的并发冲突都是并发写,或者既有读又有写。 悲观是指当对一个资源进行加锁时,默认这个资源是...
  • Java中的偏向锁,轻量级锁, 重量级锁解析

    万次阅读 多人点赞 2018-08-13 18:39:49
    参考文章 聊聊并发(二)Java SE1.6中的Synchronized Lock Lock Lock: Enter! 5 Things You Didn’t Know About Synchronization in Java and Scala ...Java 中的 在 Java 中主要2种加锁机制: synchr...
  • 自旋锁与重量级锁

    2021-05-07 14:02:10
    轻量级锁 CAS 抢锁失败,线程将会被挂起进入阻塞状态。如果正在持有锁的线程在很短的时间内释放资源,那么进入阻塞状态的线程无疑又要申请锁资源。 JVM 提供了一种自旋锁,可以通过自旋方式不断尝试获取锁,从而避免...
  • 对象头:synchronized用的锁是存在Java对象头里的。Java对象头里的Mark Word里默认存储对象的... 重量级锁 。锁可以升级但是不能降级。升级的目的是为了提高获得锁和释放锁的效率。 偏向锁 当一个线程访问同步块获.
  • ​ 为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令。 1.2 执行流程 获取锁: 检测Mark ...
  • synchronized原理,偏向锁,轻量级锁,重量级锁,锁升级
  • 重量级锁和轻量级锁 重量级锁和轻量级锁 一般(一般一般一般) 类似于悲观锁和乐观锁,站在了不同的角度划分的。 轻量级锁又叫自旋锁。 重量级锁与轻量级锁是站在 工作量 的角度来划分的; 而乐观锁和悲观锁则是站...
  • synchronized前言JDK对Synchronize的优化-锁膨胀偏向锁轻量级锁重量级锁重量级锁模式下的SynchronizeSynchronize 的核心组件Synchronized 的流程Synchronized 的实现Synchronized 的作用范围synchronized的简单使用 ...
  • 重量级锁
  • 状态标志 线程持有的 偏向线程ID 偏向时间戳 类型指针 指向类元数据InstanceKlass,确定该对象所属的类型。指向的其实是方法区中存放的类元信息 说明:如果对象是数组,还需要记录数组的长度 以 32 位...
  • 偏向锁,轻量级锁的应用场景

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 37,694
精华内容 15,077
关键字:

重量级锁