转载

JDK源码那些事儿之万物之源Object

从这篇文章开始进行AQS相关的学习,如果你还不明白什么是AQS,可以先去了解下,由于涉及的源码众多,笔者会一步一步进行深入说明整理,在学习AQS前,有很多基础知识是需要我们先去了解的,比如本文所说的Object

前言

JDK版本号:1.8.0_171

我相信很多开发者都没有完全去了解过 Object 类,只是使用的时候知道而已,其实其中还是有很多可以学习的知识

Java中的父类也称为超类,而Java是面向对象的语言,面向对象的基础就是类。Java中所有类都有一个共同的祖先 Object 类,Object类是唯一没有父类的类,Object类引用变量可以存储任何类对象的引用。那么作为万物之源的Object类不知道大家有没有去看一看其中的源码呢?

为什么先说 Object 类呢?因为其中有部分方法涉及到了多线程等待通知机制,也就是线程间通信协作的一种实现方式,是我们深入AQS前应该先了解的知识,方便后期对比,加深理解

Object

接下来我们一起看一看Object的源码实现

registerNatives

之前的源码分析中已经提及到了这个方法的作用,这里就简单再说明下,其主要作用是将C/C++中的方法映射到Java中的native方法,实现方法命名的解耦。函数的执行是在静态代码块中执行的,在类首次进行加载的时候执行

private static native void registerNatives();
    static {
        registerNatives();
    }

getClass

返回运行时对象的Class类型对象,也就是运行时实际的Class类型对象,同样是native方法,反射也是使用其来实现的

public final native Class<?> getClass();

hashCode/equals

hashCode返回对象的哈希码,是一个整数,这个整数是对象根据特定算法计算出来的一个散列值(hash值),而equals方法直接使用==进行比对,也就是比较对象的地址,默认的约定是相同的对象必须具有相同的哈希码。子类可以重写hashCode方法,但是重写后euqals方法通常也需要重写,这里就提出了那个经典的问题: 为什么hashCode和euqals重写其中一个方法,另一个方法也建议重写?

首先你应该知道的是这两个方法具有以下特性:

  • 两个对象通过equals()判断为true,则这两个对象的hashCode()返回值也必须相同
  • 两个对象的hashCode()返回值相同,equals()比较不一定需要为true,可以是不同对象

为什么要这么定义呢?其实我们联想下HashMap就明白了,HashMap的底层数据结构就是哈希表,通过key的hashCode进行散列,但是这里我们明白还需要解决hash冲突,这里就通过对象key的equals完成对比,我们比较时是需要比较对象的属性值的,而不是对象地址,这也是我们为什么通常使用String对象来作为key,因为String已经对hashCode和equals进行了重写,如果我们自己写了一个对象,没有进行重写,作为HashMap的key,你可以想想会出现什么情况?

public native int hashCode();

    public boolean equals(Object obj) {
        return (this == obj);
    }

clone

clone方法是个本地方法,效率很高。当需要复制一个相同的对象时一般都通过clone来实现,而不是new一个新对象再把原对象的属性值等复制给新对象。Object类中定义的clone方法是protected的,必须在子类中重载这个方法才能使用。clone方法返回的是个Object对象,必须进行强制类型转换才能得到需要的类型。在设计模式中原型模式就是通过clone方法来完成的,有兴趣可以去查找资料进行了解

实现clone方法需要继承Cloneable接口,不继承会抛出CloneNotSupportedException异常

protected native Object clone() throws CloneNotSupportedException;

toString

可以看到默认的toString实现,这也就是我们未重写时默认的输出,一般而言我们创建对象时都会进行重写,便于日志输出

public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

notify

我们可以看到作者的注释:

唤醒正在此对象的monitor上等待的单个线程,如果有任何线程在该对象上等待,则选择其中一个唤醒。该选择是任意的,并且可以根据实现情况进行选择。线程通过调用wait方法在对象的monitor上等待。在当前线程放弃该对象上的锁之前,唤醒的线程将无法继续。唤醒的线程将以通常的方式与任何其他可能正在主动竞争以与此对象进行同步的线程竞争。此方法只能由作为该对象的monitor的所有者的线程调用。线程通过synchronized成为对象monitor的所有者,也就是获取对象的锁,同一时刻只有一个线程可以获得对象的锁

简单说明下,通知可能等待该对象的对象锁的其他线程。由JVM随机挑选一个处于wait状态的线程

  • 在调用notify()之前,线程必须获得该对象的对象级别锁
  • 执行完notify()方法后,不会马上释放锁,要直到退出synchronized代码块,当前线程才会释放锁
  • notify()一次只随机通知一个线程进行唤醒
public final native void notify();

notifyAll

唤醒正在此对象的monitor上等待的所有线程,在当前线程放弃对该对象的锁定之前,唤醒的线程将无法继续。唤醒的线程将以通常的方式与可能正在竞争在此对象上进行同步的任何其他线程竞争,此方法只能由拥有该对象的monitor的所有者的线程调用

和notify功能差不多,只不过是使所有正在等待线程池中等待同一共享资源的全部线程从等待状态退出,进入可运行状态,让它们竞争对象的锁,只有获得锁的线程才能进入就绪状态

public final native void notifyAll();

wait

使当前线程等待,直到另一个线程在对象上调用notify方法或notifyAll方法唤醒等待线程,可以等待指定的时间,在等待过程中可以被中断,这里抛出InterruptedException异常

public final native void wait(long timeout) throws InterruptedException;

注意,在调用此方法前,当前线程必须先拥有对象的锁才能进行,注释上也说明了其使用方法:

synchronized (obj) {
       while (<condition does not hold>)
           obj.wait(timeout);
       ... // Perform action appropriate to condition
   }

finalize

当垃圾回收确定不再有对该对象的引用时,由垃圾回收器在对象上调用。子类覆盖finalize方法以处置系统资源或执行其他清除。finalize的一般约定是,当Java虚拟机确定不再有任何手段可以使尚未死亡的任何线程访问该对象时(除非由于执行操作而导致),调用finalize。简单点说就是在垃圾回收之前调用,但是不推荐使用,了解下就好

protected void finalize() throws Throwable { }

synchronized

Object对象中wait/notify/notifyAll就是线程间进行通信协作的一种方式,也就是通常的等待唤醒机制,只是在使用时我们需要先获取对应的对象锁才能进行调用,一般而言通过synchronized完成

那么,synchronized是如何实现的呢?我们通过代码来看看,方法上添加了synchronized和对代码块添加synchronized是不同的

public synchronized void synchronizedMethod(){
        System.out.println("synchronizedMethod");
    }

    public void synchronizedBlock(){
        synchronized(this){
            System.out.println("synchronizedBlock");
        }
    }

我们反编译下看看,可以看到在方法上的 flags 上添加了 ACC_SYNCHRONIZED 标识,而代码块上是多了 monitorentermonitorexit 两个jvm指令集,那么你应该明白了synchronized是通过JVM底层来实现的,既然不是Java API层面上的实现,那么先了解就好,我们的重点学习部分在于Java API层面上的同步框架实现——AQS

public synchronized void synchronizedMethod();
    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           #3                  // String synchronizedMethod
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 14: 0
        line 15: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/example/demo/SynchronizedTest;

  public void synchronizedBlock();
    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           #5                  // String synchronizedBlock
         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 18: 0
        line 19: 4
        line 20: 12
        line 21: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  this   Lcom/example/demo/SynchronizedTest;

总结

本文简单介绍了万物之源Object的源码和其中的方法,重点关注下wait/notify/notifyAll,其需要结合synchronized来完成线程间通信协作,通过反编译文件理解JVM对其进行的处理,之后会继续进行AQS的相关学习整理

以上内容如有问题欢迎指出,笔者验证后将及时修正,谢谢

原文  https://segmentfault.com/a/1190000022505608
正文到此结束
Loading...