Java 杂谈

Java synchronized 关键字原理学习

2019-03-10  本文已影响4人  jwfy

在上一篇Java 线程 和 锁 基础知识已经介绍了Java中的线程和锁的一些基本概念,现在就来学习和了解下Java的内置锁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 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();
    }
}

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

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、参考链接

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

微信公众号
上一篇 下一篇

猜你喜欢

热点阅读