java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java synchronized底层实现原理

Java并发编程之synchronized底层实现原理分析

作者:寒山道杳

这篇文章主要介绍了Java并发编程之synchronized底层实现原理,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

一、为什么出现synchronized

对于程序员而言,不管是在平常的工作中还是面试中,都会经常用到或者被问到synchronized。在多线程并发编程中,synchronized早已是元老级的角色了,很多人都称其为重量级锁,但是随着Java SE 1.6对其进行各种优化之后,便显得不再是那么的重了。

也正是因为多线程并发的出现,便产生了线程安全这样的问题,对于线程安全的主要原因如下:

而对于解决这样的一个问题的办法是:同一时刻有且只有一条线程在操作共享数据,其他线程必须等待该线程处理完数据后再对共享数据进行操作

此时便产生了互斥锁,互斥锁的特性如下:

对于Java而言,synchronized关键字满足了以上的要求。

二、实现原理

首先我们要知道synchronized锁的不是代码,锁的是对象。

根据获取的锁的分类:获取对象锁和获取类锁:

获取对像锁的两种方法

获取类锁的两种方法

对象锁和类锁的总结:有线程访问对象的同步代码块时,另外的线程可以访问该兑对象的非同步代码块

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或者抛出异常时必须释放锁,那么锁存在哪里呢?

我们先来看一段代码:

public class SyncBlockTest {
    public void syncsTask() {
        synchronized (this) {
            System.out.println("Hello");
        }
    }
 
    public synchronized void syncTask() {
        System.out.println("Hello Baby");
    }
}

在使用javac工具把上面代码变异成class,然后使用javap工具查看编译好的class文件,如下:

  public com.interview.javabasic.thread.SyncBlockTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
 
  public void syncsTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String Hello
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
      LineNumberTable:
        line 10: 0
        line 11: 4
        line 12: 12
        line 13: 22
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [ class com/interview/javabasic/thread/SyncBlockTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
 
  public synchronized void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String Hello Baby
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 16: 0
        line 17: 8
}

同步代码块:从上面的字节码中可以看出,同步语句块的实现是使用monitorentermonitorexit指令的,monitorenter指向同步代码块的开始位置,它首先去获取PrintStream这个类,然后传入“Hello”这个参数,然后再调用PrintStream中的println()方法去打印,monitorexit指明同步代码块的结束位置,当执行到monitorenter时,当前线程将试图获取对象锁所对应的monitor的持有权。

同步方法:从上面代码的syncTask()方法字节码中看,这里面并没monitorenter和monitorexit,且字节码较短,其实这里方法的同步是隐式的,是无需通过字节码指令控制,在上面可以看到一个“ACC_SYNCHRONIZED”这样的一个访问标志,用来区分一个方法是否是同步方法。当方法调用时,调用指令将会检查ACC_SYNCHRONIZED是否被设置,如果被设置,当前线程将会持有monitor,然后再执行方法,最后不管方法是否正常完成都会释放monitor。

三、实现synchronized的基础

Java对象头和monitor是实现synchronized的基础,下面将会说说关于Java对象头和monitor。

Java对象头:

hotspot虚拟机中,对象在内存的布局分布分为3个部分:对象头,实例数据,和对齐填充。

对象头的结构如下:

虚拟机位数头对象结构说明
32/64 bitMark Word默认存储对象的hashCode,分代年龄,锁类型,锁标志位等信息
32/64 bitClass Metadata类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类型的数据
32/64 bitArray length

数组的长度(如果当前的对象是数组

mark word 被设计为非固定的数据结构,以便在极小的空间内存储更多的信息,它会根据对象的状态复用自己的存储空间。

例如,在32位的Hotspot虚拟机中,如果对象处于未被锁定的情况下。

那么Mark Word 的32bit空间中有25bit用于存储对象的哈希码、4bit用于存储对象的分代年龄、2bi用于t存储锁的标记位、1bit固定为0,而在其他的状态下(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储结构如下:

存储内容标志位状态
对象哈希吗、对象分代年龄01未锁定
指向锁记录的指针00轻量级锁定
指向重量级锁的指针10膨胀(重量级锁定)
空,不需要记录信息11GC标记
偏向线程ID、偏向时间戳、对象分代年龄01可偏向

monitor:

每个Java对象天生就自带了一把看不见的锁,它可以视为是一种同步工具或者是一种同步机制,monitor还是线程私有的数据结构,每一个线程都有一个可用monitor 列表,同时还有一个全局的可用列表,如上面所说每一个被锁住的对象都会持有一个monitor。

monitor结构如下:

四、锁优化

Java6以后,对锁进行了大量的优化,例如:AdaptiveSpinning(自适应自旋)、Lock Eliminate(锁消除)、Lock Coarsening(锁粗化)、Lightweight Locking(轻量级锁)、Biased Locking(偏向锁)等等

自旋锁

定义:所谓的自旋锁就是让没有获取到锁的线程继续等待一会儿,但不放弃CPU的执行时间,这是的等一会和不放弃CPU的时间即是自旋锁

自适应自旋锁

锁消除

锁消除是对锁更彻底的优化,JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁

public class StringBufferWithoutSync {
    public void add(String str1, String str2) {
        //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
        //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }
 
    public static void main(String[] args) {
        StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
        for (int i = 0; i < 1000; i++) {
            withoutSync.add("aaa", "bbb");
        }
    }
 
}

例如Java中的StringBuffer 它是线程安全的,由于其append()方法只会在内部使用的,也就不可能被其他线程引用,因此对于这个变量sb而言,便不属于共享资源了,JVM会自动消除内部的锁。

锁粗化

原则上我们都知道,在加同步锁的时候,尽可能将同步块的作用范围先知道尽量小的范围,及只在共享数据的实际工作范围中进行同步,这样是为了需要同步的数量尽可能的变小,在存在锁同步竞争中,使得等待锁的时间减小。

上述情况,大部分时候是正确的,但是如果存在一系列频繁的操作,对同一个对象反复的加锁、解锁, 甚至加锁时是在循环体中操作的,这样即使没有线程竞争,频繁的进行互斥锁操作,也是导致不必要的性能开销。

对于解决这样的问题,我们只有尽可能的扩大加锁的范围,例如下面的循环100次append,JVM会自己检测到这样的一个问题,就会将加锁的次数减至一次。

    public static String copyString(String target){
        int i = 0;
        StringBuffer sb = new StringBuffer();
        while (i<100){
            sb.append(target);
        }
        return sb.toString();
    }

五、synchronized锁的状态

synchronized锁有四种状态分别为:无锁、偏向锁、轻量级锁、重量级锁

锁的膨胀方向:无锁——>偏向锁——>轻量级锁——>重量级锁

偏向锁

作用:减少同一线程获取锁的代价

引入偏向锁是因为大多数情况下,锁并不存在多线程竞争,总是由同一线程多次获得

获取锁

释放锁

偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。

其步骤如下:

轻量级锁

获取锁

释放锁

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢;

重量级锁

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文