1. 导读
接上一篇的分享, 我们一起看一下Object类中剩余的6个方法:
1.1 toString();
1.2 notify();
1.3 notifyAll();
1.4 wait();
1.5 finalize();
1.6 registerNatives();
2. toString方法
toString方法是我们比较常用的方法, 在Object中的默认实现返回一个 类名+'@'+hasCode的16进制拼接的字符串;
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
注意到toString方法是没有被final修饰的, 证明他可以被子类重写;
划重点:
.1 我们可以根据自身需求重写toString方法, 默认实现是返回类名+'@'+hashCode的16进制拼接而成的字符串;
.2 我们调用System.out.println(obj), 默认调用的就是obj的toString方法;
3. notify, notifyAll 和 wait方法
把这三个方法放一起是因为这三个方法是配套的, 用来实现JAVA多线程的协作; 既然是多线程相关的方法, 为什么会在Object这个类中呢? 这个是因为调用这三个方法前提都是需要在synchronized修饰的同步代码块中, 而synchronized锁的实现是基于对象(Object)监视器的;
那么这三个方法之间是如何协作的? 每个方法又具体干了什么呢?
3.1 wait: 在同步代码块中调用该方法时, 当前线程立即释放锁并等待, 直到有其他线程调用notify/notifyAll或超时等待时, 才会去再次竞争锁, 成功后继续执行下面的逻辑;
public final native void wait(long timeout) throws InterruptedException;
wait方法被final, native修饰, 证明他是不可被重写的原生方法; 该方法在等待的时候, 有其他线程打断了他的等待, 那么他会抛出InterruptedException并退出等待;
Object类中还有wait(), wait(long timeout, int nanos)这两种wait的实现, 但其实都是调用的wait(long timeout);
3.2 notify: 线程A在同步代码块中调用该方法时, 会随机地唤醒一个等待在该对象锁上的线程B, 注意这时候唤醒的线程B还没有持有锁, 必须要等到线程A释放锁后才能持有该把锁;
当线程在A对象的同步代码块中执行B对象的notify时, 会抛出IllegalMonitorStateException;如果没有这个限制, 我们想想会发生什么情况;
拿最常用的生产者消费者举例:
1. 消费者消费(notify生产者);
2. 当货物不存在时等待生产者生产(wait);
3. 生产者生产货物(notify消费者);
4. 当货物没被消费时等待(wait);
我们期望的是 1-->2-->3-->4-->1-->2...这样的顺序;
但是如果消费者和生产者持有的是两把不同的对象锁, 那么当消费者notify时, 因为生产者等待在另一把锁上, 导致无法唤醒生产者, 那么就会导致:1-->2-->4,生产者和消费者会同时阻塞;
所以为了消除这种竞态条件, 在A对象的同步代码块中, 只能调用A对象的notify方法, 否则就会抛出IllegalMonitorStateException;
3.3 notifyAll: 线程A在同步代码块中调用该方法时, 会唤醒所有等待在该锁上的线程, 同样的, 这些唤醒的线程只有在线程A释放锁以后, 才能再次竞争该把锁, 竞争到锁的线程继续执行, 其他的线程继续等待;
如果调用的线程不是该把锁的持有者, 那么也会抛出IllegalMonitorStateException;
public final native void notify();
public final native void notifyAll();
nofity 和 notifyAll都是不可重写的原生方法, 虽然这两个方法没有显式的抛出IllegalMonitorStateException这个异常, 但是当竞态条件产生时, IllegalMonitorStateException这个异常自然就出现了;
划重点:
.1 执行notify | notifyAll时, 唤醒的线程并不会立即持有锁, 故而会形成假唤醒的情况, 那么在写wait方法的时, 推荐使用以下方式:
synchronized(lock) {
while(!condition) {
lock.wait();
}
//doSomething;
}
当条件不满足时, 该线程还需继续等待;
.2 执行wait | notify | notifyAll的对象, 必须与synchronized锁住的对象是同一个, 否则会形成竞态条件导致IllegalMonitorStateException异常的产生;
.3 java多线程这块内容较多, 这期只是简略的介绍下这三个方法, 后面会有一系列的文章专门分享多线程相关的内容;
4. finalize方法
protected void finalize() throws Throwable { }
java的内存管理依赖于JVM实现的GC(Garbage Collection)机制来实现内存的回收, GC相关的内容后面再展开; JVM在进行GC时, 如果这个对象需要被回收, 会先判断该方法是否有被重写, 若未重写, 则直接回收该对象内存空间;
反之则判断该对象的finalize是否被执行过, 如果没有执行过, 会先放入一个队列中, 由低优先级的线程去执行该对象的finalize方法, 执行完毕后再判断该对象是否需要回收;
如果该对象已经执行过一遍finalize方法了, 直接回收对象的内存空间;
上图就是对GC执行回收对象finaliz方法时对象状态变化的过程;
划重点:
.1 重写了finalize方法后, 在对象的整个生命周期中GC只会执行一次finalize方法;
5. registerNatives方法
最后来看一下registerNatives方法, 可能看过源码的同学都知道这个方法是Object类中的第一个方法, 我把他放到最后将的原因是和他的功能相关;
private static native void registerNatives();
static {
registerNatives();
}
首先应该关注到的是static代码块, 静态代码块是在类加载时就会执行的, 这个代码块中只是调用了registerNatives方法;
再看到registerNatives方法:
5.1 static: 这是个静态方法, 因为静态只能调用静态, 也就是只有静态方法才能在静态代码块中直接调用;
5.2 native: 原生方法, 他是由C实现的;
5.3 看到方法名, 我们可以猜到他是注册Object类中的原生方法的, 实现java中声明的native方法与C实现函数的绑定;
static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls,
methods, sizeof(methods)/sizeof(methods[0]));
}
这是OpenJDK6对Object中native方法的绑定, Java_java_lang_Object_registerNatives这样函数可直接调用java中native函数, 通过上面的代码可以清晰的看到registerNatives实现了method中native方法的绑定;
同时也可以看到methods中是没有getClass这个方法, 自然可以猜到他也是采用规定函数名称直接调用的方式实现绑定的(Java_java_lang_Object_getClass);
JNI这里就不做展开, 感兴趣的可以阅读下
Java? Native Interface: Programmer's Guide and Specification;
至此, Object类中的所有方法的解析已经告一段落了, 如有错误之处, 欢迎指正;