Java synchronized 关键字原理学习

更多并发相关内容,查看==>Java 线程&并发学习目录

在上一篇Java 线程 和 锁 基础知识已经介绍了Java中的线程和锁的一些基本概念,现在就来学习和了解下Java的内置锁synchronized。具体包含如下几个点:

  • 类锁和对象锁的用法以及同异;
  • synchronized的优化,通过对象的头部结构了解和学习偏向锁、轻量级锁、重量级锁;
  • 不同的synchronized指令差异以及其说明。

synchronized是Java原生的悲观锁、具有可重入的特性,可保证共享数据的线程安全。使用时需要和具体的对象或类关联绑定。JDK1.5开始,为了提高效率,在不同的竞争冲突情境下,synchronized也会出现从无锁->偏向锁->轻量级锁->重量级锁的单向锁转变。

1、synchronized 使用

synchronized可以在对象、类以及代码块等地方使用,只要不出现活跃性以及发布不安全等问题,一般情况下可以确保单JVM上的共享数据安全。

对象使用

public class SynchronizedDemo {
    
    private Object OBJECT = new Object();
    // 锁标识,谁占有该对象就表示占据该锁了

    public void testFunction() {
        System.out.println(Thread.currentThread().getName() + " testFunction");
    }

    public synchronized void testSynchronizedFunction() {
        // 对象方法锁
        System.out.println(Thread.currentThread().getName() + " testSynchronizedFunction");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void testSynchronizedObject() {
        // 对象代码块锁
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " testSynchronizedObject");
            try {
                Thread.sleep(1000 * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void testSynchronizedDifferentObject() {
        // 对象代码块锁,关联的是OBJECT这个对象
        synchronized (OBJECT) {
            System.out.println(Thread.currentThread().getName() + " testSynchronizedDifferentObject");
            try {
                Thread.sleep(1000 * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void testSynchronizedObjectAgain() {
        // 对象代码块锁,重入操作
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " testSynchronizedObjectAgain");
            testSynchronizedFunction();
        }
    }
}

再看看下面的测试demo的效果如何

public class SynchronizedTest {

    public static void testObject() {

        // 同一个demo,使用对象锁的时候,只有不是执行同一个

        SynchronizedDemo demo = new SynchronizedDemo();

        Runnable runnable1 = () -> demo.testSynchronizedFunction();
        Runnable runnable2 = () -> demo.testSynchronizedObject();
        Runnable runnable3 = () -> demo.testSynchronizedFunction();

        new Thread(runnable1, "run1").start();
        new Thread(runnable2, "run2").start();
        // new Thread(runnable3, "run3").start();
    }

    public static void testObject1() {

        // 不同的demo,使用对象锁的时候,各自无影响
        // 因为锁住的是对象,不同的对象之间是隔离开的

        SynchronizedDemo demo = new SynchronizedDemo();
        SynchronizedDemo demo1 = new SynchronizedDemo();

        Runnable runnable = () -> demo.testSynchronizedFunction();
        Runnable runnable1 = () -> demo1.testSynchronizedFunction();

        new Thread(runnable, "run").start();
        new Thread(runnable1, "run1").start();
    }

    public static void testObjectAgain() {

        // 同一个demo,使用对象锁后,可以再重入

        SynchronizedDemo demo = new SynchronizedDemo();

        Runnable runnable1 = () -> demo.testSynchronizedObjectAgain();
        Runnable runnable2 = () -> demo.testSynchronizedFunction();

        new Thread(runnable2, "run2").start();
        new Thread(runnable1, "run1").start();
    }
    
    public static void main(String[] args) {
        SynchronizedTest.testObject();
        //SynchronizedTest.testObject1();
        //SynchronizedTest.testObjectAgain();
    }
}

如上main方法中的不同方法调用,输出的内容基本差不多,主要是观察其睡眠暂停的时间

  • public synchronized:写在普通的方法上的就表示为「普通同步方法」,他是和当前对应的对象绑定在一起的,不同的线程在调用同一个对象的该方法时会发生竞争冲突,不同对象则不会出现竞争
  • synchronized (this):写在代码块中的,整体而言和普通方法没有本质的区别,只是和普通方法相比,锁粒度更细一些,效率(可能)更高些
  • synchronized (Object):写在代码块中的,这个锁就脱离了当前对象绑定关系而是和 Object对象 关联绑定,几个不同的类甚至可以通过传入同一个Object实现不同对象见的锁控制,此方法在很多源码中也被大量使用,也建议使用
  • 最后又提及到了可重入,一个线程在获取到锁后,再获取该锁则可以直接获取。不过需要控制好可重入的顺序,如果顺序没有控制好,再加上资源分配不恰当,会引发死锁的危险(notify方法也会引发死锁)

类使用

public class SynchronizedDemo {

    public synchronized static void testStaticFunction() {
        // 类静态方法锁
        System.out.println(Thread.currentThread().getName() + " testStaticFunction");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void testClass() {
        synchronized (SynchronizedDemo.class) {
            System.out.println(Thread.currentThread().getName() + " testClass");
            try {
                Thread.sleep(1000 * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

demo的测试用例

public class SynchronizedTest {

    public static void testClass() {

        // 同一个类,使用类锁

        SynchronizedDemo demo = new SynchronizedDemo();

        Runnable runnable1 = () -> SynchronizedDemo.testStaticFunction();
        Runnable runnable2 = () -> demo.testClass();

        new Thread(runnable2, "run2").start();
        new Thread(runnable1, "run1").start();
    }

    public static void testClass2() {
        // 一个类锁 一个对象锁,两者不会起冲突

        SynchronizedDemo demo = new SynchronizedDemo();

        Runnable runnable1 = () -> demo.testClass();
        Runnable runnable2 = () -> demo.testSynchronizedObject();

        new Thread(runnable1, "run1").start();
        new Thread(runnable2, "run2").start();
    }
}
  • public synchronized static:静态方法,和当前的类绑定关联,同一个类在调用类似方法时,会出现竞争冲突
  • synchronized (XXX.class)绑定的是指定的类XXX.class,存在几个不同的对象,方法中使用同一个类的情况
  • 同一个对象的类锁和对象锁之间不会出现竞争冲突

2、synchronized 优化

JVM结构分为程序计数器虚拟机栈本地方法栈方法区以及,而创建的对象信息则是存放在堆中

JVM结构

虚拟机栈:对象的方法调用临时申请的数据存放点、方法接口等信息,A方法调用B方法,再调用C方法,这些关系就是存放在虚拟机栈中的,日常所说的打印出错误的堆栈信息也就存在栈中
本地方法栈:方法调用的本地native方法
方法区:线程共享的区域(永生代),存储类加载器加载的类信息、常量、静态变量等信息,例如static和final
堆:对象实例存放点(包含新生代和老年代),新建的对象信息都是存放在堆中的
程序计数器:可以认为是下一条需要执行的指令指示器

对象堆的组成区域如下图,其中数据实例是类的具体内容,而对齐填充则是JVM的约定,所有对象的大小必须是8字节的倍数,例如某个对象包含对象头是63个字节,那么对齐填充则是1个字节。而和synchronized最密切的是对象头中的MarkWord 标记字段。

image

在标记字段值也包含了很多内容,例如HashCode,锁标志位等等。具体如下图在不同的锁情况下,64位的MarkWord内容。随着竞争的加大,synchronized会从无锁->偏向锁->轻量级锁->重量级锁转变的

image

该图来源自:https://blog.csdn.net/scdn_cp/article/details/86491792

  • 无锁:锁对象刚刚创建,没有竞争,偏向锁标识位为0,锁状态是01
  • 偏向锁:出现一个线程竞争,则直接把当前的线程信息记录到当前对象中,并且只偏爱,同时偏向锁标识位是为1
  • 轻量级锁:出现大于等于2个线程竞争时,就不再偏爱了,锁从偏向锁升级为轻量级锁,并记录下竞争成功的线程记录,锁状态是00
  • 重量级锁:竞争更加严重,锁升级为重量级锁(也叫同步锁),现在MarkWord中指向的不再是线程信息,而是Monitor监视器信息,同时锁状态是10
  • 被GC标记的对象:待回收了,只要下一次GC不再被引用就会被回收掉的,锁状态是11
  • 监视器Monitor:和每一个对象都有一根无形的线关联着,监视器记录着关联的对象、持有的线程、阻塞的线程信息等

3、synchronized 底层实现

java 文件通过编译后生成了class文件,再使用javap -verbose XXXX文件输出字节码,为了便于说明问题新加非常小的demo文件测试一下

public class SimpleClass {

    private Object obj = new Object();

    public synchronized void run() {
        // 同步方法
    }

    public void run1() {
        // 同步代码块
        synchronized (this) {}
    }

    public void run2() {
        // 同步指定的对象
        synchronized (obj) {}
    }

    public void run3() {
        // 同步指定的类
        synchronized (SimpleClass.class) {}
    }
}

其中run() 和 run1() 从功能上来说是完全一致的,都是绑定当前对象,查看相关指令如下代码(除去了无关指令)

  public synchronized void run();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
        ....

  public void run1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_1
         5: monitorexit
         6: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit
        12: aload_2
        13: athrow
        14: return
        .....

虽然这两者的功能完全一致,但是具体的底层实现却不一样,同步方法是直接添加了flagACC_SYNCHRONIZED标识其是一个同步的方法,而同步代码块则是使用了1条monitorenter指令和2条monitorexit指令,其中有2条monitorexit的原因主要是编译器自动产生一个异常处理器,后面一个monitorexit就是在异常处理结束后释放monitor的

  public void run2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: getfield      #3                  // Field obj:Ljava/lang/Object;
         4: dup
         5: astore_1
         6: monitorenter
         7: aload_1
         8: monitorexit
         9: goto          17
        12: astore_2
        13: aload_1
        14: monitorexit
        15: aload_2
        16: athrow
        17: return
        ...
        
  public void run3();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #4                  // class new2019/Synchronized/SimpleClass
         2: dup
         3: astore_1
         4: monitorenter
         5: aload_1
         6: monitorexit
         7: goto          15
        10: astore_2
        11: aload_1
        12: monitorexit
        13: aload_2
        14: athrow
        15: return
        ....

而和run1()相比,run2()中的指令就仅仅只多了一句指令1: getfield #3,获取被管理的对象object,用来替换默认的this,run3()的指令更加简单直接就是0: ldc #4,把#4(SimpleClass.class)推送到了当前的栈顶

这样看来使用synchronized(XX)的方法从底层指令而言没有太大的差异,就是加载了不同的数据进行处理,有的是当前对象,有的是指定对象,有的是指定的类信息,但是因为加载的数据不同,使得持有的锁也是完全不一样的,类对象会持有关联一个监视器,类Class也会持有一个监视器

关于Monitor和MarkWord的C++底层实现原理可以看看HostSpot源码

4、参考链接

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

推荐阅读更多精彩内容