重学Java——Synchronized底层实现原理

深刻Synchronized底层原理

对于synchronized你们应该都很熟悉,主要做用是在多线程并发时,保证线程访问共享数据时的线程安全。html

它的做用有三点:java

  1. 确保线程互斥的访问同步代码
  2. 保证共享为师的修改及时可见
  3. 有效解决指令重排(synchronized同步中的代码,JVM不会轻易优化重排序)

Synchronized使用

它的用法主要是从两个维度上来区分:安全

  • 根据修饰对象的分类
    • 修饰代码块
      • synchronized(this|object)
      • synchronized(类.class)
    • 修饰方法
      • 修饰非静态方法
      • 修饰静态方法
  • 根据获取的锁来分类
    • 获取对象锁
      • synchronized(this|object)
      • 修改非静态方法
    • 获取类锁
      • synchronized(类.class)
      • 修饰静态方法

1.对象锁

这个对象是新建的,跟其余对象无关:数据结构

public class SynchronizeDemo implements Runnable {

    @Override
    public void run() {
        test1();
    }

    private void test1(){
        System.out.println(Thread.currentThread().getName() + "_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        synchronized (new SynchronizeDemo()){
            try {
                System.out.println(Thread.currentThread().getName() + "_start_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "_end_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        SynchronizeDemo sd1 = new SynchronizeDemo();
        Thread thread1 = new Thread(new SynchronizeDemo(),"thread1");
        Thread thread2 = new Thread(new SynchronizeDemo(),"thread2");
        Thread thread3 = new Thread(sd1,"thread3");
        Thread thread4 = new Thread(sd1,"thread4");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}
复制代码

运行结果如图多线程

四个线程同时开始,同时结束,由于做为锁的对象与线程是属于不一样的实例并发

2.类锁

无所谓哪一个类,都会被拦截oracle

private void test2(){
        System.out.println(Thread.currentThread().getName() + "_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        synchronized (SynchronizeDemo.class){
            try {
                System.out.println(Thread.currentThread().getName() + "_start_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "_end_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
复制代码

运行结果以下:jvm

能够看到,类锁一次只能经过一个。ide

3.this对象锁

就是把synchronized (SynchronizeDemo.class)改成synchronized (this)工具

控制台打印结果

可能这显示结果有点歧义,其实多运行几回咱们会发现,1和2是同时结束的,3和4永远有前后,由于3,4同属于一个实例

4.synchronized修饰方法

private synchronized void test4(){
        ...
    }
复制代码

打印的结果以下:

thread1_: 22:42:04
thread3_: 22:42:04
thread2_: 22:42:04
thread3_start_: 22:42:04
thread1_start_: 22:42:04
thread2_start_: 22:42:04
thread1_end_: 22:42:06
thread3_end_: 22:42:06
thread2_end_: 22:42:06
thread4_: 22:42:06
thread4_start_: 22:42:06
thread4_end_: 22:42:08
复制代码

对于非静态方法,同一个实例的线程访问会被拦截,非同一实例能够同时访问,即此时默认的就是对象锁(this)

5.修饰静态方法的结果

在上面方法上加static

thread1_: 22:42:42
thread1_start_: 22:42:42
thread1_end_: 22:42:44
thread4_: 22:42:44
thread4_start_: 22:42:44
thread4_end_: 22:42:46
thread3_: 22:42:46
thread3_start_: 22:42:46
thread3_end_: 22:42:48
thread2_: 22:42:48
thread2_start_: 22:42:48
thread2_end_: 22:42:50
复制代码

同样的能够看出来,静态方法默认使用的就是类锁

synchronized使用小结

  • 对于静态方法,因为此时对象还没生成,因此默认采用的就是类锁(5)
  • 而采用类锁,就会拦截全部线程,只能让一个线程访问(2)
  • 对于对象锁this,若是是同一实例,那么按顺序执行,若是不是同一实例,就能够同时访问(3,4)
  • 若是对象锁与访问的对象无关,那么就会都同时访问(1)

Synchronized原理

实际上,在JVM中,只区分两种不一样的用法,修饰代码块与修饰方法,咱们能够查看SE8规范docs.oracle.com/javase/spec…

(英文很差,我有小助手怕不怕)大意是:Java虚拟机中的同步是经过显式(经过使用监视器输入和监视器输出指令)或隐式(经过方法调用和返回指令)的监视器输入和退出来实现的。 显示就是使用monitorenter和monitorexit来控制同步代码块;隐式是修饰方法,在运行时常量池中经过ACC_SYNCHRONIZED来标志。

多说无益,直接看它的字节码

public class Test {
    public static void main(String[] args) {
    }
    public synchronized void test1() {
    }

    public void test2() {
        synchronized (this) {
        }
    }
}
复制代码

最简单的程序,经过使用javap -v Test.class来查看它的字节码(注意是class文件,不是java文件)

public synchronized void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   LTest;

  public void test2();
    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
复制代码

能够看到,果真字节码中,synchronized修饰代码块时,是使用monitorentermonitorexit来控制,而synchronized修饰方法的时候,是使用ACC_SYNCHRONIZED标识。

本质上都是对一个对象的monitor进行获取,而这个获取的过程是排他的,也就是同一时刻只能有一个线程得到同步块对象的监视器monitor。

线程执行到monitorenter指令时,会尝试获取对象所对应的monitor全部权,也就是尝试获取锁,执行到monitorexit,也就是释放全部权,释放锁。

要想理清synchronized的锁的原理,须要掌握两个重要的概念:

  1. 对象头
  2. monitor

java对象头

在Hotspot虚拟机中,对象在内存中的存储布局,能够分为三块:对象头Header,实例数据Instance Data,对齐填充Padding。

Hotspot虚拟机的对象头包含了两部分信息:

  1. Mark Word,用于存储对象自身的运行时数据,好比hash,gc分代年龄,锁状态的标志,线程持有锁,偏向ID,偏向时间戳等等
  2. Klass Pointer:对象指向它的类的元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例。

32位HotSpot虚拟机的对象头存储结构以下

img

为了验证上图的正确,咱们能够查看hotspot的源码

在线地址hg.openjdk.java.net/jdk8u/jdk8u…

public:
  // Constants
  enum { age_bits                 = 4,//分代年龄
         lock_bits                = 2,//锁标识
         biased_lock_bits         = 1,//是否偏向锁
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,//hask
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2//偏向时间戳
  };
复制代码

hash:保存对象的哈希码

age:对象的分代年龄

biased_lock:偏向锁标识位

lock:锁状态标识位

JavaThread*:保存持有偏向锁的线程ID

epoch:保存偏向时间戳

因此,对象头中的Mark Word,synchronized源码就是用了对象头中的Mark Word来标识对象加锁状态。

monitor

Monitor Record是线程私有的数据结构,每个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每个被锁住的对象都会和一个monitor record关联(对象头的MarkWord中的LockWord指向monitor record的起始地址),同时monitor record中有一个Owner字段存放拥有该锁的线程的惟一标识,表示该锁被这个线程占用。以下图所示为Monitor Record的内部结构

线程惟一标识,当锁被释放时又设置为NULL; EntryQ:关联一个系统互斥锁(semaphore),阻塞全部试图锁住monitor record失败的线程。 RcThis:表示blocked或waiting在该monitor record上的全部线程的个数。 Nest:用来实现重入锁的计数。 HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。

Candidate:用来避免没必要要的阻塞或等待线程唤醒,由于每一次只有一个线程可以成功拥有锁,若是每次前一个释放锁的线程唤醒全部正在阻塞或等待的线程,会引发没必要要的上下文切换(从阻塞到就绪而后由于竞争锁失败又被阻塞)从而致使性能严重降低。Candidate只有两种可能的值0表示没有须要唤醒的线程1表示要唤醒一个继任线程来竞争

总结

简单总结一下,同步块使用monitorenter和monitorexit指令,而同步方法是依靠方法修饰符上的flag——ACC_SYNCHRONIZED来完成的。其本质都是对一个对象监视器monitor进行获取,这个获取过程是排他的,也就是同一时刻只能有一个线程得到由synchronized所保护的对象的监视器。而这个监视器,也能够理解为一个同步工具,它是由java对象进行描述的,在Hotspor中,是经过ObjectMonitor来实现,每一个对象中自然都内置了一个ObjectMonitor对象。

在java中,synchronized在编译后,会在同步块的先后分别造成一个monitorenter和monitorexit这两个字节码指令,这两个字节码都须要一个reference类型的参数来指明要锁定和解锁的对象,若是java程序中明确指定了对象,那就是这个对象的reference,若是没有指明,那么根据synchronized修饰的是实例方法仍是类方法,去取对应的对象实例或者类Class对象来作锁对象。

在执行monitorenter时,首先会尝试获取对象的锁,若是这个对象没有锁,或者当前线程已经拥有了这个对象的锁,那个锁的计数器加1,相应的,在执行monitorexit时指令时,会将锁计数器减1,当计数器为0时,这个锁就被释放。若是获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

扩展

synchronized同步块对同一线程来讲是可重入的,不会出现本身把本身锁死的状况,其次,同步块在已进入的线程执行完成前,会阻塞后面的其余线程进入。咱们知道,Java的线程是映射到操做系统中的的原生线程上的,若是要阻塞或者唤醒一个线程,都须要操做系统来帮忙,这就须要咱们从用户态切换到核心态,所以这个状态转换是很是耗费CPU。若是这个代码很是简单的同步块,可能切换状态的时间比代码执行时间还长。因此synchronized是一个重量级的操做,虚拟机自己也作了大量的优化,引入了偏向锁,轻量级锁,重量级锁等,这一部分锁的升级,能够等之后有时间了,再慢慢探讨。固然还能够引入重入锁,解决synchronized过于重量的问题。


参考

jdk源码剖析三:锁Synchronized

Java中synchronized的实现原理与应用

《深刻理解Java虚拟机》


个人CSDN

下面是个人公众号,欢迎你们关注我

相关文章
相关标签/搜索