Another Java Geek

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

2012将至,气候变化无常。在一天里有雨有雾又有风的情况下,掌握天气预报情况就显得很有必要,虽然预报经常不准。不过像我这样工作时基本不开QQ又不收看天气预报的人,时刻了解天气变化多少有些困难。由于高度依赖浏览器,使用Firefox天气预报插件就是很自然的选择。网上有不少这类插件,不过我自己做了一个简易天气预报,主要是为了熟悉Firefox的插件开发,以便能够编写一些好玩的小工具。

在Mozilla开发者中心,对插件(Pluggin)和扩展(Extension)的定义有明确的区别。其实我采用的是扩展,只是经常习惯于说插件罢了。Firefox扩展使用Mozilla首创的XUL(类似于HTML)和Javascript来编写。XUL是一套基于XML的标记语言,类似于HTML,非常适合于编写桌面应用程序的UI(HTML用来编写WEB应用的UI),包含丰富的界面元素和组件,实际上Firefox浏览器本身的界面也是由一个XUL文档(chrome://browser/content/browser.xul)来描述的。一个扩展的界面是用XUL来编写,并通过Javascript来实现交互和功能逻辑,而这对我来说是驾轻就熟(自我感觉良好中)。

我的简易天气预报界面很简单,只有状态栏上一个panel用于显示天气情况,还有工具菜单下的一个菜单项用于设置城市。实现的功能是:初次安装时提示用户设置城市,扩展在此会尝试检测用户的属地(通过XMLHttpRequest去一个IP归属地网站访问一下即可),设定好城市后,扩展会从中央气象局的官网(weather.com.cn)上获取指定城市的天气预报情况(其实就是访问该城市的天气预报页面,再从页面HTML中解析出天气数据),然后在状态栏上显示出来,目前只是以文字形式显示今天、明天和后天的天气条件和高低温度。

扩展编写好之后,以zip格式打包,并将后缀改为xpi,在wordpress后台上传至服务器,通过虚拟主机的cpanel控制面板添加MIME类型:将xpi后缀映射为application/x-xpinstall,这样就可以提供下载安装了。

这个扩展相当简陋,并且只能在简体中文环境下使用,对得起简易二字了,非商业应用,只是出于个人兴趣所做。另外Forecastfox Enhanced这款天气扩展我也在使用,它可以实时展示天气和温度的变化,支持国际化,的确不错,比我这个好多了。

最近,安装了一个wordpress移动版插件,这样就可以通过手机来访问博客了。

要说wordpress手机版插件,其实它也挺多的。可要想整一个完全满意的,那可就难找了。

一开始安装了一个叫Wapple Architect的插件,这是我看到唯一的宣称能让手机版视图保持桌面版风格的插件。虽然是个人站点,不必像商业网站宣传品牌形象,但保持风格的一致性,使用起来还是会比较舒服。
Wapple Architect需要填写一些注册信息,获取一个注册码才能使用。我填写了注册信息,但迟迟收不到注册码,还以为注册是忽悠人的,可能是为了骗取email联系方式之类,于是就换上了另一个插件WordPress Mobile Pack。这个插件功能还算凑合,可以通过手机浏览和进行后台管理,只是主题比较单调,默认只有4个可选的颜色方案。

过了两天,gmail里收到了Wapple Architect注册码的邮件,还有一封插件作者的email,问我为什么注册了他们的插件,却没有使用,而是在用WordPress Mobile Pack,并怂恿我改用他的插件。我心想这哥们真够敬业的,于是重新换上Wapple Architect插件并激活,哪知道博客不能浏览了,访问直接报错。不得不禁用,并回邮件说明问题。几经周折,把问题搞定了。随后使用过程中陆续发现其它一些问题,邮件反馈之后,也一一得到答复或解决。不过这个插件只能用于手机浏览,在其作者的推荐下,又安装了他的另一个用于后台管理的手机版插件。

使用这两个插件,基本上能满足手机访问的需要。但也有一些不爽之处,识别手机终端的功能需要访问插件提供方的web services,理论上,是有可能将一些敏感信息发送过去,特别是进行后台管理操作(如密码修改)时。不过没有仔细研读过插件源码,不便下结论。

线程同步是进行多线程编程时所必须考虑的一个问题。之所以要进行同步,是因为多个线程需要访问共享资源,典型的是共享内存数据。如果能为每个线程提供一份需要共享的数据的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() - startTime .get();
		System.out.println(logFile.get() + " processed: " + time + "ms.");
	}

	public static void main(String[] args) {
		for (final String logFileName : args) {
			new Thread() {
				public void run() {
					LogStats.init(logFileName);
					LogStats.process();
				}
			}.start();
		}
	}
}

为简明起见,省略了部分代码。执行这个程序,可能会产生如下输出结果:

java LogStats access.log.1 access.log.2 access.log.3
access.log.1 processed: 1294538ms
access.log.2 processed: 1265327ms
access.log.3 processed: 1189563ms

在get/set方法被调用时,ThreadLocal能根据执行的上下文(即Thread.currentThread())确定变量的值。

从外观上看,ThreadLocal类似一个HashMap,其键相当于Thread.currentThread(),值为线程的变量值。据此实现一个自定义版本的ThreadLocal类型是比较直接的:

public class CustomizedThreadLocal<T> {
	private Map<Thread,T> map = Collections.synchronizedMap(new HashMap<Thread,T>());
	protected T initialValue () {
		return null;
	}

	public T get() {
		if (!map.containsKey(Thread.currentThread())) {
			map.put(Thread.currentThread(), initialValue());
		}
		return map.get(Thread.currentThread());
	}

	public void set(T value) {
		map.put(Thread.currentThread(), value);
	}

	public void remove() {
		map.remove(Thread.currentThread());
	}
}

可以使用CustomizedThreadLocal来代替LogStats中的ThreadLocal的功能,当然效率上可能会有所差别。

在LogStats代码中,对logFile和startTime的访问没有进行显式的同步。但这并不意味着使用Thread Local Storage模式时,可以完全消除同步,只不过是在应用层代码中可以不进行同步。在ThreadLocal的内部,线程同步可能发生在对各线程变量值的访问时,就像在CustomizedThreadLocal中所做的一样(通过sychronizedMap达到同步)。查看Java类库的ThreadLocal的源码发现,对ThreadLocal的访问是这样的:JVM为每个Thread维护一个该线程独有变量的Map,称为ThreadLocalMap,这是通过本地代码(native方法)来实现的,当访问ThreadLocal时,根据ThreadLocal实例的Hash值在当前线程的ThreadLocalMap中查找对应的变量值,而一个线程对其自身的ThreadLocalMap的访问必然是单线程的而无须同步。不过在本地代码中,JVM底层可能会施加某些同步措施。

LogStats的代码只是为了演示ThreadLocal的使用,并不是良好的OO设计风格。这里使用ThreadLocal仅只是记录执行时间,没有什么实际意义,似乎完全没有必要。但是考虑在一个更大的应用程序范围内,线程的执行可能会跨越很深的代码层次结构,比如在一个基于servlet的web应用中处理用户请求的线程,要使某些变量在线程执行期间对相距甚远的两部分代码可见,除了在每个方法调用处将变量作为参数传递(这显然是一种很令人头疼的做法),另一个选择就是使用ThreadLocal静态变量。
ThreadLocal变量是和线程绑定的,而不是和任务绑定。忽视这点,可能会造成一些意想不到的问题。在线程池模式中,一个线程会被多次用来执行不同的任务,比如在servlet中,是采用线程池来为用户请求服务(这里的任务是用户请求):当一个http请求到达时,servlet容器会从线程池中取出一个空闲线程处理请求,处理完毕,再将线程放回线程池,以便可以继续为后续的请求服务。我曾在一次开发过程中,遇到因ThreadLocal使用不当造成的问题。
具体情况是这样的:当用户访问我们的网站服务(服务端是基于servlet)时,需要针对来自某种类型的用户终端的请求进行特殊处理,给出不同的响应。在服务端代码中,很多不同的模块都要对该终端类型进行判断,因此,我使用Filter,在处理请求之前,识别出该终端类型并保存在ThreadLocal静态变量中,以便其它模块直接使用。代码大体如下:

public class UserAgentFilter implements Filter {
	public static final ThreadLocal<Boolean> specialUA = new ThreadLocal<Boolean>() {
		protected Boolean initialValue() {
			return false;
		}
	};
	public void doFilter(ServletRequest request, ServletResponse response) {
		if (isSpecialUserAgent(request)) {	//////1//////
			specialUA.set(true);
		}

	}
}

这段功能上线后,通过对访问日志的分析发现,对一部分原本不是该类型的终端请求也进行了特殊处理。造成这种现象的原因是:第一次,当一个来自特殊终端的请求到达时,在1处条件判断为真,从而将specialUA的值设置为true,当请求处理结束时,处理该请求的线程被放入线程池。第二次,当一个普通终端的请求到达时,线程池中的一个空闲线程被选中来为该请求服务,选中的线程碰巧是第一次的请求处理线程,而此时在1处条件判断为假,specialUA的值在此次请求中未被设置,但却仍然为true。因为ThreadLocal变量的值是该线程在处理第一次请求时被设置的。
对该问题的解决方法是:在请求处理结束时,线程被放入线程池之前,将specialUA的值设置为默认值(false),或者在每次请求开始时都重新设置该值:

......
if (isSpecialUserAgent(request)) {	//////1//////
	specialUA.set(true);
} else {
	specialUA.set(false);
}
......

Thread Local Storage是一种很有用的多线程设计模式,特别是在将单线程程序改为多线程时。Java中的ThreadLocal类是该模式的一个实现。ThreadLocal变量值是和线程绑定的,不是和任务绑定。当ThreadLocal(或Thread Local Storage模式)和线程池模式一起使用时,一定要在任务开始或结束之时,将ThreadLocal变量设置为默认值,以防出现不可预料的问题。

在shell里,启动一个进程并放入后台执行,用

cmd &

启动一个进程放入后台,且logout后继续运行,用

nohup cmd &

如果已在前台启动了某个进程,要想转入后台执行,要先按ctrl-z暂停,再用bg后台执行之。
但这样做,logout之后,进程依然可能会被杀掉。
有好几次遇到这样的情况:在服务器上,前台启动了一个程序,到了下班时间还没有执行完毕,这时就遇到点小麻烦,因为不能够马上关机走人,否则服务器上的程序就会挂掉。
一种选择是等到程序执行完毕再关机撤离,但等待时间可能会比较长;另一选择是先杀掉进程,再用nohup...& 重新启动,但这么做,程序之前所做的工作就得丢弃(程序不能够从先前结束的位置继续执行)。
有没有办法让已在前台执行的进程转入后台,且logout之后继续运行?闲来无事,终于在网上找到了答案:按ctrl-z暂停,再用bg后台执行之,然后执行

disown -h

即可。

作为程序员,不可能一直开发新系统、编写新代码,维护老系统在所难免。因为新系统开发完了,总要有人来维护,不是自己维护,就是别人维护。从维护的角度看,我们不是维护自己的代码,就是维护别人的代码。
维护工作在软件产品的生命周期中占据了绝大部分时间。可实际上,维护工作时常陷入困境。相信很多维护人员曾遇到过维护工作难以进展的局面:代码难以理解,BUG无从调试,坏味道四处弥漫,系统故障频频,一个小的逻辑改动要扩散到很多个模块, 新功能需求的开发让本已遍体鳞伤的系统雪上加霜。
不少程序员在这种情况下苦苦挣扎并逐渐习以为常,初临此境的人试图通过重构扭转局面,最终发现势单力孤而放弃一腔热情。
为什么会形成这种局面?我觉得最主要的原因是,在系统开发和维护的各个阶段缺乏良好的设计所致。设计并非只发生在系统开发的初期,在上线以后,每一次的功能扩展和需求变更,都伴随着某种设计决定,不论是有意识的还是无意识的。不良设计的产生可能有两种情况:一种情况是开发人员和技术负责人不知道什么是好的设计或误用设计原则,从而在无意间做出糟糕的设计决策。另一种情况是在任务进度压力下,开发人员选择了短期内易于实现但对长期有害的设计方案。而组织架构层面的问题也是滋生这些弊病的土壤:一些中小型IT公司没有训练有素的架构设计人员,设计决策完全是由经验相对丰富的开发人员或技术负责人做出;没有QA及例行的代码复查机制使得隐藏的问题长期得不到暴露和纠正的机会。
不良设计是一种技术债务,如同真实债务会产生利息一样,技术债务问题如果不能及时改善,会持续增加后期维护工作的成本。
技术债务在实际工作中是难以避免的,就连在ThoughtWorks这样一个面向对象、重构、敏捷思想领袖们的聚集地,也同样存在令人忍无可忍的代码。
虽然无法避免,但也并非毫无办法。很多人希望通过重构来改善烂代码,但重构应当是常态,而不该是特例。系统每一次升级维护,都意味着对原有设计的改变,这正是适合重构的时机,也许早期的设计适应当时的需求,随着功能扩展和新需求的开发,如果设计不能与时俱进,那些缺陷代码会像散落在角落里的垃圾一样慢慢腐烂变质,腐蚀周遭的环境。与其等到代码破败不堪、臭味不断时才进行重构,为什么不提早在日常维护中通过小的重构来分散负担减轻成本呢?
Martin Fowler在<重构>一书后面说明了开发人员不愿进行重构的几个可能原因:
1.不知道如何进行重构
2.如果重构的效益是长期的,到时我可能已走人了从而分享不到好处,现在干嘛要付出努力!
3.开发新功能已令人很头疼了,我才懒得重构,老板又没要求,只要功能实现就行
4.重构可能会破坏现有的功能,做好了也没好处,可万一出问题谁来扛?
重构难以开展的原因也可以看看这里
重构并非银弹,当代码已腐朽不堪时,进行重构的难度和成本也会大大增加。
有句话说出来混尽早要还的,程序员如果不能加强手艺而进行有益的工作,早晚会尝到有害工作的恶果,并在与其斗争中精疲力尽。

当一件事情有多种看似合理的解释时,人们一般更愿意相信使自己负面性最小、而优越性最大的那一种。
比如对于一个人做事情,反复考虑、不轻易做决定的现象,可能有一种的观点会说这是深思熟虑、审慎持重的表现,而另一种观点会认为是优柔寡断、缺乏主见。对于具备此种特征的人们,可能大多倾向于相信自己属于前者。
再比如,对于性格内向的人,传统观点认为,内向、自闭通常伴随着某种程度的心理紊乱,人们也习惯将之视为不善沟通的同义词。更现代的观点则承认内向型性格及内向的人们并不比常人有何缺陷,且内向者更富于内在品质,其中一些人具有极高的天赋。我想内向者们应该都会乐于接受后者吧。

在一期名为<雨人揭密>的电视节日中,讲述了雨人这种神奇的社会现象。雨人这个名称来源于一部电影,是指那些在某些方面具有极高才能和天赋、但通常是自闭者、有些甚至连生活都不能自理的一类人。金皮克是一个真实存在过的雨人,据说他能够过目不忘,一本书只要看过一遍就能一字不差的背诵。而且他看书的方式不同于常人,一般人看书时,是看完左页再看右页,而他是左眼看左页的书,右眼看右页的书,简直是个扫描器!!!这个节目中还提到,在各个领域,有很多著名人物具有雨人的特征,牛顿、爱因斯坦、莫扎特等都是这方面的代表。
现实生活中,高智商的雨人并不多见,和雨人自闭特征接近的内向者比比皆是。现代社会一般推崇外向型人格,内向型人格虽然在某些场合被认可,但显然不被商业价值观所推崇。君不见,无论是否属实,求职简历上统统冠以“性格开朗、善于与人交往、沟通能力强”的自评。面试时,被问及有何缺点,若回答“性格内向”,十有八九会被咔嚓掉。
现代商业文明的高度发展,相信在一定程度上归功于外向型人格的商人们。但商业的兴起和发展,离不开科学技术文化艺术等方面的进步,而内向型人格在这些方面更易取得成功。世界是多元化的,同质化的世界是不可能持续发展的,正是有了不同类型人格的存在,整个世界才变得更为有趣。

在做应用开发时,有时会遇到一种场景:在完成某个主任务的同时,需要处理一些其它的子任务,为了加快响应速度,会将子任务放在另外的线程中执行。例如,搜索引擎在处理用户的查询请求时,不仅要从本地数据库查找匹配的结果,同时可能会向远程的广告服务器请求广告数据,最后将搜索结果和广告一起返回给用户。在这个例子中,主线程从本地查找搜索结果,同时启动子线程从远程服务器获取广告数据,当主线程查找结束时,会将子线程得到的广告数据与搜索结果合并,最后发送给用户。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 AdThread extends Thread {

    private FutureAd future;

    public AdThread(FutureAd ad) {

        this.future = future;
    }

    public void run() {

        try {

            Ad ad = retrieveAdFromRemoteServer(); //耗时操作

            future.setAd(ad);

        } catch (Exception e) {}
    }
    //省略部分方法
}

关键部分是FutureAd对象:

public class FutureAd {

    private Ad ad;

    private boolean done;

    public FutureAd () {}

    public synchronized Ad getAd() {

        while (!done) {  //若广告线程未完成,主线程执行到get方法会阻塞

            try {

                wait();

            } catch (InterruptedException e) {}
        }

        return ad;
    }

    public synchronized void setAd(Ad ad) {

        this.ad = ad;

        this.done = true;

        notify();  //执行完毕,通知主线程
    }
}

在Java 5.0以后,Java并发框架(java.util.concurrent)已经内置了Future模式。利用java.util.concurrent包提供的ExecuteService和Future等相关实现使用Future模式,无疑是一种很好的选择,避免了从头开始开发该模式的成本及可能遇到的种种问题。以下利用该框架提供的API改写前面的代码:

ExecutorService exec = Executors.newCachedThreadPool();

public void searchService(query) {

    Future<Ad> future = exec.submit(new Callable<Ad>() {

        public Ad call() {

            Ad ad = retrieveAdFromRemoteServer();

            return ad;
        }
    });

    SearchResult result = findResult(query);

    //Ad ad = future.get(100, TimeUnit.MILLISECONDS); //此调用最多等待100ms
    Ad ad = future.get();  

    respond2User(result, ad);
}

与Future模式相关的一个多线程设计问题是忙等(busy wait)。仍以前述搜索主线程和广告线程为例,下列代码演示了忙等。
主线程:

public void searchService(query) {

    AdThread adThread = new AdThread();

    adThread.start();

    SearchResults results = findResults(query);

    while (true) {

        if (!adThread.isDone()) { //忙等!!!若子线程未完成,主线程每隔10ms检查一次,直到完成

            Thread.sleep(10);
        }
    }

    Ad ad = adThread.getAd();

    mergeResultsAndAd(results, ad);

    respond2User();
}

广告线程:

public class AdThread extends Thread {

        private Ad ad;

        private boolean done = false;

        public void run() {

            Ad ad = retrieveAdFromRemoteServer(); 

            this.ad = ad;

            this.done = true;

        }

        public boolean isDone() {

            return this.done;
        }
    }
}

这样的代码在生产环境中并不鲜见。忙等至少会造成两个问题:首先,等待的线程会消耗CPU时间,这是没有必要的。再者,等待的线程和被等待的线程会轮流占用CPU,造成不必要的上下文切换(context switch)。这两个问题都会对性能产生影响。此外,在上述代码中,对AdThread类的done实例变量的写操作结果不能保证马上被读操作看见,除非声明成volatile,原因参见java内存模型。
最近在重构老代码的过程中,遇到了很多这样的问题。而忙等的问题可以由Future模式来解决。在Future模式中,通过巧妙的设计来协调线程间通信,避免不必要的上下文切换,改善了性能。

终于搭好了自己的blog。不过此时这里空空如也,没有任何内容,希望以后慢慢丰富起来。首先将blog的搭建过程记录于此,以备日后回顾。

博客程序决定采用wordpress,因为这是最流行的个人博客系统,而且支持php的主机空间较为普及。使用的人多,有问题就容易得到技术支持。wordpress基于GPL协议发布,有一个非常活跃的开发者社区,拥有众多的插件和主题可供选择,版本更新也比较快,而且非常易于安装和使用,在服务器上的安装过程相当顺利(其实我不是托,免费使用了人家的东西,为人家说句好话也是应该的)。

支持wordpress的主机空间比较多,经过一番比较,最终选择了justhost共享主机空间,最主要的原因在于它的价格低廉,$41.88一年,附送一个免费一级域名,所提供的配置,对我来说,已经足够用了。开通速度很快,付款之后,域名马上就能访问,后台cpanel控制面板非常强大,不仅已经集成了wordpress等流行的应用程序,只要点击鼠标即可安装,甚至可以开通公共ftp和邮件服务,不过暂时我用不到。唯一的不足是没有ssh(这个是增值服务,要另外花钱),不过用ftp基本上也能满足需求。

发现ftp上传速度很慢,上传8M的wordpress程序,居然花了近2小时,这有可能是由于我直接上传的目录结构,其中包含的文件个数过多的缘故。我没有尝试过将程序打包上传,然后在服务器端解包的方法,也许这样能上传得更快。除此之外,暂时还没有遇到其它问题。

最后说明一下,本博客一年期有效域名liuchangit.com(这不废话),欢迎你的到访。