java同步锁优化方案学习笔记(偏向锁,轻量级锁,自旋锁,重量级锁)

目录

一,概述

二,CAS算法

三,Java对象的对象头,以及Mark Word

四,偏向锁 Baised Lock

五,轻量级锁

六,自旋锁 SpinLock

七,重量级锁

八,在应用层提高锁效率的方案


一,概述

什么是java的锁?

为什么java要有锁?

java的锁为什么需要优化?

怎么优化的?

1,java中使用synchronized关键字来实现同步功能,被synchronized修饰的方法或是代码块,在多线程的情况下不能同时执行,只能挨个执行,以避免一些多线程并发的问题,这是java同步语句本身存在的意义。

2,在JDK1.5之前,事情就是像前面说的那么简单,当线程A和线程B同时执行到同步代码块时,他们会争抢同步锁,假设线程A抢到了锁,那么线程B就会从运行状态(Running)变成阻塞(Blocked)状态,直到线程A退出同步代码块,线程B才能获得锁,从阻塞状态变成运行状态,进入代码块开始执行。

3,到JDK1.6,java的开发者们不再满足于这种简单的运行-阻塞-运行模式了,因为这么在操作系统中切换线程的上下文的确挺慢,于是他们搞了一套优化的方案,也就是引入偏向锁,轻量级锁,自旋锁和重量级锁等概念,来提高同步锁的效率。从此,synchronized还是那个synchronized,用法还是那个用法,但是JVM不一定在拿不到锁的时候就直接阻塞线程了,而是有了一套更快一点的方案。

4,当锁的竞争很少或者基本没有时,JVM使用偏向锁来处理同步锁,这基本就算是没加锁。锁竞争越激烈的场景,JVM会把锁的处理方案会按照偏向锁,轻量级锁,自旋锁,重量级锁的顺序不断升级(或者叫锁的膨胀),这些锁的方案会消耗越来越多的资源,锁的效率也越来越低,所以JVM能用前面的方案就不会用后面的方案。

各个锁的具体介绍在后面。

 

先了解几个基本概念

二,CAS算法

CAS的全称是Compare-And-Swap,CAS算法的基本思想是这样的:当我们要改变一个变量的值时,先判断变量的值是否和某个预期值相同,如果相同则修改,如果不同则不修改。这个算法可以保证在多线程同时修改某个变量时,不会产生线程安全问题。

java中的java.util.concurrent.atomic包下的类很多都实现了这种算法,他们使用compareAndSet()方法来实现CAS,而且往往这个compareAndSet()方法对JVM来说都是原子操作,很安全。

 

三,Java对象的对象头,以及Mark Word

java的对象由三部分组成:对象头,实例数据,填充字节

非数组对象的对象头由两部分组成:指向类的指针和Mark Word

数组对象的对象头由三部分组成,比非数组对象多了块用于记录数组长度。

Mark Word用于记录对象的HashCode和锁信息等,在32位JVM中的Mark Word长度为32bit,在64位JVM中的Mark Word长度为64bit。

Mark Word的最后2bit是锁标志位,代表当前对象处于哪种锁状态,当Mark Word处于不同的锁状态时,Mark Word记录的信息也有所不同。

32位JVM中不同锁状态的Mark Word记录的信息如下表:

锁状态

25bit

4bit

1bit

2bit

23bit

2bit

是否偏向锁

锁标志位

无锁

对象的HashCode

分代年龄

0

01

偏向锁

线程ID

Epoch

分代年龄

1

01

轻量级锁

指向栈中锁记录的指针

00

重量级锁

指向重量级锁的指针

10

GC标记

11

可以看到无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。

随着锁的升级,Mark Word里面的数据就会按照上表不断变化,JVM也会按照Mark Word里面的信息来判断对象锁处于什么状态。

关于对象头和Mark Word的详细介绍见下面的连接:

https://blog.csdn.net/lkforce/article/details/81128115

 

下面单独介绍各个锁

四,偏向锁 Baised Lock

偏向二字就是字面上的意思,说的是第一个试图获取锁的线程,JVM会把锁对象的Mark  Word从无锁状态变成偏向锁状态,并把线程id记在锁对象的Mark Word中,当这个线程以后还想要获取这个锁时,JVM发现这个锁对象处于偏向锁状态,而且线程id就是这个线程自己,就直接让他通过,不用再进行争抢锁的操作了,省了CAS操作的时间。

当然这个特权只有第一个获取锁的线程才能拥有,这也就是偏向二字的意思。

如果有第二个线程想要来争抢锁,JVM发现锁对象处于偏向锁状态,而且线程id是另外一个线程,新线程会使用CAS操作试图争抢对象锁,如果成功,Word Mark中的线程id就会替换为新线程的id,如果失败,这个偏向锁就会升级为轻量级锁,同样也是改锁对象的Mark  Word。

由此可见,偏向锁是一种用缓存空间换时间的方案,在锁竞争不是很激烈的情况下会很有用,如果竞争比较激烈,JVM先使用偏向锁然后又不断进行锁升级,锁的效率会下降。

启用偏向锁的方式:

-XX:+UseBiasedLocking

-XX:BiasedLockingStartupDelay=0

关闭偏向锁的方式:

-XX:-UseBiasedLocking

 

五,轻量级锁

如果对象锁升级为轻量级锁,JVM会在当前线程的线程栈中开辟一块单独的空间叫锁记录(Lock Record),锁记录由两部分组成,分别是Displaced hdr和Owner。

JVM会把锁对象的Mark Word复制进去,然后把在对象Mark Word中保存指向锁记录的指针,并在锁记录的Owner中保存指向Mark Word的指针。这两个保存操作都是CAS操作。

如果保存成功,则表示当前线程获得该轻量级锁,修改锁对象的Mark Word锁标志位为00。

如果保存失败,JVM就检查锁对象的Mark Word是否已经保存了指向当前线程的指针,如果有则说明当前线程已经获得了这个锁,可以继续执行。如果没有指向当前线程的指针,则代表抢锁失败。

当前线程抢锁失败后会用自旋锁重试抢锁,如果一直失败,当前锁会升级为重量级锁,线程会被阻塞,锁对象的Mark Word标志位也会改为10。

 

六,自旋锁 SpinLock

spin在英文中用于描述纺纱的纱轮疯狂自转的样子,瞧这名字起的,一看就很耗CPU。

自旋锁其实并不属于锁的状态,从Mark Word的说明可以看到,并没有一个锁状态叫自旋锁。所谓自旋其实指的就是自己重试,当线程抢锁失败后,重试几次,要是抢到锁了就继续,要是抢不到就阻塞线程。说白了还是为了尽量不要阻塞线程。

由此可见,自旋锁是是比较消耗CPU的,因为要不断的循环重试,不会释放CPU资源。另外,加锁时间普遍较短的场景非常适合自旋锁,可以极大提高锁的效率。

在JDK1.6之前,自旋锁可以用参数来确定是否启用,以及自旋的次数:

-XX:+UseSpinning 启用自旋锁

-XX:PreBlockSpin=10 自旋次数10次

而从JDK1.7开始,自旋锁默认启用,而且JVM有了一套确认自旋次数和自旋周期的方案:

1,如果平均负载小于CPUs则一直自旋。

2,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞。

3,如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞。

4,如果CPU处于节电模式则停止自旋。

5,自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据之间的时间差)。

6,自旋时会适当放弃线程优先级之间的差异。

 

java代码可以实现类似自旋锁的功能,网上有很多,我贴一个jetty中写的自旋锁的例子:

import java.util.concurrent.atomic.AtomicReference;
public class SpinLock
{
    private final AtomicReference<Thread> _lock = new AtomicReference<>(null);
    private final Lock _unlock = new Lock();
   
    public Lock lock()
    {
        Thread thread = Thread.currentThread();
        while(true)
        {
            if (!_lock.compareAndSet(null,thread))
            {
                if (_lock.get()==thread)
                    throw new IllegalStateException("SpinLock is not reentrant");
                continue;
            }
            return _unlock;
        }
    }
   
    public boolean isLocked()
    {
        return _lock.get()!=null;
    }
   
    public boolean isLockedThread()
    {
        return _lock.get()==Thread.currentThread();
    }
   
    public class Lock implements AutoCloseable
    {
        @Override
        public void close()
        {
            _lock.set(null);
        }
    }
}

主要是思路就是用AtomicReference类的compareAndSet()方法,这个方法是原子操作,通过不断循环来重试获得锁。

 

七,重量级锁

重量级锁就是java最原始的同步锁,抢不到锁的线程就会被阻塞,在等待池中等待激活。

这种锁不是公平锁,来的早的线程不一定优先激活。

关于线程的等待池,JVM也有一套完整的运行方案。

 

八,在应用层提高锁效率的方案

上面所说的锁的优化方案都是在JVM内部的,由JVM自己搞定。在应用层,开发者也可以采取一些措施,提高锁的效率。

1,减少锁的持有时间

指的是不需要同步执行的代码,不要放在同步代码块中。同步块中代码减少,锁的持续时间短,锁的性能会有所提高。

2,减小锁粒度

指的是把资源分批使用不同的锁,不同批次的资源的操作互不影响。

比如ConturrentHashMap类,把map分成多段,每段一个锁,不在一段的数据可以同时修改。

3,锁分离

把关系不大的操作使用不同的锁,使这些操作互不影响。

比如LinkedBlockingQueue类,从队列头获取数据的take()方法和从队列末尾添加数据的put()方法分别使用不同的锁,两者互不影响。

4,锁粗化

指的是当虚拟机需要连续对同一把锁进行加锁和释放时,尽量改成只使用一次锁。

比如连续多个synchronized语句块,或循环中的synchronized语句块,用的是同一个对象作为锁,那还不如直接用一个synchronized语句块把他们都包含起来。

5,弃用synchronized关键字

不使用synchronized关键字,可以自己编写代码实现类似偏向锁、自旋锁的功能,减少因为同步锁而带来的效率损耗。比如上文中jetty自己实现的自旋锁。

 

以上。

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
无状态偏向锁轻量级重量级都是Java中的机制,它们的实现方式和性能表现不同。 无状态:也称为自旋锁,当线程尝试获取时,如果已经被其他线程占用,该线程会一直自旋等待的释放,直到获取到为止。这种适用于的持有时间非常短的情况,因为长时间的自旋会浪费CPU资源。 偏向锁偏向锁是一种针对加操作的优化手段,它的目标是减少无竞争情况下的操作的性能消耗。当一个线程访问一个偏向锁时,它会将对象头中的标识位设置为偏向,并将线程ID记录在对象头中。之后,该线程再次请求时,无需再次竞争,直接获取即可。这种适用于只有一个线程访问对象的情况。 轻量级轻量级是一种针对多线程竞争情况下的优化手段,它的目标是减少线程阻塞的时间,提高程序的并发性能。当一个线程访问一个轻量级时,它会将对象头中的标识位设置为轻量级,并将对象的指针保存在线程的栈帧中。之后,其他线程再次请求时,会通过自旋的方式尝试获取,而不是阻塞等待。如果自旋失败,就会升级为重量级。这种适用于的竞争不是很激烈的情况。 重量级重量级是一种针对多线程竞争情况下的优化手段,它的目标是保证线程的正确性和程序的稳定性。当一个线程访问一个重量级时,它会进入阻塞状态,直到被释放。这种适用于的竞争非常激烈的情况。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值