syncronized关键字

1、举例同步;

2、如何保证同步?原理

3、JDK6之前的实现

4、JDK6之后的实现过程

5、官网入手、讲解现象

6、偏向延迟和偏向不延迟

7、无锁升级到偏向锁

8、偏向锁升级到轻量级锁

9、轻量级锁升级到重量级锁

一、为什么需要syncronzied关键字

syncronzied关键字是java用来在并发场景做同步的。亲儿子

在并发场景中存在着线程安全问题。所以为了解决并发问题,使用syncronzied来进行同步操作。

案例

从一个例子入手

public class SyncronizedProOne {

    private final static Logger logger = LoggerFactory.getLogger(ObjectAgeTest.class);

    private static int money = 0;

    private static CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void increment(int count){
        money+=count;
    }

    public static void main(String[] args) throws InterruptedException {

        logger.info("当前的money的值是:{}",money);

        new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                increment(2);
            }
            countDownLatch.countDown();
        },"t1").start();


        new Thread(()->{
            for (int i = 0; i < 40000; i++) {
                increment(5);
            }
            countDownLatch.countDown();
        },"t2").start();

        countDownLatch.await();

        logger.info("当前的money的值是:{}",money);
    }

}

理想中的money的值应该是:300000。但是最终控制台打印的值是:239946

出现问题

主要是下面这行代码,二者是等价的

 money+=count; <===> money = money + count

这里因为是非原子性的操作。

假设在某个时刻,money的值是100,t1线程在执行加法操作的时候,t1线程看到的money是100,然后执行完加法之后得到101,这个时候t1线程发生了上下文切换,那么就会导致101还没有来得及赋值给money。

而上下文切换到t2的时候,发现money还是100,那么t2执行加法之后,给Money进行赋值为101。然后上下文再次切换,t1将101赋值给money,此时money也是101.

那么本来结果是102,但是两次加法之后,造成了结果是101,所以导致出现了数据安全问题。

解决问题

那么如何才能让其变成对的值呢?那么只需要在:money=money+count,这个阶段能够保证是原子性操作即可。

而且还要能够让多线程及时看到money的变化。那么首先对加法赋予原子性操作即可,保证可见性问题,只需要加上一个volatile关键字即可。

如下所示:

public class SyncronizedProTwo {

    private final static Logger logger = LoggerFactory.getLogger(ObjectAgeTest.class);

    private static volatile int money = 0;

    private static CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void increment(int count){
        money+=count;
    }

    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {

        logger.info("当前的money的值是:{}",money);

        new Thread(()->{
            synchronized (lock){
                for (int i = 0; i < 50000; i++) {
                    increment(2);
                }
            }

            countDownLatch.countDown();
        },"t1").start();


        new Thread(()->{
            synchronized (lock) {
                for (int i = 0; i < 40000; i++) {
                    increment(5);
                }
            }
            countDownLatch.countDown();
        },"t2").start();

        countDownLatch.await();

        logger.info("当前的money的值是:{}",money);
    }

}

测试了多次输出结果:300000

syncronzied使用方式

syncronzied是可以解决线程安全问题的。但是上面的使用方式仅仅只是syncronzied关键字的使用方式之一。

因为syncronzied的使用方式比较固定,所以将其列出来:

作用范围 锁对象
非静态方法 当前对象 => this
静态方法 类对象 => SynchronizedSample.class (一切皆对象,这个是类对象)
代码块 指定对象 => lock (以上面的代码为例)

思考

从使用方式上列举出来的,可以看到,不管是哪种使用方式锁对象都是一个明确的对象。

为什么????

先来看一下lock锁中的设计原理。

lock锁中用一个变量state作为锁是否被持有的一个标记,而syncronzied锁住的是一个对象,那么对象中哪里有标记这个标识呢?

难道是在对象中定义一个属性来表示标记么?如下:

public class LockFlag{
    private int state;
}

但是很明显的是,对象也可能是没有属性。如Object,而且上面使用的锁对象正是Object类型的对象。

那么既然这样子可以实现,那么很明显在堆内存中对象是可以进行标记的。那么就需要来了解堆内存中的对象是怎样的?

二、对象分析

JVM规范中规定:对象由三部分组成:对象头对象中的实例数据(可有可无,Object对象就没有)、对齐填充(以8个bit为单位,java对象的大小必须是8的倍数,如果不足8的倍数,需要进行填充到8的倍数)

所谓的实例数据如下所示:

public class A{
    // 实例数据
    private int age;
}

因为对象中可能不存在实例数据,所以这个不是本篇研究的重点。

那么重点就应该是对象头了。

2.1、对象头打印

对象头我们可以利用JOL工具类来进行分析

<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
    <!-- 这里将provided给去掉了 -->
</dependency>

然后写测试代码来进行测试:

因为使用的Object类型的,没有实例数据,所以在上面没有体现出来。

但是看到了Object数据类型的大小占据了16个字节。在一道面试题中见到过,记录一下。

2.2、JVM对对象头的规范

从ORACLE中的官网中爬取下来的一部分截图:

每个在堆中的对象有公共的结构。在堆中的对象对象头的布局中,Type,GC状态、同步状态、对象hashcode

对象头由两个关键字:mark wordklass pointer组成

如果是数组的话,还有一个长度字段。

对关键属性分析一下:

这里解释了为什么可以通过:obj.getClass()方法得到对象的类的类型。

而上面的状态的标识又都是在mark wordklass pointer中的。

所以需要来了解一下mark wordklass pointer又都是什么?

klass pointer

表示指向一个对象,这个对象中可以看到对象布局和方法;

也就说klass pointer指针指向的java中的类的地址(或者是说指向java类中的首地址,也就是class文件的地址(方法区中的首地址));

mark word

看官方文档中的说明:

用二进制表示同步状态、hashcode。可能会在二进制中低位同步关联状态。在GC期间,可能会有GC状态标记。

可以知道,mark word中二进制是会变化的,因为需要表示这么多的状态。

那么来看下HotSpot虚拟机中对mark word的说明:

可以看到在64位操作系统中至少存在着四种状态的变化。

那么这里来做一个总结:

对象头怎么来看?

上面已经通过JOL工具类来打印对象头的二进制标志位了。但是这里打印的二进制为和我们理解的有点问题。

为了解释,下面来写段代码表示:

打印出来的对象头如下所示:

00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000 

但是此时此刻一定要注意的是:因为X86架构计算机中是按照小端存储的。

如下所示:

!

高位存在高地址,低位存在低地址。而在读的时候,也会从低位开始读,所以读取出来的顺序和我们看到的顺序是相反的。

那么将对应的二进制反过来看一下:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

然后对照着上面的总结图来看一下:

按照上面的未偏向划分:

00000000 00000000 00000000 0 未使用

0000000 00000000 00000000 00000000 hashcode

0 没有被使用

0000 GC + 对象年龄

0 偏向标识 (0不可以偏向线程;1表示可以偏向线程)
01 锁的状态。偏向锁、轻量级锁、重量级锁、GC标记

01 偏向锁
10 轻量级锁
11 重量级锁
00 GC标记

所以这样子来研究才是对的。

三、syncronized关键字的几种状态说明

无锁状态

对于一个对象来说,在没有进入到同步块中时,都是无锁状态。

无锁状态下又划分成两个状态:是否可偏向。

是否可偏向的意思就是说,对于lock对象锁来说,是否可以让锁对象偏向某个线程。

在JVM中,规定了项目启动的四秒钟内是禁止可偏向的。

为什么?这个在研究下面的状态后再来分析。

无锁不可偏向

默认情况下,项目启动的时候,就是无锁不可偏向的。直接看下代码:

无锁可偏向

我们有两种方式可以获取得到无锁可偏向。

  • 1、设置偏向延迟为0;在idea中添加VM参数:-XX:BiasedLockingStartupDelay=0
  • 2、让线程先睡眠,再运行程序;

分别来测试一下:

注意:我忘记了修改注释了,所以下面两个测试不要看注释

1、设置偏向延迟为0

2、线程睡眠

偏向锁

大前提:当锁对象为无锁可偏向时,如果线程进入到同步代码块中来,那么将会导致锁对象的对象头偏向当前线程。

仔细观察一下,可以发现:尽管对象头中标记都是101,但是发现后面的不同。

因为在偏向锁中,需要记录一下偏向锁偏向线程的ID。这里是不同的。

轻量级锁

轻量级锁是如何发生的呢?

我总结了一下,分为以下两种情况:

  • 1、多个线程交替执行;没有交集;
  • 2、多个线程互斥执行;

下面写代码看一下:

1、多个线程交替执行

2、多个线程互斥执行;

重量级锁

在某一时刻,同步块中的锁产生了竞争。

对应的打印结果看下:

补充说明

但是有几种情况不得不来补充一下,以下几种场景分别都是存在的。

锁对象不可偏向时进入到同步代码块,直接进入到轻量级锁

代码演示:

对象调用hashcode进入到同步块中,直接升级成轻量级锁

又忘了修改注释了,不要看注释

锁对象调用wait方法会直接进入到重量级锁

对应输出:

补充一个特殊代码

对应的输出:

这里很奇怪,按照我们的理解,t2线程对象头中的信息应该是000才对,但是这里是101.

偏向锁,而且偏向的线程居然还是t1.

所以这里我才要来补充说明:

因为没有专业测试大佬和底层知识,所以只能够靠自己的猜想来进行猜测,然后给出自己的证明过程。

我的猜测:

在我看来,因为t1线程执行完成了,OS应该是释放了资源。但是在给t2线程分配资源的时候,复用了原来的线程ID号。

因为OS分配的线程号我们不知道是不是一样的。

但是对于JVM来说,JVM知道肯定不会是同一个线程的,但是对于syncronzied关键字来说,他是根据线程ID来判断的。

因为t2线程复用的线程t1的ID号,所以t2线程中认为当前线程(本来说是t2),认为是t1线程的ID。

所以没有发生锁撤销,直接复用了。

代码证明

但是证明失败!!!!!

于是乎,我以为是JVM中的线程ID被复用了,我又从Thread类中找到了关于线程ID的描述

线程在其生命周期中是一个唯一且不可改变的。

只有当线程终止的时候,线程的ID号才可能被复用。

因为当前t1线程没有终止,所以对于JVM中的线程ID来说,是不可能被复用的。

但是我检查了代码,发现t2线程根本没有加锁!!!!!!!!!!!!

于是乎,马上加锁看看

马上变成了轻量级锁。

但是我又马上写了另外一个代码。

测试结果:

完美的解决方案。

四、锁升级过程

偏向锁升级到轻量级锁

引用安琪拉博客中的一张流程图

从无锁到偏向锁转化的过程(JVM -XX:+UseBiasedLocking 开启偏向锁)

  • 1、首先A 线程访问同步代码块,使用CAS 操作将Lock对象的Thread ID 放到 Mark Word 当中,表示偏向这个线程;
  • 2、如果CAS 成功,此时线程A 就获取了锁;
  • 3、如果线程CAS 失败,证明有别的线程持有锁,例如上图的线程B 来CAS 就失败的,这个时候启动偏向锁撤销 (revoke bias);
  • 4、锁撤销流程:
    • 1-1、 让 A线程在全局安全点阻塞(类似于GC前线程在安全点阻塞);
    • 1-2、遍历线程栈,查看是否有被锁对象的锁记录( Lock Record);如果有Lock Record,需要修复锁记录和Markword,使其变成无锁状态;
    • 1-3、恢复A线程执行,将是否为偏向锁状态置为 0 ,开始进行轻量级加锁流程 (后面讲述)

下面看下这个Lock Record的过程:

锁撤销之后(偏向锁状态为0,表示不可偏向任何线程)。现在无论是A线程还是B线程执行到同步代码块进行加锁,流程如下:

  • 1、线程在自己的栈桢中创建锁记录 LockRecord;
  • 2、线程A 将 Mark Word 拷贝到线程栈的 Lock Record中,这个位置叫 displayced hdr,如下图所示:

1、将锁记录中的Owner指针指向加锁的对象(存放对象地址);

2、将锁对象的对象头的MarkWord替换为指向锁记录的指针。[这里也就说明了为什么还可以使用对象的hashcode

这二步如下图所示:

这时锁标志位变成 00 ,表示轻量级锁

所以上最开始的总结图中,可以看到,为什么只能够看到Lock Record的值和锁表示00了。

轻量级锁升级到重量级锁

当锁升级为轻量级锁之后,如果依然有新线程过来竞争锁,首先新线程会自旋尝试获取锁,尝试到一定次数(默认10次)依然没有拿到,锁就会升级成重量级锁。

那么为什么这么设计?

一般来说,同步代码块内的代码应该很快就执行结束,这时候线程B 自旋一段时间是很容易拿到锁的,但是如果不巧,没拿到,自旋其实就是死循环,很耗CPU的,所以就直接转成重量级锁咯,这样就不用了线程一直自旋了。减少CPU的消耗

升级到了重量级锁后,可以看到对象头中只会存在一个ObjectPointer了。

那么下面再来看下这个对象:

下图详细介绍重要变量的作用

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;   // 重入次数(syncronized支持可重入)
    _waiters      = 0,   // 等待线程数
    _recursions   = 0;  
    _object       = NULL;
    _owner        = NULL;  // 当前持有锁的线程
    _WaitSet      = NULL;  // 调用了 wait 方法的线程被阻塞 放置在这个集合中
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 等待锁 处于block的线程的集合 有资格成为候选资源的线程
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

对象关联的 ObjectMonitor 对象有线程内部竞争锁的机制,在多线程场景下,因为抢不到锁而且自旋失败的情况下,将会进入到WaitSet队列中来,等待其他的线程执行Object.notify() or Object.notifyAll() 将该线程唤醒。然后该线程又进入到EntryList中来。

这段解释来自Thread类对WAITING状态的描述:

A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
    
A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.join() is waiting for a specified thread to terminate.    

如下图所示:

因为在ObjectMonitor中没有体现出来ContentionList,首先说明下ContentionList集合的由来。

JVM 每次从Waiting Queue 的尾部取出一个线程放到OnDeck作为候选者,但是如果并发比较高,Waiting Queue会被大量线程执行CAS操作,为了降低对尾部元素的竞争,将Waiting Queue 拆分成ContentionList 和 EntryList 二个队列, JVM将一部分线程移到EntryList 作为准备进OnDeck的预备线程。另外说明几点:

  • 1、所有请求锁的线程首先被放在ContentionList这个竞争队列中;
  • 2、Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
  • 3、任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
  • 4、当前已经获取到所资源的线程被称为 Owner;
  • 5、处于 ContentionList、EntryList中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的);。而WaitSet处于Waitting状态 ,

因为这里需要调用内核函数,设计到了用户态到内核态的切换,导致效率低下。所以这是JDK升级的原因之一。

JDK6之前的实现逻辑

  • 1、当有两个线程t1、线程t2都要开始给 money变量 + 钱,要进行操作的时候 ,发现方法上加了synchronized锁,这时线程调度到t1线程执行,t1线程就抢先拿到了锁。拿到锁的步骤为:
    • ObjectMonitor 中的 _owner设置成 t1线程;
    • 将 mark word 设置为 ObjectMonitor对象地址,锁标志位改为10;【此刻存在锁竞争,锁升级】
    • 将t2线程阻塞放到 ContentionList 队列;
    • 然后线程t1执行完临界区代码之后,就会将锁释放掉,然后唤醒阻塞着的线程,线程t2从ContentionList 进入到entrySet中,然后进入到OnDesk中来,获取得到锁之后,执行代码。
为什么要进行升级?

在JDK6之前,syncronzied的实现逻辑是依赖底层操作系统的 mutex 相关指令实现,加锁解锁需要在用户态和内核态之间切换,性能损耗非常明显。

但是Sun公司研究人员发现,大多数对象的加锁和解锁都是在特定的线程中完成。也就是出现线程竞争锁的情况概率比较低。他们做了一个实验,找了一些典型的软件,测试同一个线程加锁解锁的重复率,如下图所示,可以看到重复加锁比例非常高。早期JVM 有 19% 的执行时间浪费在锁上。

Thin locks are a lot cheaper than inflated locks, but their performance suffers from the fact that every compare-and-swap operation must be executed atomically on multi-processor machines, although most objects are locked and unlocked only by one particular thread.

It was reported that 19% of the total execution time was wasted by thread synchronization in an early version of Java virtual machine。

也就是说,对于大多数并发场景下,导致的锁竞争并不是这么剧烈。

在轻量级锁中就可以解决问题。

描述一种情况:t1线程持有锁执行完临界区代码之后,而t2线程此时才刚刚进来,然后执行代码。

从时间轴上来看,t1线程和t2线程并没有并发。

但是因为类似情况有点多,而获取得到锁需要利用操作系统的内置函数,进行用户态到内核态切换,导致了性能上的降低。

所以研究人员决定来进行升级。因此JDK 6 之后做了改进,引入了偏向锁和轻量级锁

测试waitset代码

来演示一下syncronzied关键字中的waitset集合中的线程顺序。

public class SyncronziedWaitSetTestOne {
    private final static Logger logger = LoggerFactory.getLogger(SyncronziedWaitSetTestOne.class);
    static List<Thread> threadList = new ArrayList<>();
    static Object lock = new Object();
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                synchronized (lock) {
                    logger.info("thread execure");
                        // 这里无意义的睡眠,只是想让其在控制台上打印时好看一点
                    try {
                        TimeUnit.MICROSECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "t" + i);
            threadList.add(thread);
        }
        logger.info("获取得到锁的顺序应该是正序0-9");

        synchronized (lock){
            for (Thread thread : threadList) {
                logger.info("{}-->启动顺序,正序0-9",thread.getName());
                thread.start();
                try {
                    // 为什么满足能够顺序调用start方法!也就是说,控制线程进入到syncronzied中的顺序
                    // 但是具体取决于是哪个线程被OS调用,取决于OS
                    TimeUnit.MICROSECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

世界看输出结果:

这里说明了waitset中是先进后出的。先进去的反而获取不到锁,而后出进去的反而先执行。

那么下面再次来证明一下这里的原理。

public class SyncronziedWaitSetTestTwo {
    private final static Logger logger = LoggerFactory.getLogger(SyncronziedWaitSetTestTwo.class);
    static Object lock = new Object();
    static volatile boolean flag = false;
    public static void main(String[] args) throws InterruptedException {

        new Thread(()->{
            synchronized (lock){
                while (!flag){
                    logger.info("因为条件不满足而释放锁");
                    try {
                        lock.wait();
                        logger.info("被唤醒了,再次检查是否满足条件");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                logger.info("条件满足,执行临界区代码");
            }
        },"tom").start();

        // 为了满足先进入到waitset集合中来 
        TimeUnit.MICROSECONDS.sleep(10);
        synchronized (lock){
            for (int i = 0; i < 5; i++) {
                new Thread(()->{
                    synchronized (lock){
                        logger.info("当前当前名称是:{}",Thread.currentThread().getName());
                    }
                },"normal"+i).start();
                TimeUnit.MICROSECONDS.sleep(1);
            }
            flag = true;
            lock.notifyAll();
             logger.info("打印输出顺序");
        }
    }
}

我在这里做了一个大胆的假设(因为没有看过底层C++实现)

我觉得在ContentionList 中应该是顺序进入的,但是在waitset中应该是采用了头插入且遍历的时候,也是从头部先获取得到线程。

那么再来总结一下,其中的状态转变:

如下所示:

五、syncronzied是非公平锁

和lock非公平锁有点类似

主要有以下二点原因:

  • 1、Synchronized 在线程竞争锁时,首先做的不是直接进ContentionList 队列排队,而是尝试自旋获取锁(可能ContentionList 有别的线程在等锁),如果获取不到才进入 ContentionList,这明显对于已经进入队列的线程是不公平的;
  • 2、另一个不公平的是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。这明显对于已经进入队列的线程是不公平的;

六、锁的重偏向和批量撤销

对于一个类的对象来说,如果锁撤销次数太多,那么JVM会认为存在着问题。

偏向锁撤销的过程中:首先释放锁,然后将锁还原成无锁,然后复制对象头中的信息到对象头中去,然后升级成轻量级锁

当JVM撤销太多次了之后,JVM认为类设计存在着问题,将会进行批量撤销。

轻量级锁撤销到一定次数(默认20)之后,剩下的就不再撤销了,就再次重新偏向即可

JVM默认是>=20次的时候,会使用重偏向。但是20次以下不包含20,依然会进行锁的膨胀,先撤销,然后升级成轻量级锁

但是如果此时此刻,还有另外一个线程仍然发生了锁的撤销,在达到40次后,JVM认为严重出现了问题。直接将锁升级成轻量级锁。

写一段代码来表示:

public class BatchRevokeTestOne {
    private final static Logger logger = LoggerFactory.getLogger(BatchRevokeTestOne.class);

    public static void main(String[] args) throws InterruptedException {
        List<Abc> abcList = new ArrayList<>();
        // 为了让对象头中呈现出无锁可偏向
        TimeUnit.SECONDS.sleep(5);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 200; i++) {
                Abc abc = new Abc();
                // 让abc对象都偏向t1线程
                synchronized (abc) {
                    abcList.add(abc);
                }
            }

            // 为了不让线程ID复用
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");
        t1.start();
        // 为了让线程t1先启动
        TimeUnit.MICROSECONDS.sleep(10);

        Thread t2 = new Thread(() -> {
            int cout = 1;
            for (Abc abc : abcList) {
                synchronized (abc) {
                    // 应该是轻量级锁了!但是依然是偏向锁前几个是轻量级锁,而后面的将成为偏向锁
                    if (cout == 15) {
                        logger.info("当前对象是list集合中的第{}个对象,应该是轻量锁000,对应的对象头是:{}", cout + 1, ClassLayout.parseInstance(abc).toPrintable());
                    }
                    // 20之后的就应该是批量锁撤销
                    if (cout == 20) {
                        logger.info("当前对象是list集合中的第{}个对象,应该是偏向锁101,对应的对象头是:{}", cout, ClassLayout.parseInstance(abc).toPrintable());
                        return;
                    }
                }
                cout++;
            }
        }, "t2");
        t2.start();
        t2.join();
        logger.info("再次将当前类的对象进行偏向");
        // 但是如果此时锁对象仍然有偏向其他线程了的,那么就会导致40之后的升级到轻量级锁
        // 批量撤销
        new Thread(() -> {
            int count = 1;
            for (int i = 0; i < abcList.size(); i++) {
                Abc abc = abcList.get(i);
                synchronized (abc){
                    if (count==15){
                        logger.info("count为:{},应该是偏向锁101,对应的对象头是:{}", count,ClassLayout.parseInstance(abc).toPrintable());
                    }
                    if (count==25){
                        logger.info("count为:{},应该是轻量级锁000,对应的对象头是:{}",count, ClassLayout.parseInstance(abc).toPrintable());
                    }
                    if (count==70){
                        logger.info("count为:{},应该是轻量级锁000,对应的对象头是:{}",count, ClassLayout.parseInstance(abc).toPrintable());
                        return;
                    }
                }
                count++;
            }
        }, "t3").start();
    }
}

日志打印:

2022-10-23 15:47:20.714 [t2] INFO  c.g.s.syncronizedbatchrevoke.BatchRevokeTestOne - 当前对象是list集合中的第16个对象,应该是轻量锁000,对应的对象头是:com.guang.syncronized.syncronizedbatchrevoke.Abc object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           78 f1 fd 1e (01111000 11110001 11111101 00011110) (519958904)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           34 6b 01 f8 (00110100 01101011 00000001 11111000) (-134124748)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2022-10-23 15:47:20.720 [t2] INFO  c.g.s.syncronizedbatchrevoke.BatchRevokeTestOne - 当前对象是list集合中的第20个对象,应该是偏向锁101,对应的对象头是:com.guang.syncronized.syncronizedbatchrevoke.Abc object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 f9 a4 1d (00000101 11111001 10100100 00011101) (497350917)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           34 6b 01 f8 (00110100 01101011 00000001 11111000) (-134124748)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2022-10-23 15:47:20.720 [main] INFO  c.g.s.syncronizedbatchrevoke.BatchRevokeTestOne - 再次将当前类的对象进行偏向
2022-10-23 15:47:20.722 [t3] INFO  c.g.s.syncronizedbatchrevoke.BatchRevokeTestOne - count为:15,应该是偏向锁101,对应的对象头是:com.guang.syncronized.syncronizedbatchrevoke.Abc object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           a8 f3 ed 1e (10101000 11110011 11101101 00011110) (518910888)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           34 6b 01 f8 (00110100 01101011 00000001 11111000) (-134124748)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2022-10-23 15:47:20.723 [t3] INFO  c.g.s.syncronizedbatchrevoke.BatchRevokeTestOne - count为:25,应该是轻量级锁000,对应的对象头是:com.guang.syncronized.syncronizedbatchrevoke.Abc object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 91 7b 1e (00000101 10010001 01111011 00011110) (511414533)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           34 6b 01 f8 (00110100 01101011 00000001 11111000) (-134124748)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2022-10-23 15:47:20.723 [t3] INFO  c.g.s.syncronizedbatchrevoke.BatchRevokeTestOne - count为:70,应该是轻量级锁000,对应的对象头是:com.guang.syncronized.syncronizedbatchrevoke.Abc object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 91 7b 1e (00000101 10010001 01111011 00011110) (511414533)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           34 6b 01 f8 (00110100 01101011 00000001 11111000) (-134124748)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到在20次及其之后,是可以进行偏向的。

但是在进行20-40之间进行锁升级的时候,没有测试出来对应的效果。

写到最后

我总结一下全文内容

0、生产上既然用了syncronized关键字,那么一定是存在并发场景的。一般来说是需要禁用掉偏向锁的。

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

// 如果有大并发场景下,直接启用重量级锁
    
-XX:+UseHeavyMonitors //设置重量级锁

但是重量级锁其实现原理是通过操作系统调用MutexLock来保证竞态资源的互斥,所以会产生用户态与内核态的切换;
    
操作系统中的程序,一般都是在用户态下运行的。当程序需要借助操作系统来完成一些自己无法完成的操作时,便会从用户态切换到内核态,如果频繁切换,这样的开销还是很大的 
    
CAS:
    比较实际内存值与预期内存值,相同则更新内存值,否则什么都不做(这是Java调用的一串cpu指令,不具体讨论)
	更新失败的原因就是各个线程cpu缓存的可见性问题

1、syncronzied为什么可以保证在并发场景下线程同步;

2、jdk6之前syncronized关键字的实现过程以及为什么做优化;

3、jdk6之后的syncronized关键字的四种状态以及状态之间的切换;

4、几种特殊情况的状态转换;

5、查看对象头中的几种特殊情况;

6、waitset的顺序性问题;

7、锁的撤销和重偏向问题;

禁用掉偏向锁,之后,加锁之后的代码:

public class SyncronizedFobbindonBiasTest {

    private final static Logger logger = LoggerFactory.getLogger(SyncronizedFobbindonBiasTest.class);

    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        Object lock = new Object();
        logger.info("无锁不可偏向,001,打印对象头:{}", ClassLayout.parseInstance(lock).toPrintable());
        synchronized (lock){
            logger.info("偏向锁,000,打印对象头:{}", ClassLayout.parseInstance(lock).toPrintable());
        }
    }
}

打印输出结果:

2022-10-23 16:13:41.737 [main] INFO  c.g.s.s.SyncronizedFobbindonBiasTest - 无锁不可偏向,打印对象头:java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2022-10-23 16:13:41.741 [main] INFO  c.g.s.s.SyncronizedFobbindonBiasTest - 偏向锁,打印对象头:java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           88 f4 20 03 (10001000 11110100 00100000 00000011) (52491400)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
posted @ 2022-10-20 11:53  写的代码很烂  阅读(15)  评论(0编辑  收藏  举报