Java泛型的类型擦除始末,找回被擦除的类型
Java设计之初,语言的规范和虚拟机的规范是分开的。Java语言的规范叫JLS(Java Language Specification),Java虚拟机规范叫JVMS(Java Virtual Machine Specification)。
最初,JLS和JVMS都没有考虑到泛型。泛型在JDK 5开始引入,虽然Java语言支持泛型,不过JVM并没有这个概念,如果需要JVM支持,大量的字节码可能会被重新定义,因为变化实在太大,因此只能在编译时,把类型擦除掉,实现向下兼容。为了更好地理解本文,读者最好准备以下知识:
-
熟悉泛型的概念
Generic、Parameterized、Type Parameter、Type Argument等,可在Java泛型几个常见的术语补充相关知识。
- Java反射
- JVM简单的字节码
Java调用wait()和notify()必须获得锁的原因
今天看到个很有意思的问题,《为什么使用 notify 和 notifyAll 的时候要先获得锁》?
这个问题其实真不好回答,就像大家习以为常的事物,突然被问为什么了。这问题我思考了一下,同时也去stackoverflow找到同类型的问题,不过答案都无法令我满意。最后翻到了jls(Java Language Specification),大概知道了真正的原因。
我觉得为什么notify必须要synchorized,根本原因在于对jls规范中规定了对wait set的操作必须是原子的。先看看jls对wait set的描述。
Every object, in addition to having an associated monitor, has an associated wait set. A wait set is a set of threads.
When an object is first created, its wait set is empty. Elementary actions that add threads to and remove threads from wait sets are atomic. Wait sets are manipulated solely through the methods
Object.wait
,Object.notify
, andObject.notifyAll
.Ref: https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html
简单来说,当对象创建时,会顺带创建Monitor和Wait Set,这些是在C语言层面去创建的。然后它告诉我们对Wait Set的操作都是原子的,这就能解释,为什么wait和notify必须获得锁。因为没有锁,就没办法保证对wait set的操作是原子的。
Java什么时候才能用synchronized
编写多线程的代码时,使用synchronized关键字能提供JVM级的线程同步。因为synchronized本身性能不高,于是Doug Lea编写了JUC模块,提供了AQS这样强大的接口,以及ReentrantLock这样方便的类。
也许你会听到有人跟你讲,jdk6后synchronized得到了优化,性能已经不低了。但实际上,在多线程竞争时,synchronized效率依然很低,线程竞争激烈时,还是不可以用synchronized,为什么呢?
上面的图片出自openjdk HotSpot对synchronized的描述,右边是jdk6以前的加锁流程,左边是jdk6后的优化,即加入了偏向锁。简单来说,当第一个线程进入synchronized代码块时,JVM会利用对象头的mark word存下当前线程ID,下次同一线程再进入,只需要对比线程ID即可,不需要经过右边那样的加锁过程。
因此我们可以看到,jdk6优化的是同一线程的进入,但在竞争激烈的情况下,还是要走普通的加锁流程。右边的流程,即是是同一线程进入,也要经过一次CAS操作判断锁的归属,CAS操作变多,带来的是很大的性能损耗。
那么ReentrantLock为什么性能更高呢?我们可以看看它的加锁过程。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
上面代码,只有第一次获得锁时才会执行CAS操作,加锁失败会执行park进入队列等待唤醒。else if 判断的是线程重进入,只是对比线程,不需要执行CAS操作。因此,ReentrantLock在资源竞争激烈时,也能保持较高的性能。
回到原本的问题,什么情况下才能用synchronized呢? 很简单,在肯定资源几乎不会有竞争的情况下,才可以使用,比如spring的代码。
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
......
}
}
还有一点需要注意,使用synchronized最好创建一个Object作为Lock,不要尝试去锁定某个对象本身。因为一旦访问了对象的hash code,偏向锁会失效。
Object lock = new Object();
synchronized(lock) {
//do something...
}
//不要这样做
//synchronized(this) {
//}
想要知道这些的所有原因,就必须去读一下openjdk这篇文章了。Synchronization and Object Locking。
Java泛型几个常见的术语
泛型有几个专业术语: Generic Type、Parameterized Type、Type Parameter、Type Arguments。这几个东西我也不知道怎么翻译,直接照搬外网的解释了。不想翻译内容,就当个人笔记。文章内容全部来自,这里有很多泛型的解释,GenericsFAQ 强推一波。
Generic type
A generic type is a type with formal type parameters. A parameterized type is an instantiation of a generic type with actual type arguments.
A generic type is a reference type that has one or more type parameters. These type parameters are later replaced by type arguments when the generic type is instantiated (or declared ).
Example (of a generic type):
interface Collection<E> { public void add (E x); public Iterator iterator(); }
谈谈Java的变异(不变 协变 逆变)
注意: 这篇文章几年前写的,回头看内容存在很多问题,对这些概念解释的也很不清晰,不建议观看,后续可能会更新文章内容,或者重新发一篇新的。这篇留着作为曾经的记忆
Subtyping
要想了解变异先要理解Subtyping的概念。Subtyping是面向对象里”类型多态(Type Polymorphism)”的其中一种表现形式,它主要描述”is a”这样的关系。比如S
是T
的子类型,那么他们的关系可以表达为 S is a subtype of T。维基百科有一段对Subtyping的描述。
In programming language theory, subtyping (also subtype polymorphism or inclusion polymorphism) is a form of type polymorphism in which a subtype is a datatype that is related to another datatype (the supertype) by some notion of substitutability.
If S is a subtype of T, the subtyping relation is often written S <: T, to mean that any term of type S can be safely used in a context where a term of type T is expected
Subtyping和变异有什么联系呢?变异其实就是指Subtyping在更复杂的场景下,比如If S is a subtype of T, Generic<S> is subtype of Generic<T>这种关系是否还能成立。
从生物学角度看面向对象编程的多态
我们都知道面向对象编程是尽量模拟现实世界对象之间的关系,”多态”是OOP中非常重要的一个概念,它是指一个对象的多种形态。不过这个概念不是很好理解,或者说我们有时候理解的不是很透彻。最近刚好有空翻了一下Wikipedia,发现从生物学角度更好理解多态。
//已知Tiger是Cat的派生类,有没有想过,为什么这样就称为多态?
Cat cat = new Tiger();
Lambda表达式和匿名类的区别,不只是语法糖
Java8中引入了Lambda表达式,可以简化我们的编码,使用起来非常方便。
Java8之前用Runnable通常会new一个匿名类
public class LambdaDemo {
public static void main(String[] args) {
//jdk 1.7
Runnable r = new Runnable() {
@Override
public void run() {
//do something...
}
};
//jdk 1.8
Runnable r2 = () -> {
//do something...
};
}
}
有些人觉得Lambda仅仅是个语法糖,实际上并不是这样的。这里可以先抛出结论:普通匿名类方式的调用,编译时会生成XX$YY这类匿名类(在本例中是LambdaDemo$1),而Lambda不会生成类。在字节码层面,匿名类是通过invokespecial调用。 lambda是通过invokedymanic调用。下面我们来看看。
DAO还是Repository,傻傻的分不清?
DAO vs Repository
在Java开发中,我们经常会接触到DAO,有时,我们也能看到Repository。从代码上看,这两者似乎区别不是很大,很容易让人混淆。究竟这两个该在什么场景使用,我看网上讨论的不是很多。要想知道它们该怎么用,还是要先区分清楚它们的概念。
本文大部分内容都来自于参考资料中的文章,建议英文阅读能力好的朋友直接去看英文原文。