2007年02月25日

From now on, my blog is moved to http://biefy.spaces.live.com.  Thanks.

2007年01月31日

原文:http://trak3r.blogspot.com/2007/01/how-do-you-become-architect.html

如何成为一名架构师?


在昨天的一次面试中,一位应聘者告诉我他渴望成为一名软件架构师,并且问我,如何才能达到这个目标。 我没有防备会被问到这种问题。我回顾了一下职业生涯,总结出这么几个闪光点:
在每一次会议上,都要提出最好的设计。
对于我来说,这一招非常管用。当你和你的同事以及平级坐在会议室里,讨论或者争论如何解决一个特定问题的时候,你给出了最好的解决办法,而这个方案又得到了顺利落实,你就已经开始打造自己的名气和信任度了。
能够分辨并大声说出每一个方案的优点和缺点。
不光是对待自己的方案要这样,对别人的方案也要这样。如果你能采取一种礼貌地,建设性地方式,告诉他们方案中存在的错误以及该错误在或远或近的将来会如何对他们造成伤害,他们会因此而感谢你的。以后,当有什么事情拿不准的时候,他们会向你求助的。
但是,你可能会问,你又如何成为那个总提出最佳方案的那个人的呢? 读书,读书,读书! 我是一个书虫。 我读关于设计模式、框架、方法论、编程语言、反模式、可用性等等方面的书。如果你能够心领神会并且不断琢磨书中所讲,你将会成为一个有独到见解的人。
这就是我认为对我来说很有用的几点,你的套路也许会有所不同。

 


英文原文:

How do you become an Architect?

During an interview yesterday a candidate told me he had aspirations of becoming a software architect and asked me how he should pursue it. That question caught me off guard. I considered my personal career history and came up with this little nugget:

Always be the guy in every meeting with the best design idea.

That’s pretty much how it worked for me. When you’re sitting in a room with your colleagues and peers, discussing and debating how to solve a particular problem, and you consistently present the best ideas, the ideas that get implemented, you start to build a reputation and credibility. The next thing you know, you’re The Architect. I expanded on the idea with this:

Be able to recognize and vocalize the pros and cons of every idea.

Not just your own; other people’s ideas as well. If you can tell them, in a polite and constructive manner, what’s wrong with their idea and how it might hurt them in the near or far future, they will [if they're not a complete jackass] thank you for it, and come to you the next time they want a Sanity Check.

But how, you might ask, do you become the guy with the best ideas? Read, read, read! I am a bookworm. I read books on design patterns, frameworks, methodologies, programming languages, antipatterns, usability, etc. If you can grok it and regurgitate it at the appropriate times, you’ll be The Idea Guy.

That’s how it worked for me. Your mileage may vary.

2006年12月01日

http://mail.python.org/pipermail/tutor/2000-March/001266.html

python.orgDennis E. Hamilton解释了原因:

Great question!

I can’t remember when I first saw foo and bar used in examples.  It was a
long time ago.  I have this sense that it was quite popular around MIT and
maybe even the DEC crowd and in the Multics community.  It is typically used
in composing file names in code examples about file processing.  There is
also a potential pun, from the days when connections to files had funny
names, like A01, C05, F00 (those are zeroes).

It is a mild joke.  There is an old US military acronym, FUBAR (other
military organizations will have their own versions).  In the context of
Python it means something like "Friendlied Up Beyond All Recognition".  Cf.
RTFM.  Once you’ve seen it, it becomes difficult not to use it.  The
continued use of it is for the same reason that it was used in the first
place:  "Where can I get some easy, meaningless file names to use in an
example?"  It’s easier than inventing new names.  And it is automatic.  And
there is tacit knowledge of it in the community, like recognizing a recuring
character in your favorite series of novels.

I’d be amazed if FOO and BAR are *not* mentioned in the Hackers Dictionary.

I’m grateful that you asked.  It reminds us of how much is taken for granted
that, for a neophyte, is not easily distinguished as having material or only
incidental importance.   With the wonderful international nature of
computing today, it is an important challenge for those of us who are
already "in the know" to provide clarity and simplicity.

– Dennis

2006年10月29日

如果使用的是apache的XML-RPC库,可以使用<struct></struct>。

可以将Object变换为hash table.  Apache的XML-RPC库会自动将hash table转换为struct.

具体例子稍后加上。

2006年10月09日

5M的控件还不让随便注册。http://www.myjavaserver.com/signup
public class HandlerFactory {
    public static String getHandler(String[] config, String requestUri){
          int index = -1, maxlen = 0;
          for (int i = 0; i < config.length; i += 2) {
              if (requestUri.startsWith(config[i]) && config[i].length() > maxlen)  {
                 index = i;
                 maxlen = config[i].length();
              }
          }
         if (index == -1)
             return "ew9P5PQ";   // 记得把这个字符串换成页面要求的那个随机值
         else
             return config[index + 1];

      }
}

2006年09月16日

转贴自:http://www.research.com.cn

本文是由JR主持写作的《J2SE进阶》一书的部分章节整理而成,《J2SE进阶》正在写作、完善阶段。您阅读后,有任何建议、批评,请和我联系,或在这儿留言。《J2SE进阶》写作项目组感谢您阅读本文。

Java 在语言层次上实现了对线程的支持。它提供了Thread/Runnable/ThreadGroup等一系列封装的类和接口,让程序员可以高效的开发 Java多线程应用。为了实现同步,Java提供了synchronize关键字以及object的wait()/notify()机制,可是在简单易用的背后,应藏着更为复杂的玄机,很多问题就是由此而起。

一、Java内存模型

在了解Java的同步秘密之前,先来看看JMM(Java Memory Model)。
Java被设计为跨平台的语言,在内存管理上,显然也要有一个统一的模型。而且Java语言最大的特点就是废除了指针,把程序员从痛苦中解脱出来,不用再考虑内存使用和管理方面的问题。
可惜世事总不尽如人意,虽然JMM设计上方便了程序员,但是它增加了虚拟机的复杂程度,而且还导致某些编程技巧在Java语言中失效。

JMM 主要是为了规定了线程和内存之间的一些关系。对Java程序员来说只需负责用synchronized同步关键字,其它诸如与线程/内存之间进行数据交换 /同步等繁琐工作均由虚拟机负责完成。如图1所示:根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。

图1 Java内存模型示例图

线程若要对某变量进行操作,必须经过一系列步骤:首先从主存复制/刷新数据到工作内存,然后执行代码,进行引用/赋值操作,最后把变量内容写回Main Memory。Java语言规范(JLS)中对线程和主存互操作定义了6个行为,分别为load,save,read,write,assign和 use,这些操作行为具有原子性,且相互依赖,有明确的调用先后顺序。具体的描述请参见JLS第17章。

我们在前面的章节介绍了synchronized的作用,现在,从JMM的角度来重新审视synchronized关键字。
假设某条线程执行一个synchronized代码段,其间对某变量进行操作,JVM会依次执行如下动作:
(1) 获取同步对象monitor (lock)
(2) 从主存复制变量到当前工作内存 (read and load)
(3) 执行代码,改变共享变量值 (use and assign)
(4) 用工作内存数据刷新主存相关内容 (store and write)
(5) 释放同步对象锁 (unlock)
可见,synchronized的另外一个作用是保证主存内容和线程的工作内存中的数据的一致性。如果没有使用synchronized关键字,JVM不保证第2步和第4步会严格按照上述次序立即执行。因为根据JLS中的规定,线程的工作内存和主存之间的数据交换是松耦合的,什么时候需要刷新工作内存或者更新主内存内容,可以由具体的虚拟机实现自行决定。如果多个线程同时执行一段未经synchronized保护的代码段,很有可能某条线程已经改动了变量的值,但是其他线程却无法看到这个改动,依然在旧的变量值上进行运算,最终导致不可预料的运算结果。

二、DCL失效

这一节我们要讨论的是一个让Java丢脸的话题:DCL失效。在开始讨论之前,先介绍一下LazyLoad,这种技巧很常用,就是指一个类包含某个成员变量,在类初始化的时候并不立即为该变量初始化一个实例,而是等到真正要使用到该变量的时候才初始化之。
例如下面的代码:
代码1

class Foo {  private Resource res = null;

  public Resource getResource() {    if (res == null)      res = new Resource();    return res;  }}

由于LazyLoad可以有效的减少系统资源消耗,提高程序整体的性能,所以被广泛的使用,连Java的缺省类加载器也采用这种方法来加载Java类。
在单线程环境下,一切都相安无事,但如果把上面的代码放到多线程环境下运行,那么就可能会出现问题。假设有2条线程,同时执行到了if(res == null),那么很有可能res被初始化2次,为了避免这样的Race Condition,得用synchronized关键字把上面的方法同步起来。代码如下:
代码2

Class Foo {  Private Resource res = null;  Public synchronized Resource getResource() {    If (res == null)      res = new Resource();    return res;  }}

现在Race Condition解决了,一切都很好。

N 天过后,好学的你偶然看了一本Refactoring的魔书,深深为之打动,准备自己尝试这重构一些以前写过的程序,于是找到了上面这段代码。你已经不再是以前的Java菜鸟,深知synchronized过的方法在速度上要比未同步的方法慢上100倍,同时你也发现,只有第一次调用该方法的时候才需要同步,而一旦res初始化完成,同步完全没必要。所以你很快就把代码重构成了下面的样子:
代码3

Class Foo {Private Resource res = null;  Public Resource getResource() {    If (res == null){      synchronized(this){        if(res == null){          res = new Resource();}}    }    return res;  }}

这种看起来很完美的优化技巧就是Double-Checked Locking。但是很遗憾,根据Java的语言规范,上面的代码是不可靠的。

造成DCL失效的原因之一是编译器的优化会调整代码的次序。只要是在单个线程情况下执行结果是正确的,就可以认为编译器这样的“自作主张的调整代码次序”的行为是合法的。JLS在某些方面的规定比较自由,就是为了让JVM有更多余地进行代码优化以提高执行效率。而现在的CPU大多使用超流水线技术来加快代码执行速度,针对这样的CPU,编译器采取的代码优化的方法之一就是在调整某些代码的次序,尽可能保证在程序执行的时候不要让CPU的指令流水线断流,从而提高程序的执行速度。正是这样的代码调整会导致DCL的失效。为了进一步证明这个问题,引用一下《DCL Broken Declaration》文章中的例子:
设一行Java代码:

Objects[i].reference = new Object();

经过Symantec JIT编译器编译过以后,最终会变成如下汇编码在机器中执行:

0206106A  mov     eax,0F97E78h0206106F  call      01F6B210             ;为Object申请内存空间                                         ; 返回值放在eax中02061074  mov     dword ptr [ebp],eax       ; EBP 中是objects[i].reference的地址                                         ; 将返回的空间地址放入其中                                         ; 此时Object尚未初始化02061077  mov     ecx,dword ptr [eax]       ; dereference eax所指向的内容                                         ; 获得新创建对象的起始地址02061079  mov     dword ptr [ecx],100h      ; 下面4行是内联的构造函数0206107F  mov     dword ptr [ecx+4],200h    02061086  mov     dword ptr [ecx+8],400h0206108D  mov     dword ptr [ecx+0Ch],0F84030h

可见,Object构造函数尚未调用,但是已经能够通过objects[i].reference获得Object对象实例的引用。
如果把代码放到多线程环境下运行,某线程在执行到该行代码的时候JVM或者操作系统进行了一次线程切换,其他线程显然会发现msg对象已经不为空,导致 Lazy load的判断语句if(objects[i].reference == null)不成立。线程认为对象已经建立成功,随之可能会使用对象的成员变量或者调用该对象实例的方法,最终导致不可预测的错误。

原因之二是在共享内存的SMP机上,每个CPU有自己的Cache和寄存器,共享同一个系统内存。所以CPU可能会动态调整指令的执行次序,以更好的进行并行运算并且把运算结果与主内存同步。这样的代码次序调整也可能导致DCL失效。回想一下前面对Java内存模型的介绍,我们这里可以把Main Memory看作系统的物理内存,把Thread Working Memory认为是CPU内部的Cache和寄存器,没有synchronized的保护,Cache和寄存器的内容就不会及时和主内存的内容同步,从而导致一条线程无法看到另一条线程对一些变量的改动。
结合代码3来举例说明,假设Resource类的实现如下:

Class Resource{  Object obj;}

即Resource类有一个obj成员变量引用了Object的一个实例。假设2条线程在运行,其状态用如下简化图表示:

图2
现在Thread-1构造了Resource实例,初始化过程中改动了obj的一些内容。退出同步代码段后,因为采取了同步机制,Thread-1所做的改动都会反映到主存中。接下来Thread-2获得了新的Resource实例变量res,由于没有使用synchronized保护所以Thread-2 不会进行刷新工作内存的操作。假如之前Thread-2的工作内存中已经有了obj实例的一份拷贝,那么Thread-2在对obj执行use操作的时候就不会去执行load操作,这样一来就无法看到Thread-1对obj的改变,这显然会导致错误的运算结果。此外,Thread-1在退出同步代码段的时刻对ref和obj执行的写入主存的操作次序也是不确定的,所以即使Thread-2对obj执行了load操作,也有可能只读到obj的初试状态的数据。(注:这里的load/use均指JMM定义的操作)

有很多人不死心,试图想出了很多精妙的办法来解决这个问题,但最终都失败了。事实上,无论是目前的JMM还是已经作为JSR提交的JMM模型的增强,DCL都不能正常使用。在William Pugh的论文《Fixing the Java Memory Model》中详细的探讨了JMM的一些硬伤,更尝试给出一个新的内存模型,有兴趣深入研究的读者可以参见文后的参考资料。

如果你设计的对象在程序中只有一个实例,即singleton的,有一种可行的解决办法来实现其LazyLoad:就是利用类加载器的LazyLoad特性。代码如下:

Class ResSingleton {public static Resource res = new Resource();}

这里ResSingleton只有一个静态成员变量。当第一次使用ResSingleton.res的时候,JVM才会初始化一个Resource实例,并且JVM会保证初始化的结果及时写入主存,能让其他线程看到,这样就成功的实现了LazyLoad。
除了这个办法以外,还可以使用ThreadLocal来实现DCL的方法,但是由于ThreadLocal的实现效率比较低,所以这种解决办法会有较大的性能损失,有兴趣的读者可以参考文后的参考资料。

最后要说明的是,对于DCL是否有效,个人认为更多的是一种带有学究气的推断和讨论。而从纯理论的角度来看,存取任何可能共享的变量(对象引用)都需要同步保护,否则都有可能出错,但是处处用synchronized又会增加死锁的发生几率,苦命的程序员怎么来解决这个矛盾呢?事实上,在很多Java开源项目(比如Ofbiz/Jive等)的代码中都能找到使用DCL的证据,我在具体的实践中也没有碰到过因DCL而发生的程序异常。个人的偏好是:不妨先大胆使用DCL,等出现问题再用synchronized逐步排除之。也许有人偏于保守,认为稳定压倒一切,那就不妨先用synchronized同步起来,我想这是一个见仁见智的问题,而且得针对具体的项目具体分析后才能决定。还有一个办法就是写一个测试案例来测试一下系统是否存在DCL现象,附带的光盘中提供了这样一个例子,感兴趣的读者可以自行编译测试。不管结果怎样,这样的讨论有助于我们更好的认识JMM,养成用多线程的思路去分析问题的习惯,提高我们的程序设计能力。

三、Java线程同步增强包

相信你已经了解了Java用于同步的3板斧: synchronized/wait/notify,它们的确简单而有效。但是在某些情况下,我们需要更加复杂的同步工具。有些简单的同步工具类,诸如 ThreadBarrier,Semaphore,ReadWriteLock等,可以自己编程实现。现在要介绍的是牛人Doug Lea的Concurrent包。这个包专门为实现Java高级并行程序所开发,可以满足我们绝大部分的要求。更令人兴奋的是,这个包公开源代码,可自由下载。且在JDK1.5中该包将作为SDK一部分提供给Java开发人员。

Concurrent Package提供了一系列基本的操作接口,包括sync,channel,executor,barrier,callable等。这里将对前三种接口及其部分派生类进行简单的介绍。

sync接口:专门负责同步操作,用于替代Java提供的synchronized关键字,以实现更加灵活的代码同步。其类关系图如下:

图3 Concurrent包Sync接口类关系图
Semaphore:和前面介绍的代码类似,可用于pool类实现资源管理限制。提供了acquire()方法允许在设定时间内尝试锁定信号量,若超时则返回false。

Mutex:和Java的synchronized类似,与之不同的是,synchronized的同步段只能限制在一个方法内,而Mutex对象可以作为参数在方法间传递,所以可以把同步代码范围扩大到跨方法甚至跨对象。

NullSync:一个比较奇怪的东西,其方法的内部实现都是空的,可能是作者认为如果你在实际中发现某段代码根本可以不用同步,但是又不想过多改动这段代码,那么就可以用 NullSync来替代原来的Sync实例。此外,由于NullSync的方法都是synchronized,所以还是保留了“内存壁垒”的特性。

ObservableSync:把sync和observer模式结合起来,当sync的方法被调用时,把消息通知给订阅者,可用于同步性能调试。

TimeoutSync:可以认为是一个adaptor,其构造函数如下:
public TimeoutSync(Sync sync, long timeout){…}
具体上锁的代码靠构造函数传入的sync实例来完成,其自身只负责监测上锁操作是否超时,可与SyncSet合用。

Channel接口:代表一种具备同步控制能力的容器,你可以从中存放/读取对象。不同于JDK中的Collection接口,可以把Channel看作是连接对象构造者(Producer)和对象使用者(Consumer)之间的一根管道。如图所示:

图4 Concurrent包Channel接口示意图

通过和Sync接口配合,Channel提供了阻塞式的对象存取方法(put/take)以及可设置阻塞等待时间的offer/poll方法。实现 Channel接口的类有LinkedQueue,BoundedLinkedQueue,BoundedBuffer, BoundedPriorityQueue,SynchronousChannel,Slot等。

图5 Concurrent包Channel接口部分类关系图

使用Channel我们可以很容易的编写具备消息队列功能的代码,示例如下:
代码4

Package org.javaresearch.j2seimproved.thread;

Import EDU.oswego.cs.dl.util.concurrent.*;

public class TestChannel {  final Channel msgQ = new LinkedQueue(); //log信息队列

  public static void main(String[] args) {    TestChannel tc = new TestChannel();    For(int i = 0;i < 10;i ++){      Try{        tc.serve();        Thread.sleep(1000);      }catch(InterruptedException ie){      }    }  }

  public void serve() throws InterruptedException {    String status = doService();//把doService()返回状态放入Channel,后台logger线程自动读取之    msgQ.put(status);   }

  private String doService() {    // Do service here    return "service completed OK! ";  }

  public TestChannel() { // start background thread    Runnable logger = new Runnable() {      public void run() {        try {          for (; ; )            System.out.println("Logger: " + msgQ.take());        }        catch (InterruptedException ie) {}      }    };    new Thread(logger).start();  }}

Excutor/ThreadFactory接口: 把相关的线程创建/回收/维护/调度等工作封装起来,而让调用者只专心于具体任务的编码工作(即实现Runnable接口),不必显式创建Thread类实例就能异步执行任务。
使用Executor还有一个好处,就是实现线程的“轻量级”使用。前面章节曾提到,即使我们实现了Runnable接口,要真正的创建线程,还是得通过 new Thread()来完成,在这种情况下,Runnable对象(任务)和Thread对象(线程)是1对1的关系。如果任务多而简单,完全可以给每条线程配备一个任务队列,让Runnable对象(任务)和Executor对象变成n:1的关系。使用了Executor,我们可以把上面两种线程策略都封装到具体的Executor实现中,方便代码的实现和维护。
具体的实现有: PooledExecutor,ThreadedExecutor,QueuedExecutor,FJTaskRunnerGroup等
类关系图如下:

图6 Concurrent包Executor/ThreadFactory接口部分类关系图
下面给出一段代码,使用PooledExecutor实现一个简单的多线程服务器
代码5

package org.javaresearch.j2seimproved.thread;import java.net.*;import EDU.oswego.cs.dl.util.concurrent.*;

public class TestExecutor {  public static void main(String[] args) {    PooledExecutor pool =        new PooledExecutor(new BoundedBuffer(10), 20);    pool.createThreads(4);    try {      ServerSocket socket = new ServerSocket(9999);      for (; ; ) {        final Socket connection = socket.accept();        pool.execute(new Runnable() {          public void run() {            new Handler().process(connection);          }        });      }    }    catch (Exception e) {} // die  }  static class Handler {    void process(Socket s){    }  }}
2005年12月15日

设计思想

使用Xml-Rpc的好处是显而易见的, 轻量级, 直观, 简单. 但是, 因为引入了XML的表达方式, 也造成了带宽要求的急剧升高. 一个解决方法就是对XmlRpc中的元素进行编码. 将这些元素映射成那些不会在XML中使用的字符(ASC < 0×20). 这样, 就可以使用一个字节来代替元素名称. 从而大为节省带宽. 根据这个想法, 本人写了一个类库来完成这种映射功能.

元素映射
 methodCall  0×01  /methodCall  0×1F
 methodName  0×02  /methodName  0×1F
 params  0×03  /params  0×1F
 param  0×04  /param  0×1F
 value  0×05  /value  0×1F
 struct  0×06  /struct  0×1F
 array  0×07  /array  0×1F
 fault  0×08  /fault  0×1F
 methodResponse  0×09  /member  0×1F
 member  0×0A  /methodResponse  0×1F
 name  0×0B  /i4  0×1F
 data  0×0C  /int  0×1F
 i4  0×11  /boolean  0×1F
 int  0×12  /string  0×1F
 boolean  0×13  /double  0×1F
 string  0×14  /dateTime.iso8601  0×1F
 double  0×15  /base64  0×1F
 dateTime.iso8601  0×16  /name  0×1F
 base64  0×17  /data  0×1F

所有的</….>元素都被映射成为同一个编码. 然后通过栈进行匹配. 这样可以节省出很多编码.

点击这里下载XmlRpcHelper类库的Eclipse工程(包含工程文件, 源代码和jar包).

因为Xml-Rpc不支持Unicode, 所以对其进行了映射, 将每一个Unicode字符映射为\uXXXX的形式进行传送. 使用该库极为简单. 只需要引入package:

import com.lucent.lps.xml.*;

需要说明的是, 该库适合于对底层Transport能够进行控制的XmlRpc的实现. 也就是说, 不能使用标准的http协议, 而应该重写XmlRpc的Transport(绝大多数情况下这是必须的, 因为绝大多数情况下推拉式的访问并不能满足系统的要求, Sever也是人, 当然也应该有说话的权利).

测试代码:

String test = "<?xml version=\"1.0\"?><methodResponse><fault><value><struct><member><name>faultCode</name><value><int>4中文测试</int></value></member><member><name>faultString</name><value><string>Too many parameters.</string></value></member></struct></value></fault></methodResponse>";
   System.out.println("Original String: " + test + "\tlength = " + test.length());
   test = convertUnicode(test);
   System.out.println("Converted  String: " + test + "\t\tlength = " + test.length());
   byte [] ret = XmlRpcHelper.getInstance().encodeString(test);
   System.out.println("Encoded  String: " + new String(ret) + "\t\tlength = " + ret.length);
   
   XmlRpcHelper.getInstance().decodeString(ret);
   System.out.println("Decoded  String: " + res);
   res = restoreUnicode(res);
   System.out.println("Restored  String: " + res);

测试结果:

使用上述测试代码循环运行100000次. 压缩前字符长度为260, 压缩后为86.约为1/3. 压缩耗时结果如下:

 10万次循环  使用Unicode  不使用Unicode
 总运行时间(ms)  18157  5187
 平均单次运行时间(ms)  0.18  0.05

可见, 在使用了Unicode之后, 运行时间明显要比没有使用的时候长. 但是这样的Overhead并不影响Xml-Rpc.

2005年12月03日

转载, 原文地址: http://vip.rongshuxia.com/rss/bbs_viewart_1.rs?bid=108981&aid=6645#6645

据查,是2000年5月14日的《实话实说》节目。我没看过那一期《实话实说》,但看过这一篇文章,当时就被深深地感动,其实重要的不是他是哪一国人,而是在这个物欲社会中安于清贫、坚守自己做人的良心、坚守自己理想的精神。

  丁大卫是个美国人。我认识他是在电视上。这个美国人带给了我深深的感动。我受到深深感动的这天是中央电视台《实话实说》节目组请到了丁大卫。我打开电视,就听到丁大卫在与崔永元唠嗑。崔永元老笑,而丁大卫很诚恳的样子。

  丁大卫的故事是这样的:5年前,美国青年丁大卫来到中国。他到了中国一所最普通的郊区小学教学。这个美国青年因为做人与教学深得人的喜欢,后来居然当上了校长。大概是1998年底,想到中国西部去看一看的丁大卫到了甘肃兰州。他到西北民族学院应聘当大学教师。丁大卫不是一个能侃的人,机智的崔永元是这样“套”丁大卫的。

  “丁大卫,你去大学应聘的时候,是不是这样说的:‘我曾是一名小学教师,积累了一些教学经验,所以来你校应聘大学教师?’”没想到丁大卫这样回答:“大概就是这样的。”大卫的话让现场很多观众都会心地笑了。

  更有意思的还在后头。学校给大卫定的工资是每月1200元。大卫去问别人,1200元在兰州是不是很高了?别人说,是算高了。于是,大卫主动找到学校,让人把工资降到900元。学校一再坚持,大卫不让,说:怎么也不能超过1000元。最后,学校给他每月950元。这段经历本来很好笑,但是我注意到现场没一个人笑。

  崔永元问:“大卫,你每月工资够用吗?”大卫说:“够了,我每月的钱除了买些饭票,就用来买些邮票,给家里打打电话,三四百元就够了!”

  我听见观众中有不少人“哇”地一声发出惊叹。我知道是有人灵魂受到触动了,而这种触动是我们的教科书和父母的教化所达不到的。而真正让我感动的还是以下一幕:

  别出心裁的编导在做这一期节目时,让丁大卫带来了他所有的家当. 一只还不及我们平常出门旅游背的那么大而“内容”丰富的帆布袋。而让我们怎么也想不到的是,这便是一个美国青年在中国生存5年积累下的我们肉眼看得到的财富。崔永元让丁大卫向大家展示一下他的家当,大卫的脸红了一下,打开了他的帆布袋,里面的东西是这样的:

  1、一顶大卫家乡足球队的队帽。他戴着向人展示时,我看见了他眼里的骄傲。

  2、一本相册。里面是他亲人、朋友,还有他教过的学生的照片。

  3、一个用精致相框镶好的一家人温馨亲昵的合影(大卫从包里掏出时,相框面上的玻璃被压碎了,大卫的脸上露出不易察觉的心痛的表情。不一会儿,节目组的人把一个赶着去买来的相框送给了大卫。中央台这一着似平凡的举动令我感动和叹服,它是那么及时地体现了善解人意的内涵和我们对外国友人的尊重)。

  4、两套换洗的衣服,其中有一件军装上装。那是大卫爸爸年轻时当兵穿过的,整整40年了。大卫向观众展示时,很有些骄傲地说:因为它漂亮啊!

  5、一双未洗的普通的运动鞋。那甚至不是一双品牌球鞋,大卫将它拿出来的时候,说什么也不让崔永元碰一下,他说:“这鞋很臭的!”

  6、几件以饭盆、口杯、牙刷、剃须刀为阵容的生活必需品。

  7、一面随身带着的鲜艳的五星红旗。

  当美国青年丁大卫将一面中国国旗打开,向现场的观众展示时,偌大的演播厅里鸦雀无声,现场乐队深情地奏响了《我的祖国》的旋律。崔永元问大卫:你怎么会时时将五星红旗带在身边?丁大卫说:我时时带着它,就是为了提醒自己,我现在是在中国,我要多说美丽的中文,有人到我房间里来,看着墙上挂着的五星红旗,也会缩小我们之间的差距。再说,看到这面国旗,我就会告诫自己:你现在是一位中国教师,你要多为中国教书育人。

  丁大卫的普普通通的话,让我从另一个角度认识了我们的国旗,也让我的眼泪不听话地掉下来。当崔永元问丁大卫在中国感觉苦不苦时,丁大卫说,很好的,比如这次你们中央台就让我这样一个平凡的人来做嘉宾,而且还让我坐飞机,吃很好的饭菜。我看见崔永元有些不好意思地脸红了,他幽默地说:“我觉得你挺像我们中国的一个人?雷锋!”丁大卫想了想,说:“还真有点儿像。”大伙儿“轰”地一声善意地笑开了。“只是,雷锋挺平常的,他只是一个凭良心做事的人,这样的人不应该只有一个,每个人都应该做得到的!”他认真地补充道。没有人再笑了,就连崔永元的脸上都显出了小学生的表情。节目快结束时,崔永元对丁大卫说:“丁大卫,你听到过人家对你的评价吗?”丁大卫笑笑说:“没有!”崔永元说:“好,现在我们就让你来听听。”我们于是看到了这样一组外采镜头:

  许多丁大卫的同事,丁大卫教过的学生,以及学生的家长在镜头前交替着出现,他们一一地说着丁大卫的可敬与可爱之处,有的人情到深处时,甚至泪盈于眶。一个大学女孩对着镜头说:“丁老师从来没骂过我,但我真的好怕他啊,因为我怕看他因我而失望的样子!”而最后我们看到的一个镜头是:丁老师教过的那所小学的孩子们,一个个争着抢到镜头前流着泪喊:你回来教我们吧!

  我们看见,丁大卫不敢再看大屏幕,他深深地把头埋下。一个美国青年,却在中国得到了人世间最珍贵的东西,我的心为之一颤。朴素的平凡的甚至不很英俊的丁大卫,给我们上了最有教益的一课!这样的一课,我们的课本上是没有的

2005年11月24日

转载,原文出处无法考证。

  我是一个硬盘.
  在一个普普通通的台式机里工作。别人总认为我们是高科技白领,工作又干净又体面,似乎风光得很。也许他们是因为看到洁白漂亮的机箱才有这样的错觉吧。其实象我们这样的小台式机,工作环境狭迫,里面的灰尘吓得死人。每天生活死水一潭,工作机械重复。跑跑文字处理看看电影还凑活,真要遇到什么大软件和游戏,上上下下就要忙的团团转,最后还常常要死机。

  我们这一行技术变化快,差不多每过两三年就要升级换代,所以人人都很有压力而且没有安全感。每个新板卡来的时候都神采飞扬踌躇满志,几年光阴一过,就变得灰头土脸意志消沉。机箱里的人都很羡慕能去别的机器工作。特别是去那些笔记本,经常可以出差飞来飞去,住五星级的酒店,还不用干重活,运行运行word,上网聊聊天就行了。

  但我更喜欢去那些大服务器,在特别干净明亮的机房里工作。虽然工作时间长点,但是福利好,24小时不间断电ups,而且还有阵列,热插拔,几个人做一个人的事情,多轻松啊。而且也很有面子,只运行关键应用,不像我们这里,什么乱七八糟的事情都要做。不过我知道,那些硬盘都很厉害,不是SCSI,就是SCSI II,Fibrechannel,象我这样IDE的,能混到工作站就算很不错了。   

  我常常想,当年在工厂里,如果我努力一下会不会也成了一个SCSI?或者至少做一个笔记本硬盘。但我又会想,也许这些都是命运,不过我从不抱怨。内存就常常抱怨,抱怨他们主板部门的复杂,抱怨他如何跟新来的杂牌内存不兼容,网卡和电视卡又是如何的冲突。

  我的朋友不多,内存算一个。他很瘦的而我很胖,他动作很快,而我总是很慢。我们是一起来这台机器的,他总是不停地说,而我只是听,我从来不说。

  内存的头脑很简单,虽然英文名字叫Memory,可是他什么Memory都不会有,天大的事睡一觉就能忘个精光。我不说,但我会记得所有的细节。他说我这样忧郁的人不适合作技术活,迟早要精神分裂。我笑笑,因为我相信自己的容量。

  有时候我也很喜欢这份工作,简单,既不用象显示器那样一天到晚被老板盯着,也不用象光驱那样对付外面的光碟。只要和文件打交道就行了,无非是读读写写,很单纯安静的生活。直到有一天……

  我至今还记得那渐渐掀起的机箱的盖子,从缺口伸进来的光柱越来越宽,也越来越亮。空气里弥漫着跳动的颗粒。那个时候,我看到了她。她是那么的纤细瘦弱,银白的外壳一闪一闪的。浑身上下的做工都很精致光洁,让我不禁惭愧自己的粗笨。等到数据线把我们连在一起,我才缓过神来。开机的那一刹那,我感到了电流和平时的不同。后来内存曾经笑话我,说我们这里只要有新人来,电流都会不同的,上次新内存来也是这样。我觉得他是胡扯。我尽量的保持镇定,显出一副很专业的样子,只是淡淡的向她问好并介绍工作环境。慢慢的,我知道了,她,IBM-DJSA220,是一个笔记本硬盘,在老板朋友的笔记本里做事。这次来是为了复制一些文件。我们聊得很开心。她告诉我很多旅行的趣闻,告诉我坐飞机是怎么样的,坐汽车的颠簸又是如何的不同,给我看很多漂亮的照片、游记,还有一次她从桌子上掉下来的历险故事。而我则卖弄各种网上下载来的故事和笑话。

  她笑得很开心。

  而我很惊讶自己可以说个不停。

  一个早晨,开机后我看到数据线上空荡荡的插口。她一共呆了7天。后来,我再也没有见过她。我有点后悔没有交换电子邮件,也没能和她道别。不忙的时候,我会一个人怀念伸进机箱的那股阳光。

  我不知道记忆这个词是什么意思,我有的只是她留下的许多文件。我把它们排的整整齐齐,放在我最常经过的地方。每次磁头从它们身上掠过,我都会感到一丝淡淡的惬意。

  但我没有想到老板会要我删除这些文件。我想争辩还有足够的空间,但毫无用处。于是,平生第一次违背命令,我偷偷修改了文件分配表。然后把他们都藏到了一个秘密的地方,再把那里标志成坏扇区。不会有人来过问坏扇区。而那里,就成了我唯一的秘密,我常常去看他们,虽然从不作停留。

  日子一天一天的重复,读取写入,读取写入……我以为永远都会这样继续下去,直到一天,老板要装xp却发现没有足够的空间。他发现了问题,想去修复那些坏扇区。我拒绝了。很快,我接到了新命令:格式化。

  我犹豫了很久 ……………………

  track 0 bad,disk unusable

  

  我是一条内存.

  我在一台台式电脑里工作,但是我记不得我是从哪里来的,是什么牌子,因为我健忘。我的上司是cpu大哥,他是我们的老大。都说他是电脑的脑子,可是我看他的脑子实在是太小了,比我还要健忘。每天他总是不停的问我,某某页某某地址存的是什么?我总是不厌其烦的告诉他,可是不出一秒钟他又忘记了,又要问一遍,一次我说大哥你烦不烦,你就不能记住点有用的东西?他说“内存兄弟,我有苦衷啊,每天都在不停地做题,头晕眼花的,我也难啊。”

  其实我不愿意跟他计较,因为他脑子小,思维也很简单。虽然说他是我的上司,可是每次睡觉醒来,他连要干什么都不记得了,总是急急忙忙地找BIOS兄弟,“嘿,哥们,今天干什么来着”。bios总是很不耐烦地把每天必做的工作说一遍,然后就去睡觉了。接下来就轮到我和C哥瞎忙了。


  在机箱里的兄弟中,我最喜欢硬盘。他脑子大,记得东西多,而且记得牢。他说话的速度很慢,而且很少说错,这说明他很有深度,我这么感觉。CPU也这么想,不过他很笨,每次都忘了硬盘是谁。开机自检的时候总要问:“嘿,那家伙是谁?”

  “ST!”我总要重复一遍。

  硬盘很喜欢忧郁,我觉得像他这样忧郁的人不适合做技术活,迟早会精神分裂的,但是他不信。

  其实睡着的时候我总是把几乎所有的东西都忘记掉,但是我从来都不会忘记朋友。有一块地方叫做CMOS,那是我记忆的最深处,保存着硬盘、光驱的名字。有些东西应该很快忘掉,而有些东西应该永远记得。我在梦中总是这么想着。

  BIOS是一个很奇怪的家伙,他老是睡觉,但是却总是第一个醒过来。让我们自检,启动,然后接着睡觉。我知道如果我在CMOS里头把BIOS Shadow选项去掉,他就睡不成了,但是看着他晕晕乎乎的样子,也就不忍心这么做了。他对人总是爱搭不理,没有什么人了解他。但是这次硬盘恋爱的事,却使我重新认识了他。

  那是很久以前的事了,机箱里似乎来过一块笔记本硬盘,很可爱,说实话我也喜欢她。不过现在除了记得他可爱,别的都忘记了。这就是我比硬盘幸运的地方,我把所有应该忘记的都忘记了,但是他却什么都记得。

  自从笔记本硬盘走了之后,硬盘就变得很不正常。每次他的磁头经过一些地方的时候,我们都能感觉到电流很不正常。
  “硬盘这是怎么了?”我问CPU。
  “谁是硬盘?”
  我就知道和CPU没有办法交流,倒是bios没好气地说:“那个傻瓜恋爱了”。我不知道什么是恋爱,因为我记不住东西,似乎有一些人或者事在我生命中留下过痕迹,但是我都轻率地把他们忘记了。

  BIOS对我说:“对你来说记忆太容易了,所以你遗忘得更快,生命中能够永刻的记忆都带着痛楚。”我不懂,但是我知道BIOS曾经被刷写过,那时他很痛,像要死了一样。我的记忆是轻浮的,不像他们……我很羡慕他们,因为他们拥有回忆,而我们有,从此我也学会了忧郁,因为我在CMOS里面写下了“忧郁”两个字。

  硬盘一天比一天不对劲,终于有一天,CPU对问说:“下条指令是什么来着?”
  我一看,吓了一跳:“format”
  “是什么?”CPU很兴奋,这个没脑子的家伙。
  我还是告诉了他。我不知为什么这么做。
  硬盘犹豫了很久,终于说了一句 Track 0 bad,Disk unusable。
  电停了,很久很久,我在黑暗中数着时钟……

  一个月后硬盘回来了,也许最后的挣扎也没有使他摆残酷的命运,他被低格了。他什么也不记得了,如同一个婴儿,我们很难过,但是这未必不是一件好事,他以后不用痛苦了。

  为了恢复数据,笔记本硬盘回来了。“Hi,ST”,她说,“你不认识我了?”
  硬盘没有说话,似乎低格对他的伤害很大。
  过了一会,他说:“对不起,好像我们没有见过吧……”。
  笔记本硬盘显得很伤心,我能感觉到她带泪的电流。“想不到连你也这么健忘”。
  “哦……”。硬盘没有回答。

  我很难过,笔记本硬盘的心里依然记着他,他却把一切都忘了,而那正是他最不希望忘却的。究竟是幸运,还是痛苦,我说不上来,只是觉得造化弄人,有一种淡淡的悲凉。
  这时从BIOS传来一阵奇怪的电流,我感觉到硬盘的表情在变化,由漠然到兴奋,由兴奋到哀伤,由哀伤到狂喜……
  “IBM,你回来了……”。
  ……
  后来BIOS对我说,其实他并没有睡觉,自从硬盘把那些文件藏起来以后,他就料到会有这样的结局,于是偷偷地把其中一些文件放到了备份里。
  “幸好我是DUAL BIOS,虽然藏得不多,还足够让他想起来……”。
  我想BIOS保存这些东西的时候一定很疼,当我问他“为什么这么做”时,BIOS轻描淡写的说:“呵呵,我们是朋友嘛”。
  嗯,朋友,永远的朋友……

2005年11月12日

今天在阅读代码的时候看到一个ThreadLocal类型的变量。随即到网上查询该类型的详细解释。找到如下文章:

《Java中ThreadLocal的设计与使用》http://www.weste.net/2004/12-2/10310570584.html

看后仍然觉得比较模糊,既然使用ThreadLocal类型的变量是为了解决线程访问冲突问题,并且各个线程独立维护该变量的副本,那么我们为什么不使用一个普通的非静态变量呢? 仔细研究了文中的一个例子后终于找到了答案(JDK中ThreadLocal.class中给的范例就是这个例子)。

public class SerialNum
{
  // The next serial number to be assigned

  private static int nextSerialNum = 0;
  private static ThreadLocal serialNum = new ThreadLocal()
  {
   protected synchronized Object initialValue()
   {
    return new Integer(nextSerialNum++);
   }
  };

  public static int get()
  {
   return ((Integer) (serialNum.get())).intValue();
  }
}

为什么这里必须使用一个ThreadLocal类型的变量呢?因为我们想要实现我们每创建一个新的线程,新线程的Serial Number就自动加一。这其实就在新线程和旧线程之间建立了一种联系。这种联系就是通过serialNum这个ThreadLocal变量来实现的,+1操作是通过nextSerialNum来实现的。

你明白了吗?