Sychronized锁升级的详细分析

前言:

本篇文章重点讲述sychronized的锁升级过程,主要涉及无锁到匿名偏向锁、偏向锁、轻量级锁、重量级锁的锁升级过程和每种锁的基本原理和优缺点。

一、轻量级锁(CAS)

  1. 基本概念

CAS(Compare and set)比较并交换,就是我们平常说到的乐观锁、自旋锁、轻量级锁。

具体流程:一个线程进来,获取对应的当前对象值E,然后执行对应的计算逻辑,计算完成得到结果值V,更新结果值V之前,会把计算前获取的对象值E和当前对象值N作比较,如果E和N一致,说明在执行计算逻辑过程中,没有其他线程进入更新数据,可以直接更新值V;如果E和N不一致,则重新获取当前对象值N,然后重新执行对应的计算逻辑,计算完成得到结果值V1,更新结果值V1之前,再次会把计算前获取的对象值N和当前对象值N1作比较,如果N和N1一致,直接更新V1,如果不一致继续循环取值计算,直到计算前获取的对象值和计算后一致,成功更新结果值。(会出现经典的ABA问题)

  1. CAS的ABA问题

问题描述:根据上面CAS的流程,我们很容易发现,在更新前比较对象初始值和当前值的时候,如果获取对象的初始值为A,在当前线程计算过程中,这个初始值A被其他线程变更为了B,又被其他线程更新为了A,当前线程计算完成后,做比较的时候,虽然都是A,但是已经经历了很多对象的变更,可能内部属性已经发生了变化,尤其对应引用对象来说。所以解决这个问题,我们在对应的对象上面加版本号,每变更一次,版本号加1,再比较的时候,使用对象值加版本号,就可以解决当前的ABA问题。

  1. Atomic的对应类

AtomicIntege采用的就是CAS的锁机制,我们可以感觉代码验证测试下效果,以下代码通过cas锁保证了多线程下count的原子性。

public class T01_AtomicInteger {
    AtomicInteger count = new AtomicInteger(0);
    void m() {
        for (int i = 0; i < 10000; i++)
            count.incrementAndGet(); 
    }

    public static void main(String[] args) {
        T01_AtomicInteger t = new T01_AtomicInteger();

        List<Thread> threads = new ArrayList<Thread>();

        for (int i = 0; i < 100; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }

        threads.forEach((o) -> o.start());

        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
}

count.incrementAndGet()-->unsafe.getAndAddInt-->this.compareAndSwapInt(var1, var2, var5, var5 + var4)的方法下面使用了CAS的方法代码截图。

unsafe是jvm层控制操作系统层的一个类,底层是C++的方法实现,最终底层实现是lock cmpxchg保证对象的原子性。

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END;
  1. CAS的缺点

  • CPU开销比较大:

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力

  • 不能保证代码块的原子性:

CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

  • ABA问题(上面已经介绍):

二、偏向锁

偏向锁的基本概念:

偏向锁的作用就是,当一个线程再次访问这个同步代码或方法时,该线程只需去对象头的 Mark Word 中判断一下是否有偏向锁指向它的 ID,无需再进入 Monitor 去竞争对象了。当对象被当做同步锁并有一个线程抢到了锁时,锁标志位还是 01,“是否偏向锁”标志位设置为 1,并且记录抢到锁的线程 ID,表示进入偏向锁状态。

一旦出现其它线程竞争锁资源时,偏向锁就会被撤销。偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法,如果是,则升级锁,反之则被其它线程抢占。

偏向锁的获取:

首先获取锁 对象的 Markword,判断是否处于可偏向状态。(biased_lock=1、且 ThreadId 为空)
如果是可偏向状态,则通过 CAS 操作,把当前线程的 ID 写入到 MarkWord
a) 如果 cas 成功,那么 markword 就会变成这样。 表示已经获得了锁对象的偏向锁,接着执行同步代码块
b) 如果 cas 失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行
如果是已偏向状态,需要检查 markword 中存储的
ThreadID 是否等于当前线程的 ThreadID
a) 如果相等,不需要再次获得锁,可直接执行同步代码块
b) 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁

偏向锁的撤销(消耗CPU资源):

偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,发现 cas 失败也就是存在线程竞争时,直接把被偏向
的锁对象升级到被加了轻量级锁的状态。对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程
有两种情况:
原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无 锁状态并且争抢锁的线程可以基于 CAS 重新偏向当前线程
如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块
在我们的应用开发中,绝大部分情况下一定会存在 2 个以上的线程竞争,那么如果开启偏向锁,反而会提升获取锁的资源消耗。所以可以通过 jvm 参数UseBiasedLocking 来设置开启或关闭偏向锁

偏向锁的参数设置:

-XX:-UseBiasedLocking // 关闭偏向锁(默认打开)

-XX:+UseHeavyMonitors // 设置重量级锁

测偏向锁的代码:

public static void main(String[] args) throws Exception {
        Thread.sleep(5000);
        Lock lock = new Lock();
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        synchronized (lock) {
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
ClassLayout使用的pom依赖:
<dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
 </dependency>

代码执行结果:

三、重量级锁(Sychnorized)

重量级锁主要基于操作系统中的Monitor锁实现的,重量级锁的执行效率比较低,处于重量级锁时被阻塞的线程不会消耗CPU资源。

它的底层是通过Monitor锁实现等待,如果当前对象锁的状态为偏向锁或轻量级锁,那么在调用锁对象的wait方法或notify方法,或者计算 锁对象的HashCode时,偏向锁或轻量级锁就会膨胀为重量级锁。

每一个重量级锁的线程,都会有成对出现的MonitorEnter和MonitorExit。sychnorized既保证了原子性又保证了可见性。

sychnorized锁对象和类的具体实现:

    private static int count = 10;
    private final Object o = new Object();

    /**
     * @Author wangchengzhi
     * @Description 锁定一个对象O
     * @Date 9:12 2023/3/9
     * @Param
     * @return
     **/
    public void m() {
        synchronized (o) {
            //任何线程要执行下面的代码,必须先锁定o
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }

    /**
     * @Author wangchengzhi
     * @Description使用synchronized (this)锁定当前对象
     * @Date 9:14 2023/3/9
     * @Param
     * @return
     **/
    public void m1(){
        synchronized (this){
            //任何线程要执行下面的代码,必须先锁定o
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }

    /**
     * @Author wangchengzhi
     * @Description synchronized加在方法上面,等同于synchronized (this)的方法
     * @Date 9:14 2023/3/9
     * @Param
     * @return
     **/
    public synchronized void m2(){
            //任何线程要执行下面的代码,必须先锁定o
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
    }

    /**
     * @Author wangchengzhi
     * @Description synchronized加载static修饰的方法上面,static方法不能用this
     * 所以我们使用m4的synchronized(T.class)锁对应的类对象
     * @Date 9:14 2023/3/9
     * @Param
     * @return
     **/
    public synchronized static  void m3(){
        //任何线程要执行下面的代码,必须先锁定o
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    /**
     * @Author wangchengzhi
     * @Description 效果和synchronized static相同
     * 所以我们使用m4的synchronized(T.class)锁对应的类对象
     * @Date 9:14 2023/3/9
     * @Param
     * @return
     **/
    public  static  void m4(){
        synchronized (T01_SyncObject.class){
            //任何线程要执行下面的代码,必须先锁定o
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }

    }

可重入锁:

重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。

synchronized 和 ReentrantLock 都是可重入锁。

可重入锁的意义之一在于防止死锁

测试可重入锁的代码:

 public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    System.out.println("第1次获取锁,这个锁是:" + this);
                    int index = 1;
                    while (true) {
                        synchronized (this) {
                            System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
                        }
                        if (index == 10) {
                            break;
                        }
                    }
                }
            }
        }).start();
    }

四、对象头中标记锁的状态

目前 Synchronized 锁状态有四种,级别从低到高分别是:无锁、偏向锁、轻量级锁和重量级锁。对应到 Mark Word 里,锁状态标识如下图所示:

  • 无锁:无锁不锁定资源,所有线程都能访问并修改同一资源,但同时只有一个线程能修改成功。

  • 偏向锁:Java6 中加入,如果一段同步代码,只被同一个线程访问,那么该线程会自动获取锁。

  • 轻量级锁:Java6 中加入,偏向锁被其他线程访问时,就会升级为轻量级锁,其他线程通过自旋尝试获取锁,不阻塞。

  • 重量级锁:等待锁的线程都会进入阻塞状态

五、锁升级的过程

锁升级的过程 new对象 无锁状态--->匿名偏向锁--->偏向锁--->轻量级锁--->重量级锁

对应的锁升级代码:

package com.mashibing.juc.c_001_sync_basics;

import org.openjdk.jol.info.ClassLayout;

/**
 * @Description TODO
 * @Author wangchengzhi
 * @Date 2023/3/9 16:59
 */
public class TestLockUp {
    static class A{}

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        A a = new A();
        //初始时偏向锁,但是此时偏向id为0,谁都不偏向
        System.out.println("初始偏向锁");
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        //偏向锁,此时有偏向id,偏向main线程
        synchronized (a) {
            System.out.println("thread1 locking 偏向锁");
            long l = System.nanoTime();
            System.out.println(ClassLayout.parseInstance(a).toPrintable());
            long l1 = System.nanoTime();
            System.out.println((l1 - l) / 1000.0 / 1000.0);
        }
        Thread.sleep(4000);

        //轻量级锁
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println("--thread3 locking-- 轻量级锁");
                    long l = System.nanoTime();
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                    long l1 = System.nanoTime();
                    System.out.println((l1 - l) / 1000.0 / 1000.0);
                }
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };


        //thread2,thread3 从轻量级锁-->重量级,都是重量级锁,因为它们存在竞争,锁升级
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println("thread1 locking  重量级锁");
                    try {
                        Thread.sleep(4000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    long l = System.nanoTime();
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                    long l1 = System.nanoTime();
                    System.out.println((l1 - l) / 1000.0 / 1000.0);
                }
                try {
                    //thread1退出同步代码块,且没有死亡
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread thread3 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println("thread2 locking 重量级锁");
                    long l = System.nanoTime();
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                    long l1 = System.nanoTime();
                    System.out.println((l1 - l) / 1000.0 / 1000.0);
                }
                try {
                    //thread1退出同步代码块,且没有死亡
                    Thread.sleep(3000);
                    System.out.println("线程死亡");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        //另一个线程获取main线程获取过的锁,此时main线程还存活,造成锁  偏向main线程锁->轻量级锁
        thread1.start();

        //延时确保让上一个线程执行完
        Thread.sleep(3000);
        thread2.start();

        //让thread3延时,造成锁竞争
        Thread.sleep(2000);
        thread3.start();

    }
}

结语:

本文通过偏向锁、轻量级锁、重量级锁几种锁的基本概念解释和代码demo运行,简单给大家分享了对应的锁概念,并对sychrozied的锁升级过程进行了解释,希望能帮助到大家对锁的理解,后期会出更详细的锁文章,期待您的关注。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值