美文网首页基础原理javaandroid
Java多线程之synchronized实现原理

Java多线程之synchronized实现原理

作者: 芝士就是力量007 | 来源:发表于2019-04-19 11:54 被阅读233次

    一、synchronized简介

    在并发编程中多个线程同时操作同一个资源,极易导致错误数据的产生。因此为了解决这个问题,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行。

    在Java中,关键字synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),这点确实也是很重要的。

    二、synchronized应用方式

    synchronized主要有以下三种使用方式

    1. 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;

    2. 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;

    3. 作用于代码块,这需要指定加锁的对象,对所给的指定对象加锁,进入同步代码前要获得指定对象的锁。

    1、作用于实例方法

    public class SynchronizedMethodTest implements Runnable {
    
        private int i = 0;
        private static int TOTAL = 1000;
    
        public synchronized void add() {
            i++;
        }
    
        @Override
        public void run() {
            for (int j = 0; j < TOTAL; j++) {
                add();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            SynchronizedMethodTest s = new SynchronizedMethodTest();
            Thread a = new Thread(s, "线程A");
            Thread b = new Thread(s, "线程B");
            a.start();
            b.start();
            a.join();
            b.join();
            System.out.printf("i=%s", s.i);
        }
    
    }
    /**
     * 输出结果: i=2000
     */
    

    2、作用于静态方法

    package com.dragon.thread.sync;
    
    public class SynchronizedStaticMethodTest implements Runnable {
    
        private static int i = 0;
        private static int TOTAL = 1000;
    
        public synchronized static void add() {
            i++;
        }
    
        @Override
        public void run() {
            for (int j = 0; j < TOTAL; j++) {
                add();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            SynchronizedStaticMethodTest s1 = new SynchronizedStaticMethodTest();
            SynchronizedStaticMethodTest s2 = new SynchronizedStaticMethodTest();
            Thread a = new Thread(s1, "线程A");
            Thread b = new Thread(s2, "线程B");
            a.start();
            b.start();
            a.join();
            b.join();
            System.out.printf("i=%s", i);
        }
    
    }
    /**
     * 输出结果: i=2000
     */
    

    3、作用于代码块

    public class SynchronizedBlockTest implements Runnable {
    
        private int i = 0;
        private static int TOTAL = 1000;
    
        public void add() {
            synchronized (this) {
                i++;
            }
        }
    
        @Override
        public void run() {
            for (int j = 0; j < TOTAL; j++) {
                add();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            SynchronizedBlockTest s = new SynchronizedBlockTest();
            Thread a = new Thread(s, "线程A");
            Thread b = new Thread(s, "线程B");
            a.start();
            b.start();
            a.join();
            b.join();
            System.out.printf("i=%s", s.i);
        }
    
    }
    /**
     * 输出结果: i=2000
     */
    

    三、synchronized底层原理

    Java 虚拟机中的同步Synchronization基于进入和退出管程Monitor对象实现, 无论是显式同步(有明确的monitorentermonitorexit指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被synchronized修饰的同步方法。同步方法 并不是由monitorentermonitorexit指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED标志来隐式实现的,关于这点,稍后详细分析。下面先来了解一个概念Java对象头,这对深入理解synchronized实现原理非常关键。

    1、理解Java对象头与Monitor

    在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。


    JAVA对象实例结构

    对象头

    HotSpot虚拟机的对象头包括两部分信息:

    1. markword
      第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit64bit,官方称它为MarkWord
    2. klass
      对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.
    3. 数组长度(只有数组对象有)
      如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.

    实例数据

    实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。

    对齐填充

    第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

    32位虚拟机在不同状态下markword结构如下图所示


    markword结构

    其中轻量级锁和偏向锁是Java 6 对synchronized锁进行优化后新增加的,稍后我们会简要分析。这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

    ObjectMonitor() {
        _header       = NULL;//markOop对象头
        _count        = 0;
        _waiters      = 0,//等待线程数
        _recursions   = 0;//重入次数
        _object       = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
        _owner        = NULL;//初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
        _WaitSet      = NULL;//处于wait状态的线程,会被加入到wait set;
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ;//处于等待锁block状态的线程,会被加入到entry set;
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
      }
    
    

    ObjectMonitor中有两个队列,_WaitSet_EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入_EntryList集合,当线程获取到对象的monitor后进入_Owner区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用wait()方法,将释放当前持有的monitorowner变量恢复为null,count自减1,同时该线程进入WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示

    监视器
    由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析),ok~,有了上述知识基础后,下面我们将进一步分析synchronized在字节码层面的具体语义实现。

    2、同步方法的实现原理

    使用javap -v SynchronizedMethodTest.class反编译

    /**
     * 此处省略大段代码
     */
    
      public synchronized void add();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_SYNCHRONIZED
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: dup
             2: getfield      #2                  // Field i:I
             5: iconst_1
             6: iadd
             7: putfield      #2                  // Field i:I
            10: return
    /**
     * 此处省略大段代码
     */
    }
    SourceFile: "SynchronizedMethodTest.java"
    

    3、同步代码块的实现原理

    使用javap -v SynchronizedBlockTest.class反编译

    /**
     * 此处省略大段代码
     */
      public void add();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=3, args_size=1
             0: aload_0
             1: dup
             2: astore_1
             3: monitorenter                    //申请获得对象的内置锁
             4: aload_0
             5: dup
             6: getfield      #2                  // Field i:I
             9: iconst_1
            10: iadd
            11: putfield      #2                  // Field i:I
            14: aload_1
            15: monitorexit                       //释放对象内置锁
            16: goto          24
            19: astore_2
            20: aload_1
            21: monitorexit                      //出现异常,释放对象内置锁
            22: aload_2
            23: athrow
            24: return
    /**
     * 此处省略大段代码
     */
    }
    SourceFile: "SynchronizedBlockTest.java"
    

    从上述指令我们可以得出以下结论:

    1. 同步代码块是使用monitorentermonitorexit指令实现的,会在同步块的区域通过监听器对象去获取锁和释放锁,从而在字节码层面来控制同步scope
    2. 同步方法和静态同步方法依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。JVM根据该修饰符来实现方法的同步。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

    结束

    相关文章

      网友评论

        本文标题:Java多线程之synchronized实现原理

        本文链接:https://www.haomeiwen.com/subject/mcfnwqtx.html