由浅到深分析Synchronized,彻底理解Synchronized底层实现原理

这篇文章会记录Synchronized的常用使用场景与Synchronized的底层实现原理。虽然我们平时经常会在多线程中使用Synchronized关键字,但可能对于这个我们很熟悉的关键字的底层到底是怎样实现的没有过多关注。作为开发者,既然使用到了,可以试着去一步一步揭开下它的底层面纱。

为什么要使用Synchronized?

首先我们来看下这段代码


public class Demo {
    private static int count=0;
    public /*synchronized*/ static void inc(){
        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(()-> Demo.inc()).start();
        }
        Thread.sleep(3000);
        System.out.println("运行结果"+count);
    }
}

这段代码的运行结果:运行结果970
在这段代码中,首先没有加synchronized关键字,我们使用了循环的方法用1000个线程去访问count这个变量,运行的结果告诉我们,这个共享变量的状态是线程不安全的(我们期望1000次的访问可以得到1000的结果)。要解决这个问题, Synchronized关键字就可以达到目的。

synchronized简介

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

synchronized的基本语法

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

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

我在网上找了张图,大致也对应上面所说的。


Synchronized的使用场景

synchronized原理分析

Java对象头和monitor是实现synchronized的基础!下面就这两个概念来做详细介绍。

关于monitor,再来看一个小demo

package com.thread;

public class Demo1{

    private static int count = 0;

    public static void main(String[] args) {
        synchronized (Demo1.class) {
            inc();
        }

    }
    private static void inc() {
        count++;
    }
}

上面的代码demo使用了synchroized关键字,锁住的是类对象。编译之后,切换到Demo1.class的同级目录之后,然后用javap -v Demo1.class查看字节码文件:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class com/thread/SynchronizedDemo
         2: dup
         3: astore_1
         4: monitorenter       //注意这个
         5: invokestatic  #3                  // Method inc:()V
         8: aload_1
         9: monitorexit    //注意这个
        10: goto          18
        13: astore_2
        14: aload_1
        15: monitorexit   //注意这个
        16: aload_2
        17: athrow
        18: return

线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带 monitor。而monitor是添加Synchronized关键字之后独有的。synchronized同步块使用了monitorenter和monitorexit指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,而执行monitorexit,就是释放monitor的所有权。

对象在内存中的布局

在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。


image

Java对象头

对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。
Klass Point:是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
Mark Word:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键.

加锁时MarkWord可能储存的四种状态

synchronized 锁的升级

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

hotspot 虚拟机的作者经过调查发现,大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。所以基于这样一个概率,是的 synchronized 在JDK1.6 之后做了一些优化,为了减少获得锁和释放锁来的性能开销,引入了偏向锁、轻量级锁的概念。因此大家会发现在 synchronized 中,锁存在四种状态分别是:无锁、偏向锁、轻量级锁、重量级锁; 锁的状态根据竞争激烈的程度从低到高不断升级。

偏向锁的基本原理

偏向锁的获取
前面说过,大部分情况下,锁不仅仅不存在多线程竞争,而是总是由同一个线程多次获得,为了让线程获取锁的代价更低就引入了偏向锁的概念。怎么理解偏向锁呢?当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的 ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了
偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。并且直接把被偏向的锁对象升级到被加了轻量级锁的状态。
对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程有两种情况:

  1. 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向但前线程
  2. 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块在我们的应用开发中,绝大部分情况下一定会存在 2 个以上的线程竞争,那么如果开启偏向锁,反而会提升获取锁的资源消耗。所以可以通过 jvm 参数UseBiasedLocking 来设置开启或关闭偏向锁

这是网上一张很经典的偏向锁流程图


偏向锁流程图

轻量级锁的基本原理

加锁

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

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

自旋锁

轻量级锁在加锁过程中,用到了自旋锁所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于在执行一个啥也没有的 for 循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就能够获得锁了。自旋锁的使用,其实也是有一定的概率背景,在大部分同步代码块执行的时间都是很短的。所以通过看似无异议的循环反而能提升锁的性能。但是自旋必要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。默认情况下自旋的次数是 10 次,可以通过 preBlockSpin 来修改在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。


轻量级锁流程图

重量级锁

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

各种锁的比较

各种锁的比较

总结

JVM在运行过程会根据实际情况对添加了Synchronized关键字的部分进行锁自动升级来实现自我优化。以上就是Synchronized的实现原理和java1.6以后对其所做的优化以及在实际运行中可能遇到的锁升级原理。虽然大家都懂得使用synchronized这个关键字,但我觉得一步一步深入挖掘它的原理实现的过程也是一种乐趣。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容