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 标记字段。

Java synchronized 关键字原理学习_第1张图片
image

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

Java synchronized 关键字原理学习_第2张图片
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、参考链接

  • https://blog.csdn.net/scdn_cp/article/details/86491792
  • https://baijiahao.baidu.com/s?id=1612142459503895416

本人微信公众号(搜索jwfy)欢迎关注

微信公众号

你可能感兴趣的:(Java synchronized 关键字原理学习)