Object
Object
是Java中类结构的根。是所有类和数组(T[])的超类。
public final native Class<?> getClass()
该方法由本地方法实现。
返回一个对象的类对象。返回的类对象,就是该类中 static synchronized
方法加锁的对象。
如果对象是一个类型擦除对象,则返回对象运行时的类。也就是说 ((Object)new String()).getClass()
实际返回的是 String。
public native int hashCode()
该方法由本地方法实现。
返回对象的哈希码,已用于需要判断对象相等的场景。
这个方法可以重写,但需要满足以下约定:
- 在应用运行期间,当hashCode方法使用到的对象信息没有被修改过的情况下,对同一个对象的多次调用必须产生相同的整数。但同一个应用程序的两次运行不需要产生相同的整数。
- 如果
equals(Object)
返回两个对象相等,则这两个对象中的每一个调用hashCode方法必须产生相同的整数结果。 - 如果
equals(Object)
返回两个对象不相等,并不要求对每个对象调用hashCode方法必须产生相同的整数结果。但虽然没做要求,如果产生不相等的整数能够提高哈希表的性能。
在合理的情况下,没有被重写的 hashCode 方法确实为不同的对象返回不同的整数。这是通过将对象的内部地址转换为整数来实现的,对象不同则内部地址不同。但Java并没有要求必须这样做。所以重写后的 hashCode 方法,不同的对象可能会返回相同的整数。
public boolean equals(Object obj)
equals 在非空对象引用上实现如下等价关系:
- 自反关系:对于任何非空对象x,
x.equals(x)
应该返回true。 - 对称关系:对于任何非空对象x和y,当且仅当
y.equals(x)
返回true时,x.equals(y)
才应该返回true。 - 传递关系:对于任何非空对象x,y和z。如果
x.equals(y)
返回true并且y.equals(z)
返回true,那么x.equals(z)
应该返回true。 - 一致关系:对于任何非空对象x和y,当对象
equals
方法中使用到的信息没有被修改时,x.equals(y)
的多次调用应该保持一致。 - 对于任何非空对象x,
x.equals(null)
应该返回false。
当重写此方法时,应当重写hashCode方法。
Object类默认的实现是,判断是否是同一个对象,因此对于非空引用值x和y,只有x和y引用同一个对象时,才返回true。
protected native Object clone() throws CloneNotSupportedException
该方法由本地方法实现。
返回一个对象的副本。对于对象x,需要但不强制满足:
- x.clone() != x
- x.clone().getClass() == x.getClass()
- x.clone().equals(x)
通常来讲,clone方法需要调用 super.clone
。如果所有超类都遵守这个约定,就可以满足 x.clone().getClass() == x.getClass()
。
Object类中的默认实现是浅拷贝。并且如果对象没有实现Cloneable接口,会抛出CloneNotSupportedException。需要注意所有数组T[]都认为实现了Cloneable。
通常来讲,这个方法的重写应该实现深拷贝。
因为类Object没有实现Cloneable接口,所以在对Object对象上调用clone方法会抛出CloneNotSupportedException。
public String toString()
返回一个对象的字符串表示形式。结果应该是一个简洁但信息丰富的提示,易于阅读。Object类的默认实现是 getClass().getName() + '@' + Integer.toHexString(hashCode())
。因此建议重写此方法。
protected void finalize() throws Throwable
一个对象真正宣告死亡,至少要经历两次标记过程:如果对象在进行可达性分析之后发现没有与GC Roots相连接的引用链,那它将被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程执行。稍后GC将对F-Queue中的对象进行第二次小规模的标记。
由于Finalizer线程和GC线程没有关联,所以finalize方法并不保证一定会被执行,以及何时会被执行。
finalize方法虽然可以使该对象再次被引用,然而finalize的目的是在对象不可撤销地丢弃之前执行清理操作。
默认的finalize实现不执行任何操作,只是正常的返回。
finalize方法是这个对象上调用的最后一个方法,在此之后,这个对象的任何方法都不会再被调用。
Java不限制调用finalize方法的线程,但是调用finalize方法的线程不能再持有任何用户可见的锁。
finalize方法只会被调用一次,即使对象下一次被回收也是。
finalize方法抛出任何异常不会有任何提示,并终止finalize方法。不会影响GC对对象的回收。
为什么notify和wait在Object中实现,以及synchronized的可重入次数
Java提供解决线程安全的方式有两种,一种是synchronized关键字,另一种是juc工具包。synchronized关键字是jvm虚拟机级别提供的控制反转形式的监视器线程安全处理方式。使用synchronized关键字后,由jvm处理线程并发、锁等待,锁升级、锁占用、锁释放。如果只使用了synchronized,仅解决了线程安全问题,而没有解决线程通信问题。典型的生产-消费者模型中,需要线程通信来解决消费者线程等待、生产者线程唤醒的问题。
synchronized提供的监视器,可以使用任何Java类或对象来作为信号量,所以synchronized可以修饰任何方法,或使用任何对象作为参数。使用对象头中的markword当做锁。因此监视器只能知道锁被哪些线程持有,而不知道哪些线程持有锁。所以为了解决线程通信的问题,就需要使用监视器的信号量来唤醒等待中的进程。因为对象就是监视器的信号量,所以notify和wait方法需要在Object类中实现。虽然这两个方法在Object类中,但是由于它们的目的为了解决线程通信问题,所以在非synchronized代码块内调用会抛出IllegalMonitorStateException。也就是只能持有锁的线程能调用wait和notify方法。
synchronized加的锁是可重入锁,持有锁的线程可以多次获取锁。juc中的Lock锁可重入次数是Integer.MAX_VALUE。而synchronized的可重入次数虚拟机规范中并没有明确说明,取决于具体的虚拟机实现。但是一般来说Lock可以在循环中调用,所以需要设置可重入次数。而synchronized关键字,由于虚拟机会对字节码进行优化,进行锁消除或合并相邻、嵌套的synchronized块,是有一个逻辑上的次数上限的。这个上限取决于class文件大小限制(最多65535个方法每个方法限制为65535个字节)和jvm的堆栈大小限制。
但是从程序的角度讲,只要涉及到了计数,jvm中一定会有一个计数器的。以HotSpot虚拟机为例,查看objectMonitor.cpp文件可以看到,ObjectMonitor::enter
方法中的计数器是 _recursions
变量,它在objectMonitor.hpp中的定义是 intptr_t
类型。intptr_t
是个指针类型,在32位和64位操作系统中长度不一样,所以上限也不一样。
/** objectMonitor.hpp **/
private:
volatile intptr_t _recursions; // recursion count, 0 for first entry
// TODO-FIXME: _count, _waiters and _recursions should be of
// type int, or int32_t but not intptr_t. There's no reason
// to use 64-bit fields for these variables on a 64-bit JVM.
/** objectMonitor.cpp **/
if (cur == current) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions++;
return true;
}
从HotSpot代码中可以看出来,关于_recursions的大小是有过争议的。在使用的地方考虑过整型溢出的问题。但是从BUGID 6557169可以知道,这个风险由于字节码优化、文件大小、堆栈大小等限制,是被忽略了的。在字段定义位置的注释也可以看出设计者认为这个变量只需要32位就足够了,intptr_t在64位虚拟机上是一种浪费。
notify 和 wait
要理解notify和wait,需要理解synchronized监视器中锁池和等待池的概念。所有争抢锁的线程进入锁池,持有锁的线程调用wait方法后会进入监视器的等待池。notify方法用于唤醒处于等待池中的线程,把等待池中的线程移到锁池中继续争抢锁。
死锁
死锁的定义是一个线程进入等待之后,只能依靠其他线程的行为才能继续。比如两个线程互相持有对方等待的锁、或者一个线程进入等待迟迟没有其他线程来唤醒。知道了死锁的定义,也就可以理解notify会导致死锁的情况了。
park 和 unpark
synchronized中的wati和notify,以及LockSupport中的park和unpark,在jvm中是使用thread.hpp中定义的ParkEvent和Park属性来实现的park和unpark操作。ParkerEvent继承了PlatformEvent。基类PlatformEvent是特定于平台的,而ParkEvent则是平台无关的。Parker继承自PlatformParker。
ParkerEvent中的park,unpark方法用于实现Java的object.wait()方法和Object.notify()方法。
Parker中的park,unpark方法用于实现Java的Locksupprt.park()方法和Locksupprt.unpark()方法。
public final native void notify()
唤醒对象的监视器上等待的多个线程中的一个,选择方式取决于实现,对于HotSpot来说选择最早进入等待状态的一个。线程通过调用wait方法来等待对象的监视器。
调用notify后当前线程并不会立刻终止,而是会继续执行完直到主动释放锁后,被唤醒的线程才会继续。被唤醒的线程会与其他等待此对象锁的所有线程进行同样的竞争,而没有特权。
notify方法只能由此对象监视器的所有者线程调用,线程通过以下三种方式之一成为对象监视器的所有者:
- 通过执行该对象的synchronized方法
- 通过执行该对象中的synchronized语句块
- 通过执行类的synchronized静态方法
一个对象的监视器同时只能由一个线程获得。如果线程在没有获得任何对象的监视器的情况下调用了notify,会抛出IllegalMonitorStateException。
不建议使用此方法,因为使用此方法只能唤醒一个等待中的线程,而其他等待中的线程如果没有继续被唤醒,就是造成死锁。
public final native void notifyAll()
唤醒对象的监视器上等待的所有线程。在HotSpot中的实现,与notify不同的是,会按照FIFO的机制唤醒等待池中的线程。建议使用以避免死锁。
public final native void wait(long timeout) throws InterruptedException
当前线程必须拥有对象监视器才能成功调用,否则会抛出IllegalMonitorStateException。使当前线程进入等待池,直到如下条件:
- 另一个线程为此对象监视器调用notify或notifyAll方法
- 其他线程对此线程调用了interrupt
- 到了超时时间
然后线程会被从等待池中移除,并重新启用线程调度。该线程会与其他线程进行同样的锁竞争。一旦它获得了对象监视器,将把对监视器的所有声明都恢复到调用wait方法时的状态。
线程有可能会发生虚假唤醒,但有必要通过防御性编码来避免这种情况。所以应该使用循环来检测导致线程wait的条件:
synchronized (obj) {
while (触发线程wati的条件){
obj.wait(timeout);
}
... //唤醒后的业务逻辑
}
如果等待中的线程在等待之前或等待期间被其他线程中断,则会在等待线程重新抢到锁并恢复状态时触发InterruptedException。
请注意,wait方法进入到等待池时仅会解锁此对象的监视器。如果这个线程同时还持有其他种类或资源的锁并不会一同被释放。
public final void wait(long timeout, int nanos) throws InterruptedException
这个方法只是比 wait(long timeout)
允许更精确到纳秒的时间控制(1000000*timeout+nanos)。也就是说 wait(0,0)
是与 wait(0)
等价的。
public final void wait() throws InterruptedException
同 wait(0)
。
JDK包目录
JDK源码rt.jar中java.io包拆解——Sep 1, 2020
AutoCloseable、Closeable、InputStream/OutputStream、Reader/Writer、DataInput/DataOutput、ObjectInput/ObjectOutput、RandomAccessFile、IOException、IOError、File
JDK源码rt.jar中java.util.concurrent.locks包拆解——Sep 12, 2020
AbstractOwnableSynchronizer、AbstractQueuedLongSynchronizer、AbstractQueuedSynchronizer、ReadWriteLock、Lock、Condition
JDK源码rt.jar中java.util.concurrent包拆解——Sep 12, 2020
Executor、ExecutorService、ScheduledExecutorService、AbstractExecutorService、BlockingQueue、RejectedExecutionHandler、ConcurrentMap、SortedMap、AbstractList、AbstractSet
JDK源码rt.jar中java.util包拆解——Sep 12, 2020
Collection、List、Set、SortedSet、Queue、Map、AbstractMap、SortedMap、AbstractList、AbstractSet
Happens-before 顺序——May 5, 2023
两个动作按照 happens-before 关系按序进行。如果一个动作 happens-before 另一个动作,则第一个动作在第二个动作之前进行,并且第一个动作的结果对第二个动作可见。