Archive

Posts Tagged ‘java’

解决Java序列化的版本冲突

March 19th, 2011 西坪 No comments

序列化机制有很多种,Java原生的序列化机制是最方便、简单、直接的序列化方法之一。尽管这种机制有很多缺点,但还是有不少项目使用。我们有一个项目,将部分对象序列化后存储在Memcached中,后来程序出了升级版本。不幸的是,序列化对象对应的类有了一些变化,新版本的程序不能反序列化之前的数据。在协助升级Memcached中的数据的过程中才发现这个问题,这时便需提前将Memcached中的数据针对新版本程序做转换。 Memcached的导出机制我已经在前面的博文中作过介绍。

什么时候Java序列化会发生冲突

Java的序列化,实际上序列化的是对象的内容,也即只包括域(含父类的域)。因此类中方法的变化不会对序列化造成影响,只有类的体系和属性会对序列化造成影响。主要的导致版本冲突的变化包括:

  • 删除域
  • 改变类继承体系,讲一个实例域修改为静态或将非瞬时(transient)域修改为瞬时域
  • 修改域类型
  • 修writeObject/readObject导致不兼容的修改
  • 以及其他更多….

详细请参见官方文档

我们的程序修改了一个属性的类型,导致新版本程序不能反序列化之前的数据。

使用Reflection解决版本冲突

最早我尝试使用动态类加载和反射(Reflection),将旧版本的classes加入到类路径中,使用旧版本的classes将数据反序列化,再通过Reflection将值设置到新版本class的实例上,再将新版本的实例反序列化回Memcached中。伪代码如下:

//Add old classes to classpath and 
import ***.stat.***.service.impl.AAAContext;
 //return: new Instance of AAAContext;
 //param: serialized data of old instance AAAContext
 private byte[] getNewInstance(byte[] serializedData) throws Exception {
 
   //get the old instance by deserializing data 
   AAAContext oldObje = deserialize(serializedData);
 
   //load new classes from external jar files
   URLClassLoader clazzLoader ;
   Class aaaContextClazz;
   URL jarfile = new URL("jar", "","file:E:\\workshop\\new-lib.jar!/");
   URL jarfile1 = new URL("jar", "","file:E:\\workshop\\dependencies.jar!/");
   clazzLoader = new URLClassLoader(new URL[]{ jarfile, jarfile1}, null);
   aaaContextClazz = clazzLoader.loadClass("***.stat.***.service.impl.AAAContext");
   Object oldins = aaaContextClazz.newInstance();
 
   //get setters on the new clazz, MUST find every setter method by hand coding
   Method m[] = aaaContextClazz.getDeclaredMethods();
 
   m[0].invoke(ins, oldObject.getProp1());
   m[1].invoke(ins, oldObject.getProp2());
   .... ....
 
   //return the serialized data of new instance
   return serializeObj(ins);
}

其中比较特殊的是 “clazzLoader = new URLClassLoader(new URL[]{ jarfile, jarfile1}, null);” 这一行代码,因为Java类加载体系的继承方式,必须指定new URLClassLoader()的第二个参数为null,去掉parent加载器。在不指定该参数时,会使用当前类加载器作为parent加载器,于是则会先从当前的类加载器体系中去寻找,只能得到类路径(classpath)下的同名类。

这种方法因为要序列化的字段太多,实现起来不易,且需要老版本的各依赖包,工程人员实施起来麻烦,故转而采取了下面的方法。

使用内置的序列化措施解决冲突

花了点时间研究Java的序列化机制后,发现有更好的方法支持序列化数据的版本变化。其中最简单的就是添加readObject/writeObject方法,自定义实现序列化,完成新旧版本间的转换。主要有下面三步要做。

第一步:自定义要更新的字段

Java序列化机制容许我们指定哪些字段要序列化(官方文档)。将类型变化了的属性的序列化类型指定为旧版本中的类型(Long.class,新类型为String.class),以兼容老版本的序列化信息。定义如下:

  //定义的序列化版本ID必须与新/旧的class一致
  private static final long serialVersionUID = 3495267477129405823L;
  private static final
  ObjectStreamField[] serialPersistentFields = {
	new ObjectStreamField("id", Long.class),
	new ObjectStreamField("aaa_BeginTime", Date.class),
	new ObjectStreamField("flag", Integer.class),
              //略去几十个
 
              //将类型变化了的属性的序列化类型为旧版本中的类型
              new ObjectStreamField("sess_parameter", Long.class)
 };
第二步:定义readObject方法

定义readObject方法,读取老版本的数据,并做新旧类型转换:

private void readObject(ObjectInputStream ois)
    throws ClassNotFoundException, IOException {
  // Read version one types
  ObjectInputStream.GetField fields =  ois.readFields();
 
  this.id = (Long)fields.get("id", null);
  this.aaa_BeginTime = (Date)fields.get("aaa_BeginTime", null);
  this.flag = (Integer)fields.get("flag", null);
  //略去几十个
 
  //按旧版本读取,在转化为新版本的类型
  Long old_param = (Long)fields.get("sess_parameter", null);
 
  this.sess_parameter = String.valueOf(old_param);
}
第三步,定义writeObject方法

定义writeObject方法,以新版本class的序列化格式得到新的序列化数据:

private void writeObject(ObjectOutputStream oos)
    throws IOException {
   PutField fields = oos.putFields();
  fields.put("id", this.id);
	fields.put("aaa_BeginTime", this.aaa_BeginTime);
	fields.put("RoamFlag", this.RoamFlag);
 
  //按新版本方式写入
  fields.put("sess_parameter", this.sess_parameter);
  oos.writeFields();
}

以这种方式,只需要新增一个类就完成了转化工作。 这个类不是新版本程序中的类,实际上只是用于中间转化操作的临时代码,它以旧版本class的序列化方式读取数据,再按新版本class的序列化方式输出数据。

教训

使用默认的Java序列化机制,会使得具体的类成为数据的schema,持久化的数据受限于类的实现细节。这带来了多方面的约束。所以,如果你用Java的原生序列化,起码应该制定一个合适的Schema,并清楚类的实现细节变化可能导致数据需要特别的迁移处理。我建议选择一种别的与类的实现细节无关的序列化方案。

参考资料

  1. Java Serialization Specification (1.5)
  2. Type Changes Affecting Serialization
  3. Java序列化算法透析: 国人写的一篇Java序列化算法的分析,解析很透彻
  4. The Java serialization algorithm revealed : 一篇英文的有关序列化机制的分析文章,与前一篇参考很接近。
  5. Implementing Serializable : 简明扼要的概述了序列化机制
  6. Advanced Serialization
  7. Discover the secrets of the Java Serialization API
  8. Dynamic Class Loading and Reloading in Java

J2EE/Oracle性能调优实录

February 18th, 2011 西坪 No comments

这两天,有一个新接手的项目性能表现较差,不能满足要求。这个项目在这之前已经写完很久了,但是一些关键部署点一直没有跟上进度。此系统是用Java编写的多线程程序的,主要是从Oracle中读取数据,然后根据业务进行统计运行,在将计算结果放回Oracle中。

这个系统的性能的直观指标,就是每天能处理多少数据量。这个指标很不理想,速度很慢,拖后与现场数据生成的速度。调优的标准,就是每日处理量。

第一次优化: 提高JDBC的fetchSize

首先,分析系统各个环节的时间消耗,弄清瓶颈在哪里。

在进行了一些综合分析后,得出主要时间耗费在数据库的查询上。而从v$session_wait中查询,发现有很多 SQL*Net message from client 事件的等待,时间很长,有数百秒之多。 显然是客户端慢了。但是因为应用端是有连接池的,是不是空闲连接导致的呢? 有一些怀疑。

接下来,在数据库上用 sqlplus 执行了相同的SQL,测量其时间其实是很快的,远远小于应用消耗在数据获取的时间。联系其上面提到的等待事件,事情变逐渐明朗: 网络传输慢了。

于是检查了代码,发现大批量的数据查询竟然没有提升fetchSize属性。在调整该属性为500以后,很快收到了正面反馈。处理速度提升了将近一倍。

回去在检查Oracle中的等待事件,发现已经降到200名以外了(是与其他应用共用数据库的)。

有关 sqlnet message from client 事件的分析,可以参考 Asktom 上的文章 AskTom上的另一篇文章,则描述了类似于连接池等待,或者是在sqlplus中打开会话不操作也会导致该事件。所以在见到这个事件时还是需要仔细甄别一下的。

结论: 适当调整 FetchSize是很有必要的。Oracle JDBC默认的fetchSize为10。FetchSize的提升,一方面是减少了客户端与服务器之间的来回交互次数;另一方面,也会有效减少数据库服务器逻辑读的数量(记得是《Troubleshooting Oracle Performance》中有讲到)。

修改完成后,除了观测到 sqlnet message from client 事件消失外,发现 sys cpu 也有小幅增加。

第二次优化:减小锁定(lock)范围

但是,接下来的观测,证明这个速度还是不合意,达不到要求。于是继续分析。

仍然是分析其主要的时间消耗在什么地方。这次发现时间主要是消耗在程序对数据的处理过程中。数据的处理大部分都是在Java进程中的运算,处理过程中还可能调用到Memcached。是不是网络速度比较慢呢?尝试测试了一下网速,千兆网络的吞吐率在应用并发时还能挤出三分之一左右用来做测试文件的传输,用ping发现其latency也不算高。

在此前的观测中,发现应用服务器上的 user cpu 比较好,在30%~70%之间,主要是被JVM进程消耗了,系统负载在5.0左右。觉得有些奇怪的,因为一般这样的应用瓶颈都出在IO上。于是做了一次代码复查,很快发现有一个在方法签名上的synchronize 关键字。这个类是一个服务bean, 在spring中管理的。这时问题变得清晰起来,所有的处理任务线程都要在这里排队。这样正好符合了此前观测到的cpu现象。

改掉这个关键字,缩小锁定范围,速度又上升了将近一倍。user cpu很快降下来,系统负载也下来了,稍稍大于1。

此时,速度虽能勉强应付,但还是不够快。此时更多的时间消耗在数据的写回上面。处理速度提升了,写回阶段的压力便也迅速提升,每单位数据的写回时间有20%左右的上浮。于是,需要第三次优化,在数据写回方面提升速度。

第三次优化

(Update Apr 6th):
没心思再仔细写过程了,简略如下:
1) Oracle是多个系统共享的,数据写入成为瓶颈。在内存问题解决后,在本地内存中缓存结果,最后再集中提交,减少更新次数。数度大增2.5倍左右。
2) 将从数据库中的查询和在内存中的处理划分为两个线程并发处理,速度提高50-70%。
3) 然后,Memcached集群成为瓶颈,内网延迟成为问题。但此时速度已经足够快了,因此暂时搁置下一步优化。如果要继续提速,则可能在本地建立一个Memcached实例,或者在程序中使用一个足够大的LRU缓存以减少与Memcached交互的次数。

至此,数据后台处理速度完全达到并超过预期。

Categories: $Performance Tags: , , , , ,

Java中的Volatile关键字

December 13th, 2010 西坪 2 comments

前两天,并行实验室发表了一篇有关volatile关键字的文章[1],而该文参考文献中的Sayonara volatile[2]则更直接地要与volatile再见。两篇文章不谋而合,从c/c++到Java/.net,将这几门语言中的volatile过了一遍。列举出各种乱象,看上去volatile是如此不堪。

确实,volatile的语义不是那么常见。不是锁,但是它又拥有锁的部分特性(可见性,Java 5以后还有顺序保证)。如果不能正确理解,确实是个容易出错的地方。但是如果理解了用对了,仅就Java 5以及以后的版本而言,在目前阶段,volatile对性能上的提升还是有帮助的。

Volatile在Java中的语义以及历史

维基百科上,对Java中的Volatile给了一个准确的定义[6]

  • (In all versions of Java) There is a global ordering on the reads and writes to a volatile variable. This implies that every thread accessing a volatile field will read its current value before continuing, instead of (potentially) using a cached value. (However, there is no guarantee about the relative ordering of volatile reads and writes with regular reads and writes, meaning that it’s generally not a useful threading construct.)
  • (In Java 5 or later) Volatile reads and writes establish a happens-before relationship, much like acquiring and releasing a mutex.

翻译过来就是:

  • (在所有的Java版本中)对volatile变量上的读和写有一个全局排序。这暗示着每个线程访问一个volatile域时在往下运行之前将读到当前的值,而不是(有可能)使用一个缓存值。(然而,在volatie变量的读写与常规的读写之间的排序没有保证,意味着volatile通常不是一个有用的线程化的构造)
  • (在Java5及之后版本中) Volatile读和写建立了一种happens-before关系,就像获取和释放一个互斥体。

在Java中,要正确地并发,必须在原子性、可见性和顺序性三个方面做出保证。在Java 5以前的版本中,volatile就已经实现了可见性,但是因为不能保证与普通变量读写之间的顺序,故而经常是没有用的。在Java 5以后的版本中,则额外保证了其与普通变量读写的顺序性。这里不是特别好懂,我们将javamx上的例子[3] [4]改造一下:

public class MyBrokenFactory {
  private static volatile MyFactory instance;
  private int field1, field2 ...
 
  public static MyBrokenFactory getFactory() {
    // This is incorrect: don't do it at home, kids!
    if (instance == null) {
      synchronized (MyBrokenFactory.class) {
        if (instance == null)
          instance = new MyBrokenFactory();
      }
    }
    return instance;
  }
 
  private MyBrokenFactory() {
    field1 = ...
    field2 = ...
  }
}

上面的代码,在Java5以前,这个例子是不能正确工作的,因为volatile不保证对field1,field2的初始化(常规读写)一定在对volatile变量instance的写之前完成。但双检查锁本身并非volatile导致的,反而是Java 1.5的volatile让双检查锁能正确工作了。并行实验室将双检查锁不正确算在volatile的头上,有点冤了。

正是因为volatile在Java 5以前没有得到正确实现,更添加了不少复杂性。在Java 5之前,volatile基本没用处。

Volatile的实现机制分析

在Hotspot JVM中,
a) 在JVM层次,对volatile变量在线程本地工作区中不做缓存,对volatile的读写总是指向堆中的引用。可以视作在一个assign指令后总是跟着一个store指令[5]
b) 在机器码执行层次,通过内存屏障指令等迫使CPU不重排序,清除缓存,详细请参考《Memory Barriers and JVM Concurrency》的分析。

这与synchronized是有明显不同的,synchronized通常需要原子锁定,在SMP上要通过锁定总线等方式来实现,其代价在大多数平台上通常要比volatile高得多。

Performance

图一运行100万次
引入Volatile的主要目的,就是为了性能。我分下面几个方面来谈谈volatile在目前的Java平台中的作用。

直接性能比较

简单写了一个测试来比较一下volatile以及它的两种替代品 — 原子类型(atomic)和锁(此处指synchronized)。例子的代码改编自庄周梦蝶slides《Java NIO trick and trap》中的SystemTimer(部分代码省略,完整源码在Github上)。这个测试只是一个例子,但其实在网络库里都要做定时,原理也大抵如此。

第一个版本如下:

package me.xiping.volitileverify;
public class SystemTimerV1 {
	private final static ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
	private static final long tickUnit = Long.parseLong(System.getProperty(
			"notify.systimer.tick", "50"));
	private static volatile long time = System.currentTimeMillis();
 
	private static class TimerTicker implements Runnable {
		public void run() {
			time = System.currentTimeMillis();
		}
	}
 
	public static long currentTimeMillis() {
		return time;
	}
 
	static {
		executor.scheduleAtFixedRate(new TimerTicker(), tickUnit, tickUnit,
				TimeUnit.MILLISECONDS);
		Runtime.getRuntime().addShutdownHook(new Thread() {
			@Override
			public void run() {
				executor.shutdown();
			}
		});
	}
}

代码的重点在于volatile修饰的time域。time是一个只有一个线程写,其他线程都是读的域,没有并发修改,这里要解决的就是可见性。这正是volatile最适合的场合。

第二个版本使用synchronize关键字,与版本V1的不同如下:

package me.xiping.volitileverify;
public class SystemTimerV2 {
	....
	private static long time = System.currentTimeMillis();
 
	private static class TimerTicker implements Runnable {
		public void run() {
			synchronized (SystemTimerV2.class) {
				time = System.currentTimeMillis();
			}
		}
	}
 
	public static synchronized long currentTimeMillis() {
		return time;
	}
	....
}

第二个版本使用Java的内部锁来同步,既保证了原子性,也保证了可见性。第三个版本则是使用AtomicLong来实现的,如下:

package me.xiping.volitileverify;
 
public class SystemTimerV3 {
	......
	private static AtomicLong time = new AtomicLong(System.currentTimeMillis());
 
	private static class TimerTicker implements Runnable {
		public void run() {
			time.set(System.currentTimeMillis());
		}
	}
 
	public static long currentTimeMillis() {
		return time.get();
	}
	.....
}

在我的本上(cpu: 2core; OS: win7/cygwin; java:1.6.0_13)运行100万次,其性能如上面右图一所示(单位:ms)。
图二运行1千万次
运行1000万次性能如图二所示(注意纵坐标的值与图一不一样)。

结论:性能上的优越性是显而易见的。volatile比AtomicLong明细要快,相比于synchronized则有几倍几十倍的提高了。
(欢迎在不同平台下测试并在评论中给一个反馈,反馈时请包括处理器信息(平台,core数),OS和Java版本。源代码在这里。编译和运行需要Ant,具体指南请参考这里。)

[Update, Dec15th: 图片是用Google Chart API 生成的,在源代码中包含着两个自动执行测试并生成图片URL的脚本。脚本需运行在Bash环境下,Windows上需在Cygwin下运行。]

Mina/netty对volatile的利用

写博文时顺便统计了下volatile在一些性能比较重要的程序中的情况,我顺手统计了手边有的mina和netty两个项目的核心代码,数据如下:

Project sources path synchronized occurrence volatile occurrence
netty src/main/java 150 177
mina src/main/java 216 55

其中,mina是2009-3-31日Subversion的trunk,如下:

$svn info
Path: .
URL: http://svn.apache.org/repos/asf/mina/trunk
Repository Root: http://svn.apache.org/repos/asf
Repository UUID: 13f79535-47bb-0310-9956-ffa450edef68
Revision: 761900
Node Kind: directory
Schedule: normal
Last Changed Author: jvermillard
Last Changed Rev: 760493
Last Changed Date: 2009-03-31 23:49:15 +0800 (Tue, 31 Mar 2009)

netty的版本信息是:

$git log
commit 1ffb1aea75c36def56b709bd0892b19df78d9249
Author: Trustin Lee <trustin@gmail.com>
Date:   Fri Nov 12 10:20:03 2010 +0900

从侧面证明了volatile其实是很重要的,在性能很重要的场合,应用还是比较多的。

结论

总而言之,相比与Atomic和synchronized,volatile在性能上还是有显著的优势。

不过,正如文章中开头讨论的那样,volatile的缺点也显而易见,需要开发者对volatile的应用需要对内存模型的理解,否则容易误解而造成错误。在并发程序中,其仅可用于JVM支持的几个基本类型(int,short,long,double,reference, etc.),而这几个基本类型皆有对应的完整并发特性的包装原子类型(java.util.concurrent.atomic.*),虽然其性能与volatile有些差距,但大部分情形下也可代替volatile(但用synchronized来代替volatile是很不划算的)。

  1. 为什么在多核多线程程序中要慎用volatile关键字?
  2. Sayonara volatile
  3. Double-checked Locking (DCL) and how to fix it
  4. Double-checked Locking (DCL) and how to fix it (ctd)
  5. JVM specification#Threads and Locks
  6. Volatile variable#Java
  7. http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
  8. Memory Barriers and JVM Concurrency
Categories: $Performance Tags: , ,

JDBC和Ibatis中的Date,Time,Timestamp处理

November 25th, 2010 西坪 No comments

在此前,遇到过使用Ibatis操作Oracle时时间精度丢失的问题,昨天又遇到JDBC操作MySQL时间字段的问题,从网上看到各种式样的解释这些问题的博文/帖子,但多是雾里看花,不得要领。

  1. 理解JDBC中的时间类型
  2. MySQL与JDBC之间的类型映射
  3. Oracle与JDBC之间的类型映射
  4. Ibatis是怎么处理日期时间类型的
  5. 注释
  6. 参考资料

理解JDBC中的时间类型

java.sql包中包括三个类,Date, Time, 和 Timestamp,分别用来表示日期(无时间信息,eg: YYYY-MM-DD),时间(只处理时间,无日期部分, eg: HH:MM:SS)和时间戳(精确到纳秒级别)。在 它们都继承自java.util.Date。java.sql.Date和java.sql.Time都存有一个时间域,该时间域是精确到秒级别的,但是,他们只处理日期或者时间部分。在MySQL Connector/J(v5.1.13)的实现中可以看到,PrepareStatement#setDate 时会将时间重新format成”yyyy-MM-dd”格式。

因为历史遗留以及各种数据库本身的不同,各种JDBC实现中留下了格式各样的花招, 某些特殊场景下不遵照此情况可能也能工作,但不推荐这样做。

你必须根据你的实际需要来选择不同的类型,通常应该选用精确度与相应的数据库字段类型相比相同或者更高的JDBC类型

除此以外,还可以使用java.uitl.Date类型来处理时间。java.util.Date类型是上面各个类型的父类型,JDBC的API大都可以使用。

除此以外,在JDK1.6之前版本的构造java.sql.{Date|Time|TimeStamp}对象时存在性能问题,尤其是在多线程环境下更严重。这个Bug在这里。其原因是java.util.Date的相关方法调用了TimeZone.getDefaultRef(),而此方法是同步方法注1

MySQL的JDBC类型映射

DATE java.sql.Date
DATETIME java.sql.Timestamp
TIMESTAMP[(M)] java.sql.Timestamp
TIME java.sql.Time
YEAR[(2|4)] If yearIsDateType configuration property is set to false, then
the returned object type is java.sql.Short. If set to true (the
default) then an object of type java.sql.Date (with the date
set to January 1st, at midnight).

MySQL的DATETIME、TIMESTAMP两种字段类型的显著区别在于TIMESTAMP的取值在’1970-01-01 00:00:01′ UTC 和 ‘2038-01-19 03:14:07′ UTC之间。

MySQL在时间处理方面也有一个问题,当Datetimes类型的字段的值为0000-00-00时取值方法会得到下面的异常:

Cannot convert value '0000-00-00 00:00:00' from column xx to TIMESTAMP

这个问题的原因在于,MySQL中默认使用0000-00-00等来表示时间的特殊值(参见文档)。而在Java中,并没有一个合适的方式来表示这个时间(因为Java中时间轴上0是1970-01-01 00:00:00),早于这个时间的用负数表示,这个最小的负数在时间轴上是表示不出来的。Connector/J提供了一个属性zeroDateTimeBehavior来解决此问题。

  • exception (the default), which throws an SQLException with an SQLState of S1009.(默认行为)
  • convertToNull, which returns NULL instead of the date.
  • round, which rounds the date to the nearest closest value which is 0001-01-01.

如下所示的jdbc连接将指定该行为转化为null值。

jdbc:mysql://localhost/myDatabase?zeroDateTimeBehavior=convertToNull

Oracle与JDBC之间的类型映射

DATE java.sql.Date
DATE java.sql.Time
TIMESTAMP java.sql.Timestamp

Oracle数据库字段类型主要有DATE、TIMESTAMP。

在9i以后、11g以前的Oracle JDBC驱动中存在一个会丢失DATE类型字段的时间信息的bug,原因是其JDBC驱动将Oracle的Date类型处理为java.sql.Date类型,这就丢失了时间部分(看来对java.sql包下三种时间类型认识不足的问题还很普遍的)。关于这个问题,这篇帖子给出了不错的解释(墙外),此文中的方法适用于11g JDBC以前的各版本驱动。在11g JDBC驱动中,此问题已经解决了,Oracle 11g JDBC驱动的手册中也给出了解释注2

事实上,如果是使用Ibatis,pojo属性的类型设置为java.util.Date,确保 jdbcType不为 DATE或者TIME,则避免了这个bug。因为此时Ibatis会以java.sql.Timestamp来处理该字段。我专门对此做了验证,点此看测试项目源码

Ibatis是怎么处理日期类型的

注意:本文皆基于ibatis 2.3.4.726源码分析。不过根据我初略观察,Ibatis3也适用,但请遇到问题时有所留意。

在此前工作中碰到Oracle中的Date字段会只剩下日期部分的数据,丢失了,Google发现一些人的解决方法是将JDBCType指定为datetime。有人甚至自己编写一个自定义的TypeHandler来解决这个问题。其实这完全是瞎猫撞上死耗子,那个datetime根本没意义,却歪打正着。一般的错误都是如下的配置(或者是pojo的属性为java.sql.Date类型):

<sqlMap namespace="Info" >
  <resultMap id="Info" class="pojo.Info" >
    <result column="INFO_BEGINTIME" property="begin" jdbcType="DATE" />
    <result column="INFO_ENDTIME" property="end" jdbcType="DATE" />
  </resultMap>

此时不论你pojo.Info中的字段类型(或者的javaType属性)是java.util.Date还是java.sql.Date,最终都会丢失数据。实际上,在pojo.Info中的字段类型(或者javaType属性)为java.util.Date时,如果指定jdbcType为DATE或TIME(区分大小写),则将分别得到DateOnlyTypeHandler或TimeOnlyTypeHandler。如果你不指定jdbcType,或者指定一个错误的值,例如datetime,或者xxxx等,都会得到DateTypeHandler(日期时间都会处理)。

如果pojo.Info中的属性类型(或者配置中的javaType属性)是java.sql.Date,则选择处理handler类时不会使用jdbcType,而是根据属性类型得到SqlDateTypeHandler(只包含日期)。

在Ibatis的手册中,指出来jdbcType一般情况下是不用于判断处理方式的注3。大部分情况下是依据javaType或者pojo属性类型来判断处理方式,在少部分无法根据pojo属性类型判断的情况下才使用。如果你用java.util.Date对应到MySQL,则就是这种少数情况之一。因为MySQL可以将多个数据库字段类型映射到java.util.Date,所以需要指定是DATE还是TIME(必须是大写),如果不指定则默认是完整的日期时间(此时按JDBC的timestamp操作)。

针对ibatis对时间的处理,我写了个测试,点此看测试代码。

对于Ibatis操作Date/Time/DateTime,总结如下:

  • 将pojo的属性类型设置为java.sql.Date(或java.sql.Time, java.sql.Timestamp),此时会严格遵循这三种类型的语义。但此方法因存在前文中提到的性能问题,在JDK1.6以前的JDK版本中能少使用就少使用。
  • 如果你想在pojo中使用java.util.Date, 则要注意:
    • 完整的日期时间,要确保jdbcType为空,或为DATE,TIME以外的值
    • 只需要时间,要指定jdbcType=”TIME”
    • 只需要日期,要指定jdbcType=”DATE”

注释

注释1:Use oracle.sql.DATE or oracle.sql.TIMESTAMP rather than
java.sql.Date or java.sql.Timestamp if you are using JDK 1.5 or earlier versions or require maximum performance. You may also use the oracle.sql data type if you want to read many date values or compute or display only a small percentage. Due to a bug in all versions of Java prior to JDK 1.6, construction of java.lang.Date and java.lang.Timestamp objects is slow, especially in multithreaded environments. This bug is fixed in JDK 1.6.

注释2:Mapping SQL DATE Data type to Java Oracle Database 8i and earlier versions did not support TIMESTAMP data, but Oracle DATE data used to have a time component as an extension to the SQL standard. So, Oracle Database 8i and earlier versions of JDBC drivers mapped oracle.sql.DATE to java.sql.Timestamp to preserve the time component. Starting with Oracle Database 9.0.1, TIMESTAMP support was included and 9i JDBC drivers started mapping oracle.sql.DATE to java.sql.Date. This mapping was incorrect as it truncated the time component of Oracle DATE data. To overcome this problem, Oracle Database 11.1 introduces a new flag mapDateToTimestamp. The default value of this flag is true, which means that by default the drivers will correctly map oracle.sql.DATE to java.sql.Timestamp, retaining the time information. If you still want the incorrect but 10g compatible oracle.sql.DATE to java.sql.Date mapping, then you can get it by setting the value of mapDateToTimestamp flag to false.

注释3:Ibatis 2的手册中给出的jdbcType属性的解释:属性jdbcType用于显式地指定给本属性(property)赋值的数据库字段的数据类型。对于某些特定的操作,如果不指定字段的数据类型,某些JDBC Driver无法识别字段的数据类型。一个很好的例子是PreparedStatement.setNull(int parameterIndex, int sqlType)方法,要求指定数据类型。如果不指定数据类型,某些Driver可能指定为Types.Other或Types.Null。但是,不能保证所有的Driver都表现一致。对于这种情况,SQL Map API允许使用parameterMap子元素parameter的jdbcType属性指定数据类型。
正常情况下,只有当字段可以为NULL时才需要jdbcType属性。另一需要指定jdbcType属性的情况是字段类型为日期时间类型的情况。因为Java只有一个Date类型(java.util.Date),而大多数SQL数据库有多个-通常至少有3种。因此,需要指定字段类型是DATE还是DATETIME。
属性jdbcType可以是JDBC Types类中定义的任意参数的字符串值。虽然如此,还是有某些类型不支持(即BLOB)。本节的稍后部分会说明架构支持的数据类型。
注意!大多数JDBC Driver只有在字段可以为NULL时需要指定jdbcType属性。因此,对于这些Driver,只是在字段可以为NULL时才需要指定type属性。
注意!当使用Oracle Driver时,如果没有给可以为NULL的字段指定jdbcType属性,当试图给这些字段赋值NULL时,会出现“Invalid column type”错误。

参考资料

Categories: $Programming Tags: , , , ,

Java平台上的XML处理梳理

September 30th, 2010 西坪 No comments

做了几年Java了,也经常用XML,但是鉴于Java平台对于XML处理相关的API和实现十分混乱,一直没有完全理清其关系。而又有如此多的名词跟XML沾亲带故,尤其是Java平台里名词满天飞。你应该见过这些名字吧:SAX,DOM,JAXP,JDOM,DOM4J, JAXB , JAXM , JAXR , XLST,XPATH, JAX-RPC, XML-RPC,XForms,你都知道吗?在维基百科XML页面上还有这么一句话:”As of 2009, hundreds of XML-based languages have been developed”(截止到2009年,有数百基于XML的语言被开发出来),可能也不只Java平台上XML处理这么复杂。

基础技术

XML

XML,全称可扩展标记语言(Extensible Markup Language (XML) )是在W3C制定的几个文档中定义的,用于将内容编码为机器可读的格式。

XML我们经常接触或者使用,其支持Unicode编码、严格完整的标记和错误处理等等,大家都知道。XML最早使用DTD来对XML的内容和格式进行定义和校验,后来W3C开发了新的模式语言XML Schema来取代DTD。于是,现在我们经常见到用来规范XML格式和内容的Xsd文档,DTD 已经逐渐淡出了。

XML的编程接口

XML作为一种数据格式,其设计目标中就包括了能够轻易地为各种编程语言提供接口。概括起来主要是下面这四个类型。

  1. 面向流的API,例如 SAX 和 StAX。
  2. 树遍历的API, 例如 DOM。
  3. XML数据绑定,支持在程序语言的对象与XML文档之间自动转换。(JAXB)
  4. 声明式转换语言,例如 XSLT 和 XQuery。

Sun公司给Java语言定义了一个将上面这数种编程接口(DOM/SAX/StAX)统一的API,JAXP(Java API for XML Processing),其目的是在Java平台上统一处理XML的接口。

Java平台中怎么处理XML

DOM和SAX

DOM的全称 Document Object Model(文档对象模型),是将XML在内存中组织为一个树形结构,并按照树形结构遍历和访问。不论是使用Dom4j,或者是Javascript里,经常看到Node/Element/Attribute这些东西,这就是使用DOM操作XML/XHTML等。DOM操作的最大缺点就是要将所有东西装到内存中,方便频繁查询、修改。一般的配置文件都比较小,使用DOM问题都不大。但是数据量很大的情况下,或者要处理的XML很多,那最好不用它。

SAX的全称是 Simple API for XML, 是基于流的方式来访问XML的。CSDN的这篇博文解释道:”SAX 解析器采用了基于事件的模型,它在解析 XML 文档的时候可以触发一系列的事件,当发现给定的tag的时候,它可以激活一个回调方法,告诉该方法制定的标签已经找到。SAX 对内存的要求通常会比较低,因为它让开发人员自己来决定所要处理的tag。特别是当开发人员只需要处理文档中所包含的部分数据时,SAX 这种扩展能力得到了更好的体现。但用 SAX 解析器的时候编码工作会比较困难,而且很难同时访问同一个文档中的多处不同数据。”(该博文有转字样,不知道其出处是哪,没搜到)。

对于SAX/DOM相关的论述, 上面提到的CSDN的博文关于SAX,DOM,JAXP,JDOM,DOM4J的一些理解 有比较精彩的论述。

有关DOM/SAX两种处理接口在处理大量XML数据时的性能,Tinyfool博文提供了一组数据为证。

Java平台这两种API的实现

在Java的类库中,Dom4j最早就是以DOM方式解析XML的。现在dom4j也支持其他方式解析XML,完整实现了DOM, SAX 和 JAXP。如果你使用Dom4j,在JDK5之前的JDK版本中则需要从JAXP的主页上下载相关的API包。

JDOM是另一个基于DOM模型处理XML的类库,但似乎已经不那么流行了。

Apache的xerces项目下有C++/Java/Perl三种语言的XML解析器。Apache Xerces2 Java 是Java语言的XML解析器。使用Apache xerces-J需要两个jar包,第一个是xml-apis.jar,其中包含了sax dom jaxp等接口的定义,另一个jar包是 xercesImpl.jar,包含各个接口的实现。以前的Xercers包中还包括一个xerces.jar,现在已经废弃了(参考这里)。

XML数据绑定

JAXB,全称是Java Architecture for XML Binding,是Java EE平台中处理XML与Java对象之间数据绑定的API。名如 jaxb1-impl.jar, jaxb1-impl-2.0.jar等的包,就是这个接口的相关产物。这个API也是JAXP的一部分,Xecers-j和dom4j都提供了实现。

Apache Xmlbeans 也提供了处理XML与Java对象间数据绑定的功能。

Java平台中的声明式转换语言

XSLT, 可扩展样式转换语言,常用于将信息内容与展现分离。在《企业应用架构模式》中将之作为表现层的一种模式做了介绍。使用XSLT,同一份内容,能够转换成HTML,文本文件,PDF,Postscript等等。处理XML的声明式转换语言还有从XML中查找节点的XPATH,用于从XML集合中查询数据的XQuery等。此前在Flex平台的项目中用过XPATH来处理界面,还是很方便的。

你可能看到过xalan.jar,有的项目里还必须有这个库。其实这是Apache的Xalan-Java的库,是一个XSLT处理库,实现了 JAXP中的javax.xml.transform接口。Xalan可将XML文档转换为HTML,文本文档或者其他XML格式。这个包依赖实现了SAX接口的相关包,所以必须与实现SAX接口的包一起使用。Xalan官方推荐的是Xercers(参考这里)。

另一个可以实现了XML的声明式转换相关接口的类库是Saxon项目的Java子项目。

结语

有关Java中这些XML处理方式,Apache这里有个FAQ可以参考一下,这个FAQ尝试回答为什么Java里有这么多XML处理的实现,接口和jar包。

总结这些东西好累,Java平台上处理XML实在是太累了。这大概也与XML被用得如此宽广有关。XML作为一种严格规范化的描述能力很强的数据格式,确实无处不在。虽然XML有时很臃肿,但基于XML的技术如XFormJava API for XML-Based RPC (JAX-RPC)XML-RPC等,很多很多。

(本文涉及内容较多较杂,我只是尝试做一个整理,如有谬误请不吝赐教)

Categories: $Programming Tags: ,

DBCP连接池(一):原理与基本配置

January 29th, 2010 西坪 1 comment

大约在半年前,曾经帮同事解决过一个Commons-dbcp连接池的问题。当时遇到的问题比较诡异,但是其实并不是什么特别复杂的问题,了解DBCP的原理,大部分问题就迎刃而解了。本文主要对连接池的基本原理以及dbcp的实现方式做一个分析,对dbcp的配置参数结合原理做一个简单解释。

连接池扼要

JDBC是一套通用的Java语言与多种数据库(文件)通讯的标准API。大部分针对数据库服务器(例如Oracle, MySQL等等)的JDBC实现都是基于TCP/IP连接的客户端-服务器端通讯方式。

当我们需要执行一个数据库操作时,有下面三步:

  1. 客户端与服务器之间建立一个数据库连接
  2. 执行某种数据库操作
  3. 断开连接

如果每次处理都要走上面的三步,则应用程序与数据库服务器都要将大量的时间和资源消耗在数据连接的断开与建立上。对于某些数据库系统,一个数据库连接就是一个进程,而且数据库连接通常要占用不少资源,如排序区/Join区等等。对于并发较大的系统,建立一次连接然后缓存起来连续使用,直到程序结束等情况下再释放连接,就能够将系统资源集中在对数据库操作的处理上,从而大大提高性能。通常情况下将数据连接的建立和断开委托给一种能够数据库连接池的组件或服务进行管理。而DBCP, C3p0, Proxool等都是常用的开源的连接池组件。

就好像A公司在郊外,他们公司附近没有出租车。如果A公司有人要出去办事,他必须打电话给出租公司订车,用完车后他还要付账报销。 这样每个人出去一趟都必须订车、退车和报销。员工的很多时间白白花费在这上面了。于是A公司跟出租车公司定了一个合同,出租车公司给了他们一个车队。要用车随时去楼下找车队就可以了,用完了也不必结帐,A公司统一跟出租车公司订车和结帐。这个车队就好比连接池,由公司(应用程序)来统一向出租车公司(数据库服务器)订车(建立连接)和退车(关闭连接)。

DBCP的配置参数以及背后的原理

Commons-dbcp连接池的配置参数比较多,也比较复杂,主要分为

  • Jdbc连接参数(username, password, url, driverClassName, connectionProperties )
  • 事务处理参数 (defaultAutoCommit, defaultReadOnly, defaultTransactionIsolation, defaultCatalog)
  • 连接池参数(详见下文)
  • 连接池中链接存活性测试参数(详见下文)
  • 预处理查询池化参数(poolPreparedStatements, maxOpenPreparedStatements)
  • 丢弃失效链接相关参数(详见下文)以及一个控制是否可以正常情况下处于访问连接池包装下的底层JDBC链接参数(accessToUnderlyingConnectionAllowed)

其中Jdbc链接参数、事务处理都跟连接池关系不大,另预处理查询池化参数本文不详细叙述。有关commons-dbcp的详细参数配置信息请参考官方文档

连接池的配置

再用车队来比喻,出租车公司每提供一辆车给A公司,A公司肯定要付出一定费用。这时候维持车队的大小就很重要了,在项目少用车少的时候,车队肯定要减少,不然很多空闲车辆也要付出成本;在项目多用车多的时候,肯定要扩大车队,不然车不够用,车队忙不过来。A公司可以简单地设地两个阀值来动态调节车队中空闲的车辆数目以满足动态需求,一个是最小空闲车辆数(最小空空闲连接数(minIdle),当剩余的空车数目小于该数目时,A公司就向出租车公司请求加入新车。 一个是最大空闲车辆数(maxIdle),当剩余的车大于该数目时,就将刚刚用用完的车还给出租车公司从而减少车队数量。

另外,这家出租车公司可能要为多个客户服务,要考虑能够提供给A公司的最大的车数量,不能超过某个数量(maxActive)。所以A公司想出租车申请新车时首先要看下当前正在用的车辆数目是否超过了这个最大数目,如果没有超过那就直接申请新车,否则可以让申请者(应用程序中执行请求的线程)等待 (maxWait<=0, 无限等待; maxWait>0 当等待时间超过 maxWait时,失败)。

在连接池中,这几个参数是十分重要的,官方的说明如下,是我们调节系统性能时需要认真考虑的值。


Parameter Default Description
initialSize 0 The initial number of connections that are created when the pool is started.

Since: 1.2
线程池启动时初始化的连接数
maxActive 8 The maximum number of active connections that can be allocated from this pool at the same time, or non-positive for no limit.
最大活动连接数,如果非正整数,则不做限制。
maxIdle 8 The maximum number of connections that can remain idle in the pool, without extra ones being released, or negative for no limit.
最大空闲连接数。
minIdle 0 The minimum number of connections that can remain idle in the pool, without extra ones being created, or zero to create none.
最小空闲连接数。
maxWait indefinitely The maximum number of milliseconds that the pool will wait (when there are no available connections) for a connection to be returned before throwing an exception, or -1 to wait indefinitely.
(在没有连接可用时)连接池等待一个数据连接可用时的以毫秒计的最大等待时间,超时以后抛出异常, -1 则将无限等待

实际上,Dbcp 依赖于 commons-pool 来存储连接对象。 BasicDataSource默认使用GenericObjectPool来管理连接对象。除了请求的线程会在请求和返回连接过程中影响池中连接实例外,另有一个跑着GenericObjectPool.Evictor类型 (implements Runnable) 的实例的线程,也会影响池中的数据库连接。

请继续阅读下一篇:《DBCP连接池(二):Commons-pool的设计》

文件锁与进程互斥

January 8th, 2010 西坪 3 comments

有的情况下需要在进程之间实现互斥,这对于普通Java程序来说,因为是运行在虚拟机里的原因,手段远不如原生程序方便,但是也有一些简单的方法可以做到。第一个就是使用Socket监听某个端口,第二种方法是使用文件锁定。

使用Socket实现进程互斥

因为Java服务器套接字都是基于系统的socket接口实现的,而socket接口会保证一个套接字端点在整个系统中是唯一的,所以只需要在应用启动时监听一个端口,就可以让系统不同时启动。虽然方法比较土,但是也能用。

try {
	ServerSocket ss = new ServerSocket(3030);
}catch (Exception e) {
	logger.error("Cannot start", e.printStackTrace());
	System.exit();
}

使用文件锁在进程间同步

另一个实现Java进程间互斥的方法,就是文件锁。不过与上面使用套接字不同,使用文件锁还可以实现更高级的互斥/协作。完全可以用文件锁来实现不同Java进程之间的同步,基本思路与在线程之间使用读写锁同步相似。

JDK本身直接支持文件锁,在FileChannel下有几个与文件锁有关的方法,可以打开一个FileLock对象。当调用FileLock#release()方法,或者Channel关闭,或者JVM退出时都会释放该锁。文件锁是JVM全局的,与Java内部的锁/线程等无关,是进程之间的竞争关系。如果你不想发生意外你的程序挂起,那就应该用FileChannel#trylock,而不是直接使用FileChannel#lock(),这样在不能加锁时可以休眠一段时间再等等,或者超时退出。

文件锁像读写锁一样,可以是加共享锁,也可以加独占锁,还可以给锁定限制范围。这是操作系统本身提供的功能,JDK在操作系统层面上做了封装。在Linux下,其实就是调用fcntl来设置相关的状态。如下面的代码,会给文件加独占锁,如果再起新的进程,进程就会打开文件失败退出。

     char* lockfile;
      int lockfd;
      struct flock lock;
      time_t t;   
 
      if ((lockfd = open(lockfile, O_RDWR)) == -1) {
          fprintf(stdout, "open file %s fail: %s \n", lockfile, strerror(errno));
          exit(1);
      }
 
      t = time(NULL);
 
      lock.l_type = F_WRLCK;
      lock.l_start = 0;
      lock.l_len = 0;
      lock.l_whence = SEEK_SET;
      if (fcntl(lockfd, F_SETLKW, &lock) == -1) {
          fprintf(stdout, "lock file %s fail: %s \n", lockfile, strerror(errno));
      }
      fprintf(stdout, "lock file %s success(%d): %ld \n", lockfile, (int)getpid(), time(NULL)-t);
      sleep(30);
Categories: $Programming Tags: , , ,

Arguments on Java & C++

December 23rd, 2009 西坪 No comments

The Hypertable Team claimed they choose C++ to be Hypertable implement language for performance issues. They gave two points about that, Hypertable is memory (malloc) intensive and and CPU intensive. They also say:

(Hadoop)There are some places where Java is sub-optimal. In particular, at scale, there will be considerable memory pressure in the Namenode of the DFS. Java is a poor choice for this type of memory hungry application. Another place where the use of Java is sub-optimal is the post-map sorting in preparation for the reduce phase. This is CPU-intensive and involves the type of CPU work that Java is not good at.

There are a bunch comments on the Wiki Page. Someone gave another views. A guy gave two links to support Java is not so bad.

Some interesting comparative articles.

http://www.ddj.com/cpp/184401976?pgno=1

http://www.idiom.com/~zilla/Computer/javaCbenchmark.html

Categories: $Programming Tags: ,

理解Java虚拟机的加法运算

October 16th, 2009 西坪 No comments

在CSDN博客诗剑书生的专栏,作者提到了short类型的+操作和++操作的区别,竟然没有一个正确答案。其实这个问题没那么复杂,关键是要了解Java计算的最小单位和理解类型的自动转型。
原问题是:

short tmp = 0;
为什么tmp = tmp +1;错误但 tmp ++;却正确.

理解这个问题,先要理解实现计算的基本原理。Java虚拟机是一种进程虚拟机,它负责解释和执行字节码,跟直接运行在硬件上运行汇编指令是有区别的。普通的机器语言可以直接操作寄存器完成计算,寄存器能够表示的最小单位是字节。而Java的计算是基于栈的,并规定其基本操作单位是一个字。字的实际大小是有虚拟机的设计者来决定的,虚拟机规范只是规定一个字长要能够放下一个 byte、short、 int、 char、float等类型。所以byte、short、char的运算都像 int 一样是在以字为单位来计算的,尽管它本身比字还要小。

此时我们不难理解,tmp+1会得到一个int。根据Java的类型语义,可以向上转型,不会自动向下转型。所以 tmp = tmp+1语法错误, 关键在 = 符号这里。那为什么 tmp++就是正确的呢? 因为++ 只有一个操作数,不涉及转型的问题, 不像 = 符号。简单比对下二者生成的字节码。

	short a = 2;
	a++;

生成的字节码如下,其中的第5行指令执行了自动转型,i2s指定是int类型的数据转换为short类型的数据。

  0:   iconst_2
  1:   istore_1
  2:   iload_1
  3:   iconst_1
  4:   iadd
  5:   i2s
  6:   istore_1
  7:   return

我们在看看 int c = a+b 生成的字节码:

	short a = 2;
	short b = 111;
	int c = a + b;

生成字节码如下:

   0:   iconst_2
   1:   istore_1
   2:   bipush  111 #压栈111
   4:   istore_2
   5:   iload_1  #加载a到栈
   6:   iload_2  #加载b到栈
   7:   iadd  
   8:   istore_3  #将计算结果存到局部变量c中
   9:   return

该文中提到的另一个错误:

short c = a + b

其实是一个原理,尽管a,b,c 都是short, 但是 a+b 实际上得到了是int, 于是不能自动转型为 short 给 c。

Categories: $Programming Tags: ,

细节背后:为什么线程协作之前必须先获得锁?

October 3rd, 2009 西坪 No comments

为什么Object.wait()/notify()/notifyAll() 之前必须获得锁? 这是JLS的规定。Wait-notify机制是围绕监控器锁进行的,获得锁是很自然的前提,自身没有拿到锁之前,怎么能够尝试去操作靠锁来调控的线程呢?不过今天偶尔有时间,就看下Sun Hotspot是怎么实现这一机制的。

当我们执行下面的代码时,线程会抛出异常java.lang.IllegalMonitorStateException: current thread not owner。

public class WaitNotifyCompilerCode {
	private String aString = "Hello World!";
 
	public static void main(String[] args) {
		System.out.println("Execute start ....");
		final WaitNotifyCompilerCode w = new WaitNotifyCompilerCode();
		w.wait1SecAndPrintString();
		System.out.println("Execute end ....");
	}
 
	public void wait1SecAndPrintString() {
		try {
			this.wait(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(aString);
	}
 
}

异常栈的信息如下:

Exception in thread "main" java.lang.IllegalMonitorStateException: current thread not owner
	at java.lang.Object.wait(Native Method)
	at com.feihoo.test.waitnotify.WaitNotifyCompilerCode.wait1SecAndPrintString(WaitNotifyCompilerCode.java:35)
	at com.feihoo.test.waitnotify.WaitNotifyCompilerCode.main(WaitNotifyCompilerCode.java:27)

深入查看 OpenJDK的源码,找到 Object.wait() 函数本地代码:

JVM_ENTRY(void, JVM_MonitorWait(JNIEnv* env, jobject handle, jlong ms))
  JVMWrapper("JVM_MonitorWait");
  Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
  assert(obj->is_instance() || obj->is_array(), "JVM_MonitorWait must apply to an object");
  JavaThreadInObjectWaitState jtiows(thread, ms != 0);
  if (JvmtiExport::should_post_monitor_wait()) {
    JvmtiExport::post_monitor_wait((JavaThread *)THREAD, (oop)obj(), ms);
  }
  ObjectSynchronizer::wait(obj, ms, CHECK);
JVM_END

重点看倒数第二行,调用的函数如下面的代码:

// NOTE: must use heavy weight monitor to handle wait()
void ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) {
  if (UseBiasedLocking) {
    BiasedLocking::revoke_and_rebias(obj, false, THREAD);
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
  }
  if (millis < 0) {
    TEVENT (wait - throw IAX) ;
    THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
  }
  ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj());
  DTRACE_MONITOR_WAIT_PROBE(monitor, obj(), THREAD, millis);
  monitor->wait(millis, true, THREAD);

而上面的代码中最后一行的函数里,在做实际操作之前调用了下面的宏:

// A macro is used below because there may already be a pending
// exception which should not abort the execution of the routines
// which use this (which is why we don't put this into check_slow and
// call it with a CHECK argument).
 
#define CHECK_OWNER()                                                             \
  do {                                                                            \
    if (THREAD != _owner) {                                                       \
      if (THREAD->is_lock_owned((address) _owner)) {                              \
        _owner = THREAD ;  /* Convert from basiclock addr to Thread addr */       \
        _recursions = 0;                                                          \
        OwnerIsThread = 1 ;                                                       \
      } else {                                                                    \
        TEVENT (Throw IMSX) ;                                                     \
        THROW(vmSymbols::java_lang_IllegalMonitorStateException());               \
      }                                                                           \
    }                                                                             \
  } while (false)
Categories: $Programming Tags: , ,