基础原理javaandroid

Java多线程之synchronized实现原理

2019-04-19  本文已影响233人  芝士就是力量007

一、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对象。

结束

上一篇下一篇

猜你喜欢

热点阅读