Another Java Geek

其实我不是geek,我和你一样,我是凡客

正在浏览 Java 里的文章

Java5.0引入的enum类型可以和switch语句连用,由于平时编码中使用switch语句的几率很小,switch和enum连用的几率就更小了,今天第一次写了switch中使用枚举的代码,就遇到了问题。 代码如下: public enum Syntax { URL, SITE, FILETYPE } Syntax s = …; switch (s) { case Syntax.URL: //url specific code break; case Syntax.SITE: //site specific code break; case Syntax.FILETYPE: //filetype specific code break; default: break; } switch语句这段看似平淡无奇的代码报出了编译错误,eclipse中的错误提示如下: The qualified case label Syntax.URL must be replaced with the unqualified enum constant URL. 照着错误提示修改之后就可以编译运行了: switch [...]

OutOfMemoryError在开发过程中是司空见惯的,遇到这个错误,新手程序员都知道从两个方面入手来解决:一是排查程序是否有BUG导致内存泄漏;二是调整JVM启动参数增大内存。OutOfMemoryError有好几种情况,每次遇到这个错误时,观察OutOfMemoryError后面的提示信息,就可以发现不同之处,如: java.lang.OutOfMemoryError: Java heap space java.lang.OutOfMemoryError: unable to create new native thread java.lang.OutOfMemoryError: PermGen space java.lang.OutOfMemoryError: Requested array size exceeds VM limit 虽然都叫OutOfMemoryError,但每种错误背后的成因是不一样的,解决方法也要视情况而定,不能一概而论。只有深入了解JVM的内存结构并仔细分析错误信息,才有可能做到对症下药,手到病除。 JVM规范JVM规范对Java运行时的内存划定了几块区域(详见这里),有:JVM栈(Java Virtual Machine Stacks)、堆(Heap)、方法区(Method Area)、常量池(Runtime Constant Pool)、本地方法栈(Native Method Stacks),但对各块区域的内存布局和地址空间却没有明确规定,而留给各JVM厂商发挥的空间。 HotSpot JVMSun自家的HotSpot JVM实现对堆内存结构有相对明确的说明。按照HotSpot JVM的实现,堆内存分为3个代:Young Generation、Old(Tenured) Generation、Permanent Generation。众所周知,GC(垃圾收集)就是发生在堆内存这三个代上面的。Young用于分配新的Java对象,其又被分为三个部分:Eden Space和两块Survivor Space(称为From和To),Old用于存放在GC过程中从Young Gen中存活下来的对象,Permanent用于存放JVM加载的class等元数据。详情参见HotSpot内存管理白皮书。堆的布局图示如下: 根据这些信息,我们可以推导出JVM规范的内存分区和HotSpot实现中内存区域的对应关系:JVM规范的Heap对应到Young和Old Generation,方法区和常量池对应到Permanent Generation。对于Stack内存,HotSpot实现也没有详细说明,但HotSpot白皮书上提到,Java线程栈是用宿主操作系统的栈和线程模型来表示的,Java方法和native方法共享相同的栈。因此,可以认为在HotSpot中,JVM栈和本地方法栈是一回事。 操作系统由于一个JVM进程首先是一个操作系统进程,因此会遵循操作系统进程地址空间的规定。32位系统的地址空间为4G,即最多表示4GB的虚拟内存。在Linux系统中,高地址的1G空间(即0xC0000000~0xFFFFFFFF)被系统内核占用,低地址的3G空间(即0×00000000~0xBFFFFFFF)为用户程序所使用(显然JVM进程运行在这3G的地址空间中)。这3G的地址空间从低到高又分为多个段;Text段用于存放程序二进制代码;Data段用于存放编译时已初始化的静态变量;BSS段用于存放未初始化的静态变量;Heap即堆,用于动态内存分配的数据结构,C语言的malloc函数申请的内存即是从此处分配的,Java的new实例化的对象也是自此分配。不同于前面三个段,Heap空间是可变的,其上界由低地址向高地址增长。内存映射区,加载的动态链接库位于这个区中;Stack即栈空间,线程的执行即是占用栈内存,栈空间也是可变的,但它是通过下界从高地址向低地址移动而增长的。详情参见这里。图示如下: JVM本身是由native code所编写的,所以JVM进程同样具有Text/Data/BSS/Heap/MemoryMapping/Stack等内存段。而Java语言的Heap应当是建立在操作系统进程的Heap之上的,Java语言的Stack应该也是建立操作系统进程Stack之上的。 综合HotSpot的内存区域和操作系统进程的地址空间,可以大致得到下列图示: Java线程的内存是位于JVM或操作系统的栈(Stack)空间中,不同于对象——是位于堆(Heap)中。这是很多新手程序员容易误解的地方。注意,“Java线程的内存”这个用词不是指Java.lang.Thread对象的内存,java.lang.Thread对象本身是在Heap中分配的,当调用start()方法之后,JVM会创建一个执行单元,最终会创建一个操作系统的native thread来执行,而这个执行单元或native thread是使用Stack内存空间的。 经过上述铺垫,可以得知,JVM进程的内存大致分为Heap空间和Stack空间两部分。Heap又分为Young、Old、Permanent三个代。Stack分为Java方法栈和native方法栈(不做区分),在Stack内存区中,可以创建多个线程栈,每个线程栈占据Stack区中一小部分内存,线程栈是一个LIFO数据结构,每调用一个方法,会在栈顶创建一个Frame,方法返回时,相应的Frame会从栈顶移除(通过移动栈顶指针)。在这每一部分内存中,都有可能会出现溢出错误。回到开头的OutOfMemoryError,下面逐个说明错误原因和解决方法(每个OutOfMemoryError都有可能是程序BUG导致,因此解决方法不包括对BUG的排查)。 java.lang.OutOfMemoryError: Java [...]

在我们的web服务程序中,对每一个流入的用户请求,都需要执行N个不同任务才能完成整个处理过程。执行这些任务的多线程并发处理策略经历了几次演变。 在一开始的时候(约两年前),由于历史原因,对每一个任务,都是新开一个线程,如下: Thread t = new Thread(task); t.start(); 所以对每个用户请求,在请求处理主线程中,都new了N个线程,每个线程的执行结果通过polling方式获取,即主线程在busy wait中轮循。这种方式非常不好:新创建的线程,执行一次任务,就被销毁,不能重用,线程创建/销毁的开销是很大的;而且不能限制资源消耗,只要有用户请求流入,就会不断地创建新线程,若流量太大,服务器很容易崩掉;且请求处理主线程存在busy wait,弊端多多,参见Future模式。 因此,对并发处理策略进行了改造(约一年前):通过线程池和Future模式,实现了在一个共享线程池中执行所有用户请求的所有任务。这样基本上避免了线程创建/销毁的开销(在服务启动时会一次性创建线程池中所有线程),可以通过设定线程池大小来限制服务器的资源占用进而提升稳定性,消除了busy wait。 随着时间的推移,共享线程池的弊端也慢慢浮现出来。由于N种不同任务的执行耗时是不一样的,线程池中总的线程数有限,因此耗时最长的一种任务就会较多地占用线程池中的线程,当流量低于服务器处理能力时,这不会有什么问题。而当流量超出服务器承载能力时,那些执行时间较短的任务就得不到可用的线程去执行,进而导致请求阻塞且不会超时,最终造成整个服务处理效率低下,响应非常缓慢。流量超出服务器处理能力越多,每种任务的执行耗时差别越大,后果越明显。 针对共享线程池的问题,再次进行了改造(约一月前):按照任务类型的不同,将共享线程池分割为多个线程池,同种任务由同一个线程池来处理,避免了不同任务之间竞争资源,造成某些任务得不到可用线程而饿死。这样虽然在流量过大时,同样会存在问题(这是不可避免的),但可以通过不同的线程池,为每种任务设定不同地资源上限,预留最少资源,来缓解共享线程池的问题。这种分割线程池的方式,是物理划分,也可以通过物理共享、逻辑划分的方式来实现,在淘宝数据平台博客上有一篇支持配额的共享线程池即是如此。综合来看,物理划分和逻辑划分两种方式是不相上下的,在我们当前的应用场景中,物理划分的方式基本上算是做到了尽头。

在实际开发过程中,常见使用Double Check Locking(DCL)实现延迟初始化的单例模式。在Java中,虽然这早已被证实是一种有害的编程习惯,但这并不能阻止它在程序员之间的传播。DCL存在线程安全隐患,不少Java编程书上有关于这个问题的介绍,网上也很多讨论。不过通常情况(并发量不大、安全性要求不高)下,它能够工作得很好,这大概就是它得以流传的原因吧。 DCL是一种反模式,典型的DCL如下: public class Singleton { private static Singleton instance = null; private Singleton() { initialize(); } public static Singleton getInstance() { if (instance == null) { sychronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } 按照网上人们对DCL的分析,线程安全问题在于instance = new Singleton();这行代码:第一个线程执行到此处时,可能是先分配对象的内存空间,在尚未初始化对象之前,已将这块空间的地址赋值给instance变量,之所以这样,是因为编译器会执行指令重排(statement reordering)的优化,使得实际执行的指令顺序并非按照语句的自然顺序,即乱序(out of order)执行。进而导致第二个线程在外层if条件处跳过,直接返回instance引用,使得对象处在不完整的状态,这被称为Unsafe [...]

在java开发中,经常会使用各种开源框架或类库,例如log4j、junit、spring等,当然也包括公司内部的库。随着项目的演进,lib目录下会积累很多jar包。有时想查找代码中用到的某个类包含在哪个jar文件中,往往会大费周折。有以下方法可达到此目的: 一是手工将jar包逐个解开,从中查找类文件名。 二是借助某些工具,比如eclipse的“查找”对话框(按CTRL-H打开),在Java Search选项页设置如下: 此外在linux环境下还有一种简便的方法: cd /path/to/lib for i in `ls`; do jar tf $i | grep “PropertiesConfig” && echo $i; done 其中,/path/to/lib代表应用程序的lib目录(lib目录下放置jar包基本是java开发的惯例),该命令用来查找包含PropertiesConfig类的jar文件名。 某日,一同事开发过程中,遇到总是报NoSuchMethodException,调试许久,问题依旧。初步怀疑是classpath中存在同一个类的两个不同版本,而实际加载的是旧版本所致。在当时场景下,从推理来看多版本情况是不会发生的。但通过以上方法很快发现,在两个不同jar包中确实存在同一个类的不同版本,从而问题得到解决。

线程同步是进行多线程编程时所必须考虑的一个问题。之所以要进行同步,是因为多个线程需要访问共享资源,典型的是共享内存数据。如果能为每个线程提供一份需要共享的数据的copy,那么对该数据的访问也就没有必要进行同步了。Thread Local Storage就是能够达到这个目的的一个多线程设计模式。Thread Local Storage,顾名思义,就是“线程本地数据”,指每个线程拥有各自独立的数据拷贝。Thread Local Storage还有另外一些称呼:Thread Specific Storage,Thread Specific Data等。 Java类库中的ThreadLocal类就是该模式的一个实现。在该类的api文档中有如此说明:ThreadLocal类型的变量不同于普通变量,每个访问它的线程都有一份各自独立初始化的copy,对它的访问是通过get/set方法实现的。ThreadLocal实例典型情况下是类的private static字段。 下面一段程序使用了ThreadLocal类。这个程序假定用于对网站的访问日志进行某种处理,如果有N个日志文件需要处理,就启动N个线程,需要记录处理每个日志文件所花费的时间。 public class LogStats { private static final ThreadLocal<String> logFile = new ThreadLocal<String>(); private static final ThreadLocal<Long> startTime = new ThreadLocal<Long>(); public static void init(String logFileName) { logFile.set(logFileName); startTime.set(System.currentTimeMillis()); } public static void process() { open(logFile.get()); readAndProcessEachRecord(); long time = System.currentTimeMillis() – [...]

在做应用开发时,有时会遇到一种场景:在完成某个主任务的同时,需要处理一些其它的子任务,为了加快响应速度,会将子任务放在另外的线程中执行。例如,搜索引擎在处理用户的查询请求时,不仅要从本地数据库查找匹配的结果,同时可能会向远程的广告服务器请求广告数据,最后将搜索结果和广告一起返回给用户。在这个例子中,主线程从本地查找搜索结果,同时启动子线程从远程服务器获取广告数据,当主线程查找结束时,会将子线程得到的广告数据与搜索结果合并,最后发送给用户。Java代码如下: public void searchService(query) { AdThread adThread = new AdThread(); adThread.start(); findResult(query); respond2User(); } 主线程启动子线程后,子线程的执行已不受主线程的控制,其何时执行完毕,主线程无法预知,而其执行结果是由主线程来主动索取的。为了做到这一点,就需要用到Future模式。 Future模式是现实中提货单的抽象,好比去摄影店拍照,照片需要过些时候才能洗出来,而我们不可能一直等下去,商家一般会给我们一张单据,并告知第二天10:00以后凭此单领取照片,而我们就可以暂时离开去做其它事情,等到第二天再带着单据来到摄影店领取照片,如果我们9:30就到了,照片还没有洗出来,我们就会继续等一会儿,直到照片洗出来。以下代码用Future模式实现前述的主线程和广告子线程之间的协作: 主线程: public void searchService(query) { FutureAd future = startAdThread(); SearchResult result = findResult(query); Ad ad = future.getAd(); respond2User(result, ad); } /* 返回FutureAd对象,类似提货单,主线程稍后通过FutureAd获取准备好的广告数据 */ FutureAd startAdThread() { FutureAd future = new FutureAd(); new AdThread(future).start(); return future; } 广告线程: public class [...]