笔记 · 锁优化

锁竞争

在JDK 1.5之前,若想实现线程同步,只能通过synchronized关键字这一种方式实现;
底层,java也是通过synchronized关键字来做到数据的原子性维护的;
synchronized关键字是JVM实现的一种内置锁,从底层角度来说,这种锁的获取与释放都是由JVM帮助我们隐式实现的。

从JDK 1.5开始,并发包引入了Lock锁,Lock同步锁是基于java实现的,因此锁的获取与释放都是通过java代码来控制的;
然而synchronized是基于底层操作系统的Mutex Lock来实现的,每次对锁的获取与释放动作都会带来用户态与内核态之间的切换,这种切换会极大地增加系统的负担。

从JDK 1.6开始,JVM为了提升synchronized锁的性能,引入了:偏向锁、轻量级锁、重量级锁等。从而减少锁的竞争所带来的的用户态与内核态之间的切换;这种锁的优化实际上是通过Java对象头中的一些标志位来实现的;

( 对象头的构成:1、Mark Word		2、指向类的指针		3、数组长度
其中Mark Word (它记录了对象、锁及垃圾回收相关的信息,在64位的JVM中,其长度也是64bit) 的位信息
包含以下组成部分:
1. 无锁标记			-- 没有线程对其上锁
2. 偏向锁标记		
3. 轻量级锁标记
4. 重量级锁标记		-- 用户态与内核态之间的切换,被称为重量级锁
5. GC标记			-- 判断对象是否可被回收
)

对于synchronized锁来说,锁的升级主要都是通过 Mark Word中的锁标志位是否是偏向锁标志位来达成的;synchronized关键字所对应的锁都是先从偏向锁开始,随着锁竞争的不断升级,逐步演化至轻量级锁,最后则变成重量级锁

注:偏向锁在JDK 15版本中已被废弃。
在这里插入图片描述

偏向锁

针对于一个线程(假设为线程A)来说的,它的主要作用就是优化同一个线程对同一把锁多次获取的情况;如果一个synchronized方法被一个线程访问,那么这个方法所在的对象就会在Mark Word中将偏向锁进行标记,同时还会有一个字段来存储该线程的ID;当这个线程再次访问同一个synchronized方法时,它会检查这个对象的Mark Word 的偏向锁标记以及是否指向了其线程ID,如果是的话,那么该线程就无需再进入管程(Monitor)(即无需从用户态进入内核态获取锁,原因是在线程A的两次访问之间,并没有其他的线程获取过这个对象的锁),而是直接进入到该方法体中。

如果是另外一个线程(假设为线程B),在线程A访问之后,再次访问这个synchronized方法,那么实际情况会如何呢?

答:因为偏向锁 偏向的是上一次访问这个synchronized方法的线程A,所以当线程A访问过对象后,紧接着是线程B访问这个方法的时候(即 线程id不同),这个方法所在对象的,对象头上的 偏向锁标识 会被取消掉。
线程B获取锁成功或失败的处理,请看轻量级锁。

轻量级锁

若第一个线程已经获取过当前对象的锁。这时第二个线程与第一个线程开始尝试争抢该对象的锁,由于该对象的锁之前被第一个线程获取过了,因此它是偏向锁。而第二个线程在争抢时,会发现该对象头中的Mark Word 已经是偏向锁,但里面存储的线程ID不是自己(是第一个线程),那么它会进行CAS (Compare and Swap),从而获取到锁。这里第二个线程抢锁会存在两种情况:

  1. 获取锁成功:它会直接将Mark Word中的线程ID由第一个线程变成自己(偏向锁标记位保持不变),这样该对象依然会保持偏向锁的状态。
  2. 获取锁失败:则表示这时可能会有多个线程同时在尝试争抢该对象的锁,那么这时偏向锁就会升级为轻量级锁。

自旋属于轻量级锁的一种实现方式。

自旋锁
概述

如果线程A很快就可以执行完对象方法,并释放锁。此时就不会让线程B进入EntryList,而是让线程B进行自旋(即什么都不做,空转,等待线程A释放锁资源)(自旋会占用CPU的资源)。
由于在自旋的过程中,线程B并没有进入EntryList集合中等待,所以它一直是处于 用户态 的场景中,避免了 用户态与内核态 切换造成的性能开销。不过,自旋需要消费CPU资源。

若自旋失败(依然无法获取到锁),那么锁就会升级为重量级锁,这种情况下,无法获取到锁的线程都会进入到Monitor(即内核态)。

自旋对于Synchronized 关键字的底层意义

JVM中的同步是基于进入与退出监视器对象(管程对象) (Monitor) 来实现的,每个对象实例都会有一个Monitor对象,Monitor对象会和Java对象一同创建并销毁。Monitor对象是由C++实现的。

当多个线程同时访问一段同步代码时,这些线程会被放到一个EntryList集合中,处于阻塞状态的线程都会被放到该集合当中。接下来,当线程获取到对象的Monitor时,Monitor是依赖于底层操作系统的mutex lock (互斥锁) 来实现互斥的,线程获取mutex 成功,则会持有该mutex,这时其他线程就无法在获取到该mutex。

如果线程调用了wait() 方法,那么该线程就会释放掉所持有的mutex,并且该线程会进入到waitSet 集合(等待集合)中,等待下一次被其他线程调用 notify() / notifyAll() 唤醒。如果当前线程顺利执行完方法,那么它会释放掉所持有的mutex。
在这里插入图片描述
总结:同步锁在这种实现方式中,因为Monitor是依赖于底层的操作系统实现,这样就存在用户态与内核态之间的切换,所以会增加性能开销。
通过对象互斥锁的概念来保证共享数据操作的完整性。每个对象都对应于一个可称为[互斥锁]的标记,这个标记用于保证在任何时刻,只能有一个线程访问该对象。

(
用户态与内核态之间的切换:
1、当线程执行业务代码的时候, 因为现在还处于程序的范畴之内,所以他是处于用户态 的状态下。
2、假设线程A尝试的去获取某个对象的锁,但这把锁被另一个线程B持有。此时线程A 想获取对象的锁(Monitor),但被底层操作系统的mutex lock互斥了,没有获得锁,进入了等待的状态,它就会进入 内核态
3、线程A会一直尝试的获得对象的锁,如果获取成功,则会从 内核态 切换到 用户态。
4、所以会一直存在 用户态与内核态之间的切换。

JVM针对这种情况采取的优化措施:
1、首先明确锁已经被 线程B持有了。这种情况下,线程A是无法获取到对象的锁。
2、本来 线程A会进入EntryList集合中,并切换成 内核态。
3、优化点:如果线程B很快就可以执行完对象方法,并释放锁。此时就不会让线程A进入EntryList,而是让线程A进行自旋(即什么都不做,空转,等待线程B释放锁资源)(自旋会占用CPU的资源)。
由于在自旋的过程中,线程A并没有进入EntryList集合中等待,所以它一直是处于 用户态 的场景中,避免了 用户态与内核态 切换造成的性能开销。

自旋的问题:
1、如果线程B执行了很长的时间,线程A的自旋就是一种对CPU资源的浪费。
)

处于EntryList 与 WaitSet 中的线程均处于阻塞状态(因为它们在底层c++实现中,都是ObjectWaiter类型),阻塞操作是由操作系统完成的,在linux下是通过pthread_mutex_lock函数实现的。
线程被阻塞后便会进入到内核调度状态,这会导致系统在用户态 与 内核态之间来回切换的情况,严重影响锁的性能。

解决上述问题的办法是自旋(spin)。其原理是:当发生对Monitor的争抢时,若Owner (即持有Monitor的线程) 能够在很短的时间内释放掉锁,则那些正在争用的线程就可以稍微等待一下(即自旋),在Owner线程释放锁之后,争抢线程可能会立即释放掉锁,从而避免了线程阻塞。不过,当Owner运行的时间超过了临界值,争抢线程自旋一段时间后依然无法获取到锁,这时争抢线程则会停止自旋进入到阻塞状态。
所以总体思想是:先自旋,不成功在阻塞,尽量降低阻塞的可能性 (避免用户态与内核态之间的切换),这对 执行时间很短的代码块 来说有极大的性能提升。显然,自旋在多处理器(多核)上才有意义。

自旋锁在JDK1.4.2中引入,使用 -XX:+UseSpinning 来开启。JDK6中变为默认开启。

重量级锁

特点:线程最终从用户态进入到内核态。
在这里插入图片描述

锁消除

class Test {
    // 成员变量,所有线程共享,一次只能被一个线程获取
    // private Object object = new Object();

    public void method() {
        // 局部变量,每个线程各独有一份,其他线程不可见。这样synchronized关键字,就没有作用了。
        Object object = new Object();
        /**
         JIT编译器(Just In Time编译器)即时编译器,可以在动态编译同步代码块时,使用一种叫做逃逸分析
         的技术,通过该技术判断程序中所使用的的锁对象是否只被一个线程所使用,而没有散步到其他线程中;
         如果情况是这样的话,那么JIT编译器在编译这个同步代码时就会不产生synchronized关键字所标识的
         锁的申请与释放机器码 (相当于忽略了synchronized关键字),从而消除了锁的使用流程。
         **/
        synchronized (object) {
            System.out.println("hello world");
        }
    }
}

锁粗化

class MyTest1 {
    // 成员变量,所有线程共享,一次只能被一个线程获取
    private Object object = new Object();

    public void method() {
        /**
         从程序角度来看,下面的代码会分别进行三次锁获取、三次锁释放。
         JIT编译器(Just In Time编译器)即时编译器在执行动态编译时,若发现前后相邻的synchronized块
         使用的是同一个锁对象,那么它就会把这几个synchronized块给合并为一个较大的同步块。这样做的
         好处在于线程在执行这些代码时,就无需频繁申请与释放锁了,从而达到申请与释放锁一次,就可以执
         行完 全部的同步代码块。
         **/
        synchronized (object) {
            System.out.println("hello world");
        }

        synchronized (object) {
            System.out.println("welcome");
        }

        synchronized (object) {
            System.out.println("person");
        }
    }
}

在这里插入图片描述

注:资料主要来源【张龙

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值