一些零散的java知识点。
关于泛型
似乎Class<? extends Xface>
和Class<Xface>
在很多情况下是等价的。
|
|
Class.newInstance
有些区别以前就知道,比如Class.newInstance
要求必须有无参的public构造函数,而Constructor.newInstance
更灵活,利用反射的手段还可以调用私有的构造函数。
但为何不推荐用Class.newInstance
?因为二者对异常处理的差别。见:http://stackoverflow.com/questions/195321/why-is-class-newinstance-evil。
|
|
上面这段代码编译是没问题的,但运行时会由于IOException
异常终止。正常情况下IOException
必须被捕获并处理的(只有RuntimeException
及其子类可以不被捕获),但Class.newInstance
绕过了编译期的这个检查。
相当于IOException
直接被抛到最上层,程序直接终止,后面的语句都不会执行。而且打印出的堆栈信息也有问题:
|
|
堆栈给出的异常代码是构造函数,而不是Class.newInstance
,debug时会非常麻烦。
而Constructor.newInstance
会将所有异常包装成一个InvocationTargetException
再次抛出:
|
|
异常堆栈:
|
|
这个异常信息清晰很多,也说明了异常的语句确实是newInstance
那一步。
综上,确实应该尽量少用Class.newInstance
。
volatile
这篇文章讲的比较详细:http://www.ibm.com/developerworks/cn/java/j-jtp06197.html。
简言之,每个线程有自己的本地内存和共享的主内存(只是逻辑上的概念)。线程在赋值a=1时,只是将本地内存中的值修改,并不保证将修改刷新到主内存中。
如何保证刷新主内存?1.加synchronized关键字。线程在离开synchronized语句块时,会自动刷新,JVM强制。2.使用volatile关键字。volatile的变量可以保证读写操作是原子性的,会立即刷到主内存。
volatile保证对某个变量(其实就是某块内存地址)的读写都是原子性的。必定不会有多个线程同时读volatile变量、或者同时写volatile变量。所以可以用作简单的同步机制。i++
这种操作,即使加了volatile不能保证原子性。i++
可以分解为三步:读/增加/写回。第一步的读和第三步的写是原子性的。但整体不是原子性。
volatile一般用于基本类型,int/boolean之类,其实也可以用于对象,见文中的例子。但这种用法非常容易出错,还是不要用的好,给自己找麻烦。
用volatile要仔细思考,否则得不偿失,只为了一点点性能提升,还不如用synchronized直观。
transient
这个关键字感觉很少见。
只在实现Serializable
接口的类中有用,被这个关键字修饰的变量不会序列化。
不过本来java自带的序列化机制就用的不多吧。。。
感觉这是个历史遗留问题,要实现这种功能完全可以用注解,就像Jackson JSON一样,没必要新增个关键字。
wait()
看到一个比较有意思的问题。作者最后提的几个问题比较有意思。
第一个问题,代码中的while()不能换成if()。根源还是在于notifyAll不能精确唤醒某个线程。notifyAll会唤醒所有等待的线程,但其中有些线程是”目标”,要输出数据.有些线程只是”误伤”,需要继续wait。 所以即使唤醒后也要在while循环里继续判断是否wait。
改成if的话,各个线程的执行顺序无法确定了,有可能死锁。如果线程3先执行完毕,线程2获得锁,把state改为3,线程1和线程2就只能一直等待。
第二个问题,notifyAll也不能改成notify。notify是随机唤醒一个等待的线程。可能死锁。比如线程1设置state=2,然后notify,假设线程3被唤醒,发现state不是3,继续wait。接着线程1获得锁,发现state不是1,也wait。于是3个线程都处于wait状态。
几个注意点:wait()一般都要在while循环里调用;sth.wait()的线程会释放sth对象的锁,在唤醒前不会再参与锁的竞争;sth.wait()的前提是当前线程持有sth对象的锁,否则会抛IllegalMonitorStateException异常;wait()的线程被唤醒后,会重新尝试获得锁,然后从wait()开始执行。
wait/notify是比较底层的线程同步手段,能不用尽量不用。concurrent包提供了很多替代品。
题外话:1.JVM中的所有对象都有一个隐含的锁,或者叫监视器(monitor),包括XX.class对象。synchronized的原理就是利用这个;2.JVM中的线程都是映射到系统内核本身的线程机制的。
CountDownLatch
最近用CountDownLatch碰到一个死锁的问题。代码:
|
|
运行这个程序的时候,有时能正常结束,有时就会死锁。debug了半天,请教了同事才明白。
在PutRunner.run()
方法里将latch.countDown()
放到最后,前面的try-catch看似捕获了所有的异常(checked exception),但如果有RuntimeException(unchecked exception)线程会直接终止,导致latch.countDown()
不会执行,于是main线程一直卡在latch.await()
不能结束。
坑爹的是这个RuntimeException会被吃掉。。。在命令行中根本看不到任何异常信息。一个类似的问题见StackOverflow。
将catch (IOException e)
改成catch (Exception e)
,就可以了。当然也有其他更优雅的解决方法。
NoClassDefFoundError vs ClassNotFoundException
java的classpath机制还是比较头疼的,最常见的两个错误就是NoClassDefFoundError
和ClassNotFoundException
,关于二者的区别可以参考这个帖子。
从原理上说,jvm会从classpath中寻找class并加载到内存中。在使用一个类时,会先从内存中找,然后从classpath中找。如果“内存-classpath”这个链路中找不到,或者找到后出了其他错误,抛NoClassDefFoundError
;如果从classpath找不到,抛ClassNotFoundException
。注意二者之间微妙的联系:很多情况下,NoClassDefFoundError是由ClassNotFoundException引起的,比如下面这种(这个错误是由于编译时classpath中有guava,但运行时classpath中没有导致的):
|
|
但NoClassDefFoundError也不一定全是由ClassNotFoundException引起。如果classpath能找到对应的类,但初始化类的过程中出错,也会出现NoClassDefFoundError:
|
|
|
|
错误信息:
|
|
另外注意异常信息中的<clinit>
,这是一个特殊的标志,表示初始化某个类的static方法和变量。类似的还有一个<init>
,表示调用类的构造函数。
从使用场景上来说:
- 如果某个类是编译时需要的并且编译成功:
1.1 运行时不存在,一般会抛NoClassDefFoundError + 类名,第一次调用时会有个Caused by ClassNotFoundException
1.2 运行时存在但类初始化出错,第一次使用该类时出现ExceptionInInitializerError,以后再使用时就会抛NoClassDefFoundError: Could not initialize class + 类名 - 如果某个类是通过反射加载的(换句话说,不需要编译时存在),比如
Class.forName
:
2.1 运行时不存在,直接抛ClassNotFoundException(这些反射的方法会跳过内存中查找的步骤,直接从classpath中查找)
2.2 运行时存在但类初始化出错,跟1.2是一样的情况
Java Cookies
一些有用的小技巧
|
|
除了guava外,netty可以提供了很多有意思的工具类:
http://netty.io/wiki/using-as-a-generic-library.html
其中的Listenable Future、对象池感觉比较有用
接口的默认实现
Java中的interface中只能定义方法签名,不能定义方法体,但Java8中这个限制被打破了,接口中可以带默认实现,称作default方法:
https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html
一个例子就是JDK自带的java.util.Iterator
Apache vs Tomcat
他们的区别是啥?经常有同学问到这个问题。。。网上也有很多解答,但都比较笼统。
“既然php可以通过mod_php在Apache中使用,为啥没有一个mod_java?”
“既然tomcat已经能处理http请求了,为何前端还要一个Apache/Nginx?”
回答这些问题需要先去考下古。
Apache(httpd)出现的比较早,第一个版本是1995年的,是专门处理http请求的服务端软件。概括的说,对http请求主要有3种处理方式:1. 如果是静态文件直接返回;2. 结合CGI动态生成内容;3. 转发请求给其他host。
所谓的CGI,是一种很古老的技术,让各种语言都能处理http请求。简单的说,每次Apache接收到一个请求,就会fork-and-join
产生一个新的进程,这个进程的标准输入/标准输出就对应于socket的输入/输出,还有其他一些环境变量也对应于各种参数,比如IP/UA之类。绝大多数语言都可以实现CGI,毕竟处理标准输入输出/环境变量是所有语言的标配。
php的mod_php
就是这个原理。
这就是所谓的多进程模型,但多进程时毫无疑问吞吐量会有问题。于是有了所谓的FastCGI,大概原理就是一个进程池,不用为每个请求都新建一个进程了。后续还有所谓的SCGI和WSGI等衍生。
既然如此,为什么不直接用java+CGI去处理http请求呢?还真有这么做的:
https://opensource.apple.com/source/FastCGI/FastCGI-4/fcgi/doc/fcgi-java.htm
注意下这篇文章是1996年的,而且还是苹果的,真是少见。。。96年的时候,JDK还只是1.0版本,Servlet规范还没出现(Servlet 1.0规范是97年6月提出的),如果想用java处理http,确实也只能用CGI。估计当时的java对服务端也没什么想法吧,感觉像玩具一样的语言,在前端做各种applet。。。直到JDK1.2,Sun才提出了J2EE,也就是现在的JavaEE,开始发力服务端。
那么,为啥没发展出所谓的mod_java
呢?看上面文章中的代码就知道,用CGI去处理http非常“不OO”,这跟java的理念是完全相悖的。而且这是一种比较“底层”的API,使用不方便,维护、扩展都有很多问题,很多特性(比如session处理)都需要另外去补充。而且java是基于多线程模型去处理请求的,跟Apache的多进程模型也不同。当然也不能忽略Sun的推动与宣传。
不管怎样,java+CGI没有成为主流,servlet成了java世界的标准,应该说这是历史的选择?从设计上来讲Servlet可以支持各种类型的请求,但实际中绝大多数实现都是用于处理http请求。Tomcat就是所谓的Servlet容器,符合servlet规范的java工程,直接部署到tomcat中即可运行。
那么,为啥不能用tomcat直接处理所有请求?为啥要加一层Apache/Nginx?从理论上来说,是可以用tomcat处理所有请求的,但还要考虑到效率的问题。虽然Apache“结合CGI动态生成内容”这个功能被废掉了,但“处理静态文件”和“转发请求”这两个功能还在啊,可以用来减轻tomcat的负载,还能实现负载均衡等功能。
单纯说处理静态请求的话,各种Servlet容器还是不如Apache/Nginx,毕竟不是专门干这个的。虽然也能用DefaultServlet
处理静态文件,但多了一层Servlet的处理逻辑,性能上总会有些损耗。
一篇比较好的介绍Servlet的文章:
https://www.ibm.com/developerworks/cn/java/j-lo-servlet/
如何减少JVM线程数
前段时间碰到一个问题,JVM创建的线程数达到了系统上限。根本的解决方法当然是增加系统阈值,但JVM本身也是有些可以优化的地方,比如GC线程和Attach线程。参考:How to reduce the number of threads used by the jvm。
另一个资料:https://my.oschina.net/igooglezm/blog/757587
JVM报错zip file closed
很奇怪的错误,似乎是在加载jar文件的时候报错,还可能一同出现“Inflater has been closed”错误。debug了一下代码,似乎跟Java的SPI机制有关,原理暂时不明。新增一个jaxp.properties配置文件可以解决:
|
|
xerces这个库好像还有各种各样的蛋疼问题。