关于作者

用户名:SaviourJLF
笔名:子枫
地区: -
行业:其他

日历  

快速登录

+ 用户名:
+ 密 码:

在线留言



访问统计:
文章个数:47
评论个数:1
留言条数:4




Powered by BlogDriver 2.1

子枫的博客

 

从明天起, 做一个幸福的人
喂马, 劈柴, 周游世界
从明天起, 关心粮食和蔬菜
我有一所房子, 面朝大海, 春暖花开

文章

JavaScript:世界上误解最深的语言[转]

JavaScript:
The World's Most Misunderstood Programming Languag


JavaScript:世界上误解最深的语言

Douglas Crockford
www.crockford.com




JavaScript, aka Mocha, aka LiveScript, aka JScript, aka ECMAScript, is one of the world's most popular programming languages. Virtually every personal computer in the world has at least one JavaScript interpreter installed on it and in active use. JavaScript's popularity is due entirely to its role as the scripting language of the WWW.

JavaScript,或者叫 Mocha,或者叫 LiveScript,或者叫 JScript,又或者叫 ECMAScript,是世界上最流行的编程语言之一。事实上世界上的每一台个人电脑都安装并在频繁使用至少一个JavaScript解释器。JavaScript的流行完全是由于他在WWW脚本语言领域中的地位决定的。


Despite its popularity, few know that JavaScript is a very nice dynamic object-oriented general-purpose programming language. How can this be a secret? Why is this language so misunderstood?

尽管它很流行,但是很少有人知道JavaScript是一个非常棒的动态面向对象通用编程语言。这居然能成为一个秘密!这门语言为什么被误解如此之深?



The Name

名字

The Java- prefix suggests that JavaScript is somehow related to Java, that it is a subset or less capable version of Java. It seems that the name was intentionally selected to create confusion, and from confusion comes misunderstanding. JavaScript is not interpreted Java. Java is interpreted Java. JavaScript is a different language.

Java- 前缀很容易使人联想到Java,并认为它是Java的子集或简化版的Java。看起来最初给它选这个名字是别有用心的,是故意混淆概念、故意制造"误解"的。JavaScript不是解释执行的Java。Java是解释执行的Java。JavaScript是另外一种语言。


JavaScript has a syntactic similarity to Java, much as Java has to C. But it is no more a subset of Java than Java is a subset of C. It is better than Java in the applications that Java (fka Oak) was originally intended for.

JavaScript的语法和Java有相似之处,这就像Java的语法和C很相像一样。但是它不是Java的子集,就像Java不是C的子集一样。它在Java(Oak)最初打算进军的领域中比Java更好。


JavaScript was not developed at Sun Microsystems, the home of Java. JavaScript was developed at Netscape. It was originally called LiveScript, but that name wasn't confusing enough.

JavaScript不是Sun Microsystems的产品,Sun是Java的家。JavaScript是在Netscape被开发出来的。它最初叫LiveScript,嗯……还是这个名字好。


The -Script suffix suggests that it is not a real programming language, that a scripting language is less than a programming language. But it is really a matter of specialization. Compared to C, JavaScript trades performance for expressive power and dynamism.

-Script后缀让人认为他不是一门真正的编程语言,和一门"编程语言"还有相当的差距。但是这只是应用领域的问题。和C相比,JavaScript是牺牲了性能但换来了丰富的表现力和灵活的形态。




Lisp in C's Clothing

披着C皮的Lisp

JavaScript's C-like syntax, including curly braces and the clunky for statement, makes it appear to be an ordinary procedural language. This is misleading because JavaScript has more in common with functional languages like Lisp or Scheme than with C or Java. It has arrays instead of lists and objects instead of property lists. Functions are first class. It has closures. You get lambdas without having to balance all those parens.

JavaScript的类C语法,包括大括号和语句的形式,让它看起来像普通的面向过程编程语言。这是一种误解,因为JavaScript和函数式语言,比如 Lisp 或 Scheme,有更多的相似之处,而不是和C或Java。它使用数组而不是列表,使用对象而不是属性列表。函数是第一位的,它有闭包(closures),另外你还可以使用lambda表达式。

 




Typecasting

类型转换

JavaScript was designed to run in Netscape Navigator. Its success there led to it becoming standard equipment in virtually all web browsers. This has resulted in typecasting. JavaScript is the George Reeves of programming languages. JavaScript is well suited to a large class of non-Web-related applications

JavaScript最初被设计成在Netscape Navigator中运行,它在Navigator中的成功引领它成为事实上所有web浏览器的标准装备。这就造就了"类型转换"。JavaScript是编程语言中的 George Reeves(超人),是大量非web程序的称职之选。




Moving Target

移动靶

The first versions of JavaScript were quite weak. They lacked exception handling, inner functions, and inheritance. In its present form, it is now a complete object-oriented programming language. But many opinions of the language are based on its immature forms.

JavaScript的最初几版非常弱,没有异常处理,没有内部函数和继承。现如今,它已经成为完全面向对象的编程语言。但是这门语言的许多思想是基于它不成熟的形式的。


The ECMA committee that has stewardship over the language is developing extensions which, while well intentioned, will aggravate one of the language's biggest problems: There are already too many versions. This creates confusion.

ECMA委员会,这门语言的管家,正在对它进行扩展,也在蓄意恶化它最大的问题:有太多的版本。这是混乱的根源。





Design Errors

设计上的错误

No programming language is perfect. JavaScript has its share of design errors, such as the overloading of + to mean both addition and concatenation with type coercion, and the error-prone with statement should be avoided. The reserved word policies are much too strict. Semicolon insertion was a huge mistake, as was the notation for literal regular expressions. These mistakes have led to programming errors, and called the design of the language as a whole into question. Fortunately, many of these problems can be mitigated with a good lint program.

没有什么编程语言是完美的。JavaScript也有它设计上的错误,比如重载的+号随着类型的不同既表示"相加"又表示"连接",和本该避免的有错误倾向的 with 语句。它的保留字策略过于严格。分号的插入是一个巨大的错误,比如作为字面正则表达式的符号时。这些失误已直接导致编程中的错误,也使这门语言的整体设计遭人质疑。还好,这些问题中有许多都可以在良好的 lint 程序中得以缓解。


The design of the language on the whole is quite sound. Surprisingly, the ECMAScript committee does not appear to be interested in correcting these problems. Perhaps they are more interested in making new ones.

这门语言的整体设计(上的问题)是相当明显的。奇怪的是ECMAScript委员会并没有对修正其中存在的问题表现出太大的兴趣,也许他们更热衷于制造新的问题。


 


Lousy Implementations

糟糕的实现

Some of the earlier implementations of JavaScript were quite buggy. This reflected badly on the language. Compounding that, those implementations were embedded in horribly buggy web browsers.

JavaScript的一些早期实现有许多bug,这反过来对语言本身产生了很坏的影响。更糟糕的是这些满是bug的实现是嵌入在满是bug的web浏览器中的。




Bad Books

糟糕的书

Nearly all of the books about JavaScript are quite awful. They contain errors, poor examples, and promote bad practices. Important features of the language are often explained poorly, or left out entirely. I have reviewed dozens of JavaScript books, and I can only recommend one: JavaScript: The Definitive Guide (4th Edition) by David Flanagan. (Attention authors: If you have written a good one, please send me a review copy.)

几乎所有的JavaScript书都是相当可怕的。它们包含错误,包含不好的例子,并鼓励不好的做法。JavaScript语言的一些重要特性它们要么没有解释清楚,要么根本就没有提及。我看过很多JavaScript的书,但我只能推荐一本:David Flanagan著的 JavaScript: The Definitive Guide (4th Edition) (《JavaScript权威指南 第四版》)。(作者们请注意:如果你们写出了好书请发给我一份副本,我给你们校对。)




Substandard Standard

准标准”的标准

The official specification for the language is published by ECMA. The specification is of extremely poor quality. It is difficult to read and very difficult to understand. This has been a contributor to the Bad Book problem because authors have been unable to use the standard document to improve their own understanding of the language. ECMA and the TC39 committee should be deeply embarrassed.

ECMA公布的官方语言规范的质量极其的差。不仅难读而且极其难懂。它可为那些"糟糕的书"做出了不小的贡献,因为那些作者无法通过这个标准文档来更深地理解这门语言。ECMA和TC39应该为此感到非常尴尬。




Amateurs

业余者

Most of the people writing in JavaScript are not programmers. They lack the training and discipline to write good programs. JavaScript has so much expressive power that they are able to do useful things in it, anyway. This has given JavaScript a reputation of being strictlyfor the amateurs, that it is not suitable for professional programming. This is simply not the case.

使用JavaScript的人大多不是程序员,他们缺少写良好程序的培训和训练。JavaScript有非常强大的表现力,不管怎样他们也能使用它做有用的事情。这给了JavaScript一个”全然适合业余爱好者而不适合专业程序员“的名声。这很明显是一个错误。




Object-Oriented

面向对象

Is JavaScript object-oriented? It has objects which can contain data and methods that act upon that data. Objects can contain other objects. It does not have classes, but it does have constructors which do what classes do, including acting as containers for class variables and methods. It does not have class-oriented inheritance, but it does have prototype-oriented inheritance.

JavaScript是面向对象的吗?它有对象,它的对象可以包含数据以及对数据进行操作的方法,对象也可以包含其他的对象。它没有类,但是它有构造函数来做类的事情,包括声明类的变量和方法。它没有面向类的继承,但是他有面向原型的继承。


The two main ways of building up object systems are by inheritance (is-a) and by aggregation (has-a). JavaScript does both, but its dynamic nature allows it to excel at aggregation.

构建对象系统的两大主要方法是继承(is-a)和聚合(has-a)。这两者JavaScript都有,但是它的动态天性允许有比聚合更好的实现方式。


Some argue that JavaScript is not truly object oriented because it does not provide information hiding. That is, objects cannot have private variables and private methods: All members are public.

一些关于JavaScript不是真的面向对象的争论其理由是它没有提供信息隐藏。也就是说JavaScript的对象没有私有变量和私有方法:它的所有成员都是公开的。


But it turns out that JavaScript objects can have private variables and private methods. (Click here now to find out how.) Of course, few understand this because JavaScript is the world's most misunderstood programming language.

但是事实是JavaScript 的对象可以有私有变量和私有方法(点击这里来看如何实现)。当然,之所以很少有人知道这个是因为JavaScript是世界上误解最深的语言嘛。


Some argue that JavaScript is not truly object oriented because it does not provide inheritance. But it turns out that JavaScript supports not only classical inheritance, but other code reuse patterns as well.

另一些关于JavaScript不是真的面向对象的争论其理由是它没有提供继承。但是事实是JavaScript不但支持经典的继承,而且支持其他一些代码重用的模式。


Copyright 2001 Douglas Crockford. All Rights Reserved Wrrrldwide.


版权所有

- 作者: 子枫 2008年05月27日, 星期二 16:27  回复(0) |  引用(0) 加入博采

Java Web 框架的“甜点”[转]
摘要:Jave Web Framework Sweet Spots Java Web 框架的“甜点” 查看全文

- 作者: 子枫 2008年05月27日, 星期二 11:51  回复(0) |  引用(0) 加入博采

Planned nd Evolutionary Design
摘要:转自CSDN 新闻频道,原文章为《设计已死?》 http://news.csdn.net/n/20060429/89952.html 查看全文

- 作者: 子枫 2006年06月1日, 星期四 17:37  回复(0) |  引用(2) 加入博采

[转]全世界所有程序员都会犯的错误-蔡学镛
摘要:当年,国际巨星成龙的「龙种」曝光,他出面召开记者会,向世人自白他犯了「全世界所有男人都会犯的错误」。。。虽然没犯过「全世界所有男人都会犯的错误」,但是我倒是曾经犯了「全世界所有程序员都会犯的错误」。。。 查看全文

- 作者: 子枫 2006年01月20日, 星期五 10:28  回复(0) |  引用(2) 加入博采

[转]log4j配置笔记

(1)首先需要一个log4j-1.2.*.jar包,jar包的位置:webapps/web/WEB-INF/lib/下
在log4j Web站点下载ZIP(Windows)或gzipped(基于Unix系统)格式的发行版本:http://jakarta.apache.org/log4j/docs/download.html 下载,将发行版解包,将创建logging-log4j-1.2.13目录,在此顶层目录dist/lib/目录下有log4j-1.2.13.jar文件。

(2)在web/WEB-INF/web.xml文件中做如下配置:配置一个Servlet映射,用来初始化log4j.
<servlet>
    <servlet-name>log4jInitServlet</servlet-name>
    <servlet-class>com.airingbox.kzSuggestion.Log4jInitServlet</servlet-class>
    <init-param>
        <param-name>Log4j-init-file</param-name>
        <param-value>/WEB-INF/classes/log4j.properties</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>log4jInitServlet</servlet-name>
    <url-pattern>/log4jinitservlet</url-pattern>
</servlet-mapping>
<load-on-startup>1</load-on-startup>是启动顺序,0:表示什么时候用什么时候启动,1:表示自动启动

(3)编写初始化的servlet的类文件
package com.airinbox.kzSuggestion;

import org.apache.log4j.PropertyConfigurator;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletContext;
import java.io.PrintWriter;
import java.io.IOException;

/**
 * 初始化log4j模块
 * <p>Title: init</p>
 * <p>Description: </p>
 * <p>Copyright: Copyright (c) 2005</p>
 * <p>Company: </p>
 * @author zhanglipeng
 * @version 1.0
 */
public class Log4jInitServlet extends HttpServlet {

  public  void init() {
    String prefix =  getServletContext().getRealPath("/");
    String file = getInitParameter("Log4j-init-file");
    if(file != null) {
      /**
       *读取配置文件的方法
       *BasicConfigurator.configure (): 自动快速地使用缺省Log4j环境。
       *PropertyConfigurator.configure ( String configFilename) :读取使用Java
       *的特性文件编写的配置文件。
       *DOMConfigurator.configure ( String filename ) :读取XML形式的配置文件。
       */
      PropertyConfigurator.configure(prefix+file);
    }
    System.out.println("LOG4J模块初始化成功!");
  }

  public  void doGet(HttpServletRequest req, HttpServletResponse res) {
  }
}
  (4)编写要读取的属性文件
# 将信息输出到控制台
log4j.rootLogger=INFO,A1,A4
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-4r %-5p [%t] %37c %3x - %m%n
# Available levels are DEBUG, INFO, WARN, ERROR, FATAL
他们级别最高的是fatal,就是说如果你在log4j.rootLogger=fatal,A1,A4 那么在日志中就不会出现比他级别低的debug,info,warn,error的日志信息,如果用info,就只是不会出现debug信息
这里的%p是指优先级,%m是指message本身,%n是指在新的一行输出


 

# 将信息输出到mylog.txt文件中
 log4j.appender.A4=org.apache.log4j.FileAppender
 log4j.appender.A4.file=/home/kzSuggestion/log/mylog.txt
 log4j.appender.A4.layout=org.apache.log4j.PatternLayout
 log4j.appender.A4.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n

   a.rootLogger:log书写器,A1,A4,A6,A8是日志存储器的名字,就是说你要将日志输出到控制台,输出到文件,还是每天产生一个文件,还是通过UDP输出到文件
log4j.appender.A4.file=/home/kzSuggestion/log/mylog.txt是要输出到文件的绝对路径
   b.Appender - 日志目的地,把格式化好的日志信息输出到指定的地方去  

   c.ConsoleAppender - 目的地为控制台的Appender

   d.FileAppender - 目的地为文件的Appender    e.RollingFileAppender - 目的地为大小受限的文件的Appender

   f.Layout - 日志格式化器,用来把程序员的logging request格式化成字符串
      PatternLayout - 用指定的pattern格式化logging request的Layout

   g.Log4j提供的layout有以下几种:
  org.apache.log4j.HTMLLayout(以HTML表格形式布局),
  org.apache.log4j.PatternLayout(可以灵活地指定布局模式),
  org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),
  org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)

  Log4J采用类似C语言中的printf函数的打印格式格式化日志信息,打印参数如下: %m 输出代码中指定的消息

  %p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
  %r 输出自应用启动到输出该log信息耗费的毫秒数
  %c 输出所属的类目,通常就是所在类的全名
  %t 输出产生该日志事件的线程名
  %n 输出一个回车换行符,Windows平台为“\r\n”,Unix平台为“\n”
  %d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921
  %l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)

(5)这样就可以试一下了,在JSP或JAVA中输出log
    a:导入org.apache.log4j.Logger和org.apache.log4j.propertyConfigurator包
    b:调用方法:Logger logger = Logger.getLogger(你的类名.class.getName());
       或者:Logger logger = Logger.getLogger(*.jsp);
       logger.info("come in *** "); //输出log
    c:插入log信息的4种方法
  Logger.debug ( Object message ) ;
  Logger.info ( Object message ) ;
  Logger.warn ( Object message ) ;
  Logger.error ( Object message ) ;
 (6)这时可以在控制台或在应用下/log/mylog.txt中看到log的输出信息

资料下载可以到log4j的主页:

Log4j主页:http://jakarta.apache.org/log4j

- 作者: 子枫 2005年12月16日, 星期五 12:01  回复(0) |  引用(2) 加入博采

java多线程设计模式

java语言已经内置了多线程支持,所有实现Runnable接口的类都可被启动一个新线程,新线程会执行该实例的run()方法,当run()方法执行完毕后,线程就结束了。一旦一个线程执行完毕,这个实例就不能再重新启动,只能重新生成一个新实例,再启动一个新线程。

Thread类是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法:

Thread t = new Thread();
t.start();

start()方法是一个native方法,它将启动一个新线程,并执行run()方法。Thread类默认的run()方法什么也不做就退出了。注意:直接调用run()方法并不会启动一个新线程,它和调用一个普通的java方法没有什么区别。

因此,有两个方法可以实现自己的线程:

方法1:自己的类extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:

public class MyThread extends Thread {
    public run() {
        System.out.println("MyThread.run()");
    }
}

在合适的地方启动线程:new MyThread().start();

方法2:如果自己的类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口:

public class MyThread extends OtherClass implements Runnable {
    public run() {
        System.out.println("MyThread.run()");
    }
}

为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:

MyThread myt = new MyThread();
Thread t = new Thread(myt);
t.start();

事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:

public void run() {
    if (target != null) {
        target.run();
    }
}

线程还有一些Name, ThreadGroup, isDaemon等设置,由于和线程设计模式关联很少,这里就不多说了。

由于同一进程内的多个线程共享内存空间,在Java中,就是共享实例,当多个线程试图同时修改某个实例的内容时,就会造成冲突,因此,线程必须实现共享互斥,使多线程同步。

最简单的同步是将一个方法标记为synchronized,对同一个实例来说,任一时刻只能有一个synchronized方法在执行。当一个方法正在执行某个synchronized方法时,其他线程如果想要执行这个实例的任意一个synchronized方法,都必须等待当前执行 synchronized方法的线程退出此方法后,才能依次执行。

但是,非synchronized方法不受影响,不管当前有没有执行synchronized方法,非synchronized方法都可以被多个线程同时执行。

此外,必须注意,只有同一实例的synchronized方法同一时间只能被一个线程执行,不同实例的synchronized方法是可以并发的。例如,class A定义了synchronized方法sync(),则不同实例a1.sync()和a2.sync()可以同时由两个线程来执行。

多线程同步的实现最终依赖锁机制。我们可以想象某一共享资源是一间屋子,每个人都是一个线程。当A希望进入房间时,他必须获得门锁,一旦A获得门锁,他进去后就立刻将门锁上,于是B,C,D...就不得不在门外等待,直到A释放锁出来后,B,C,D...中的某一人抢到了该锁(具体抢法依赖于 JVM的实现,可以先到先得,也可以随机挑选),然后进屋又将门锁上。这样,任一时刻最多有一人在屋内(使用共享资源)。

Java语言规范内置了对多线程的支持。对于Java程序来说,每一个对象实例都有一把“锁”,一旦某个线程获得了该锁,别的线程如果希望获得该锁,只能等待这个线程释放锁之后。获得锁的方法只有一个,就是synchronized关键字。例如:

public class SharedResource {
    private int count = 0;

    public int getCount() { return count; }

    public synchronized void setCount(int count) { this.count = count; }

}

同步方法public synchronized void setCount(int count) { this.count = count; } 事实上相当于:

public void setCount(int count) {
    synchronized(this) { // 在此获得this锁
         this.count = count;

    } // 在此释放this锁
}

红色部分表示需要同步的代码段,该区域为“危险区域”,如果两个以上的线程同时执行,会引发冲突,因此,要更改SharedResource的内部状态,必须先获得SharedResource实例的锁。

退出synchronized块时,线程拥有的锁自动释放,于是,别的线程又可以获取该锁了。

为了提高性能,不一定要锁定this,例如,SharedResource有两个独立变化的变量:

public class SharedResouce {
    private int a = 0;
    private int b = 0;

    public synchronized void setA(int a) { this.a = a; }

    ublic synchronized void setB(int b) { this.b = b; }
}

若同步整个方法,则setA()的时候无法setB(),setB()时无法setA()。为了提高性能,可以使用不同对象的锁:

public class SharedResouce {
    private int a = 0;
    private int b = 0;
    private Object sync_a = new Object();
    private Object sync_b = new Object();

    public void setA(int a) {
        synchronized(sync_a) {
            this.a = a;
        }
    }

    public synchronized void setB(int b) {
        synchronized(sync_b) {
            this.b = b;
        }
    }
}

通常,多线程之间需要协调工作。例如,浏览器的一个显示图片的线程displayThread想要执行显示图片的任务,必须等待下载线程 downloadThread将该图片下载完毕。如果图片还没有下载完,displayThread可以暂停,当downloadThread完成了任务后,再通知displayThread“图片准备完毕,可以显示了”,这时,displayThread继续执行。

以上逻辑简单的说就是:如果条件不满足,则等待。当条件满足时,等待该条件的线程将被唤醒。在Java中,这个机制的实现依赖于wait/notify。等待机制与锁机制是密切关联的。例如:

synchronized(obj) {
    while(!condition) {
        obj.wait();
    }
    obj.doSomething();
}

当线程A获得了obj锁后,发现条件condition不满足,无法继续下一处理,于是线程A就wait()。

在另一线程B中,如果B更改了某些条件,使得线程A的condition条件满足了,就可以唤醒线程A:

synchronized(obj) {
    condition = true;
    obj.notify();
}

需要注意的概念是:

# 调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) {...} 代码段内。

# 调用obj.wait()后,线程A就释放了obj的锁,否则线程B无法获得obj锁,也就无法在synchronized(obj) {...} 代码段内唤醒A。

# 当obj.wait()方法返回后,线程A需要再次获得obj锁,才能继续执行。

# 如果A1,A2,A3都在obj.wait(),则B调用obj.notify()只能唤醒A1,A2,A3中的一个(具体哪一个由JVM决定)。

# obj.notifyAll()则能全部唤醒A1,A2,A3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,A1,A2,A3只有一个有机会获得锁继续执行,例如A1,其余的需要等待A1释放obj锁之后才能继续执行。

# 当B调用obj.notify/notifyAll的时候,B正持有obj锁,因此,A1,A2,A3虽被唤醒,但是仍无法获得obj锁。直到B退出synchronized块,释放obj锁后,A1,A2,A3中的一个才有机会获得锁继续执行。


前面讲了wait/notify机制,Thread还有一个sleep()静态方法,它也能使线程暂停一段时间。sleep与wait的不同点是: sleep并不释放锁,并且sleep的暂停和wait暂停是不一样的。obj.wait会使线程进入obj对象的等待集合中并等待唤醒。

但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。

如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在 wait/sleep/join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。

需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException。


GuardedSuspention模式主要思想是:

当条件不满足时,线程等待,直到条件满足时,等待该条件的线程被唤醒。

我们设计一个客户端线程和一个服务器线程,客户端线程不断发送请求给服务器线程,服务器线程不断处理请求。当请求队列为空时,服务器线程就必须等待,直到客户端发送了请求。

先定义一个请求队列:Queue

package com.crackj2ee.thread;

import java.util.*;

public class Queue {
    private List queue = new LinkedList();

    public synchronized Request getRequest() {
        while(queue.size()==0) {
            try {
                this.wait();
            }
            catch(InterruptedException ie) {
                return null;
            }
        }
        return (Request)queue.remove(0);
    }

    public synchronized void putRequest(Request request) {
        queue.add(request);
        this.notifyAll();
    }

}

蓝色部分就是服务器线程的等待条件,而客户端线程在放入了一个request后,就使服务器线程等待条件满足,于是唤醒服务器线程。

客户端线程:ClientThread

package com.crackj2ee.thread;

public class ClientThread extends Thread {
    private Queue queue;
    private String clientName;

    public ClientThread(Queue queue, String clientName) {
        this.queue = queue;
        this.clientName = clientName;
    }

    public String toString() {
        return "[ClientThread-" + clientName + "]";
    }

    public void run() {
        for(int i=0; i<100; i++) {
            Request request = new Request("" + (long)(Math.random()*10000));
            System.out.println(this + " send request: " + request);
            queue.putRequest(request);
            try {
                Thread.sleep((long)(Math.random() * 10000 + 1000));
            }
            catch(InterruptedException ie) {
            }
        }
        System.out.println(this + " shutdown.");
    }
}

服务器线程:ServerThread

package com.crackj2ee.thread;
public class ServerThread extends Thread {
    private boolean stop = false;
    private Queue queue;

    public ServerThread(Queue queue) {
        this.queue = queue;
    }

    public void shutdown() {
        stop = true;
        this.interrupt();
        try {
     &nbp;      this.join();
        }
        catch(InterruptedException ie) {}
    }

    public void run() {
        while(!stop) {
            Request request = queue.getRequest();
            System.out.println("[ServerThread] handle request: " + request);
            try {
                Thread.sleep(2000);
            }
            catch(InterruptedException ie) {}
        }
        System.out.println("[ServerThread] shutdown.");
    }
}

服务器线程在红色部分可能会阻塞,也就是说,Queue.getRequest是一个阻塞方法。这和java标准库的许多IO方法类似。

最后,写一个Main来启动他们:

package com.crackj2ee.thread;

public class Main {

    public static void main(String[] args) {
        Queue queue = new Queue();
        ServerThread server = new ServerThread(queue);
        server.start();
        ClientThread[] clients = new ClientThread[5];
        for(int i=0; i            clients[i] = new ClientThread(queue, ""+i);
            clients[i].start();
        }
        try {
            Thread.sleep(100000);
        }
        catch(InterruptedException ie) {}
        server.shutdown();
    }
}

我们启动了5个客户端线程和一个服务器线程,运行结果如下:

[ClientThread-0] send request: Request-4984
[ServerThread] handle request: Request-4984
[ClientThread-1] send request: Request-2020
[ClientThread-2] send request: Request-8980
[ClientThread-3] send request: Request-5044
[ClientThread-4] send request: Request-548
[ClientThread-4] send request: Request-6832
[ServerThread] handle request: Request-2020
[ServerThread] handle request: Request-8980
[ServerThread] handle request: Request-5044
[ServerThread] handle request: Request-548
[ClientThread-4] send request: Request-1681
[ClientThread-0] send request: Request-7859
[ClientThread-3] send request: Request-3926
[ServerThread] handle request: Request-6832
[ClientThread-2] send request: Request-9906
......

可以观察到ServerThread处理来自不同客户端的请求。

思考

Q: 服务器线程的wait条件while(queue.size()==0)能否换成if(queue.size()==0)?

A: 在这个例子中可以,因为服务器线程只有一个。但是,如果服务器线程有多个(例如Web应用程序有多个线程处理并发请求,这非常普遍),就会造成严重问题。

Q: 能否用sleep(1000)代替wait()?

A: 绝对不可以。sleep()不会释放锁,因此sleep期间别的线程根本没有办法调用getRequest()和putRequest(),导致所有相关线程都被阻塞。

Q: (Request)queue.remove(0)可以放到synchronized() {}块外面吗?

A: 不可以。因为while()是测试queue,remove()是使用queue,两者是一个原子操作,不能放在synchronized外面。

总结

多线程设计看似简单,实际上必须非常仔细地考虑各种锁定/同步的条件,稍不小心,就可能出错。并且,当线程较少时,很可能发现不了问题,一旦问题出现又难以调试。

所幸的是,已有一些被验证过的模式可以供我们使用,我们会继续介绍一些常用的多线程设计模式。

前面谈了多线程应用程序能极大地改善用户相应。例如对于一个Web应用程序,每当一个用户请求服务器连接时,服务器就可以启动一个新线程为用户服务。

然而,创建和销毁线程本身就有一定的开销,如果频繁创建和销毁线程,CPU和内存开销就不可忽略,垃圾收集器还必须负担更多的工作。因此,线程池就是为了避免频繁创建和销毁线程。

每当服务器接受了一个新的请求后,服务器就从线程池中挑选一个等待的线程并执行请求处理。处理完毕后,线程并不结束,而是转为阻塞状态再次被放入线程池中。这样就避免了频繁创建和销毁线程。

Worker Pattern实现了类似线程池的功能。首先定义Task接口:

package com.crackj2ee.thread;
public interface Task {
    void execute();
}

线程将负责执行execute()方法。注意到任务是由子类通过实现execute()方法实现的,线程本身并不知道自己执行的任务。它只负责运行一个耗时的execute()方法。

具体任务由子类实现,我们定义了一个CalculateTask和一个TimerTask:

// CalculateTask.java
package com.crackj2ee.thread;
public class CalculateTask implements Task {
    private static int count = 0;
    private int num = count;
    public CalculateTask() {
        count++;
    }
    public void execute() {
        System.out.println("[CalculateTask " + num + "] start...");
        try {
            Thread.sleep(3000);
        }
        catch(InterruptedException ie) {}
        System.out.println("[CalculateTask " + num + "] done.");
    }
}

// TimerTask.java
package com.crackj2ee.thread;
public class TimerTask implements Task {
    private static int count = 0;
    private int num = count;
    public TimerTask() {
        count++;
    }
    public void execute() {
        System.out.println("[TimerTask " + num + "] start...");
        try {
            Thread.sleep(2000);
        }
        catch(InterruptedException ie) {}
        System.out.println("[TimerTask " + num + "] done.");
    }
}

以上任务均简单的sleep若干秒。

TaskQueue实现了一个队列,客户端可以将请求放入队列,服务器线程可以从队列中取出任务:

package com.crackj2ee.thread;
import java.util.*;
public class TaskQueue {
    private List queue = new LinkedList();
    public synchronized Task getTask() {
        while(queue.size()==0) {
            try {
                this.wait();
            }
      nbsp;     catch(InterruptedException ie) {
                return null;
            }
        }
        return (Task)queue.remove(0);
    }
    public synchronized void putTask(Task task) {
        queue.add(task);
        this.notifyAll();
    }
}

终于到了真正的WorkerThread,这是真正执行任务的服务器线程:

package com.crackj2ee.thread;
public class WorkerThread extends Thread {
    private static int count = 0;
    private boolean busy = false;
    private boolean stop = false;
    private TaskQueue queue;
    public WorkerThread(ThreadGroup group, TaskQueue queue) {
        super(group, "worker-" + count);
        count++;
        this.queue = queue;
    }
    public void shutdown() {
        stop = true;
        this.interrupt();
        try {
            this.join();
        }
        catch(InterruptedException ie) {}
    }
    public boolean isIdle() {
        return !busy;
    }
    public void run() {
        System.out.println(getName() + " start.");       
        while(!stop) {
            Task task = queue.getTask();
            if(task!=null) {
                busy = true;
                task.execute();
                busy = false;
            }
        }
        System.out.println(getName() + " end.");
    }
}

前面已经讲过,queue.getTask()是一个阻塞方法,服务器线程可能在此wait()一段时间。此外,WorkerThread还有一个shutdown方法,用于安全结束线程。

最后是ThreadPool,负责管理所有的服务器线程,还可以动态增加和减少线程数:

package com.crackj2ee.thread;
import java.util.*;
public class ThreadPool extends ThreadGroup {
    private List threads = new LinkedList();
    private TaskQueue queue;
    public ThreadPool(TaskQueue queue) {
        super("Thread-Pool");
        this.queue = queue;
    }
    public synchronized void addWorkerThread() {
        Thread t = new WorkerThread(this, queue);
        threads.add(t);
        t.start();
    }
    public synchronized void removeWorkerThread() {
        if(threads.size()>0) {
            WorkerThread t = (WorkerThread)threads.remove(0);
            t.shutdown();
        }
    }
    public synchronized void currentStatus() {
        System.out.println("-----------------------------------------------");
        System.out.println("Thread count = " + threads.size());
        Iterator it = threads.iterator();
        while(it.hasNext()) {
            WorkerThread t = (WorkerThread)it.next();
            System.out.println(t.getName() + ": " + (t.isIdle() ? "idle" : "busy"));
        }
        System.out.println("-----------------------------------------------");
    }
}

currentStatus()方法是为了方便调试,打印出所有线程的当前状态。

最后,Main负责完成main()方法:

package com.crackj2ee.thread;
public class Main {
    public static void main(String[] args) {
        TaskQueue queue = new TaskQueue();
        ThreadPool pool = new ThreadPool(queue);
        for(int i=0; i<10; i++) {
            queue.putTask(new CalculateTask());
            queue.putTask(new TimerTask());
        }
        pool.addWorkerThread();
        pool.addWorkerThread();
        doSleep(8000);
        pool.currentStatus();
        pool.addWorkerThread();
        pool.addWorkerThread();
        pool.addWorkerThread();
        pool.addWorkerThread();
        pool.addWorkerThread();
        doSleep(5000);
        pool.currentStatus();
    }
    private static void doSleep(long ms) {
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException ie) {}
    }
}

main()一开始放入了20个Task,然后动态添加了一些服务线程,并定期打印线程状态,运行结果如下:

worker-0 start.
[CalculateTask 0] start...
worker-1 start.
[TimerTask 0] start...
[TimerTask 0] done.
[CalculateTask 1] start...
[CalculateTask 0] done.
[TimerTask 1] start...
[CalculateTask 1] done.
[CalculateTask 2] start...
[TimerTask 1] done.
[TimerTask 2] start...
[TimerTask 2] done.
[CalculateTask 3] start...
-----------------------------------------------
Thread count = 2
worker-0: busy
worker-1: busy
-----------------------------------------------
[CalculateTask 2] done.
[TimerTask 3] start...
worker-2 start.
[CalculateTask 4] start...
worker-3 start.
[TimerTask 4]start...
worker-4 start.
[CalculateTask 5] start...
worker-5 start.
[TimerTask 5] start...
worker-6 start.
[CalculateTask 6] start...
[CalculateTask 3] done.
[TimerTask 6] start...
[TimerTask 3] done.
[CalculateTask 7] start...
[TimerTask 4] done.
[TimerTask 7] start...
[TimerTask 5] done.
[CalculateTask 8] start...
[CalculateTask 4] done.
[TimerTask 8] start...
[CalculateTask 5] done.
[CalculateTask 9] start...
[CalculateTask 6] done.
[TimerTask 9] start...
[TimerTask 6] done.
[TimerTask 7] done.
-----------------------------------------------
Thread count = 7
worker-0: idle
worker-1: busy
worker-2: busy
worker-3: idle
worker-4: busy
worker-5: busy
worker-6: busy
-----------------------------------------------
[CalculateTask 7] done.
[CalculateTask 8] done.
[TimerTask 8] done.
[TimerTask 9] done.
[CalculateTask 9] done.

仔细观察:一开始只有两个服务器线程,因此线程状态都是忙,后来线程数增多,6个线程中的两个状态变成idle,说明处于wait()状态。

思考:本例的线程调度算法其实根本没有,因为这个应用是围绕TaskQueue设计的,不是以Thread Pool为中心设计的。因此,Task调度取决于TaskQueue的getTask()方法,你可以改进这个方法,例如使用优先队列,使优先级高的任务先被执行。

如果所有的服务器线程都处于busy状态,则说明任务繁忙,TaskQueue的队列越来越长,最终会导致服务器内存耗尽。因此,可以限制 TaskQueue的等待任务数,超过最大长度就拒绝处理。许多Web服务器在用户请求繁忙时就会拒绝用户:HTTP 503 SERVICE UNAVAILABLE

多线程读写同一个对象的数据是很普遍的,通常,要避免读写冲突,必须保证任何时候仅有一个线程在写入,有线程正在读取的时候,写入操作就必须等待。简单说,就是要避免“写-写”冲突和“读-写”冲突。但是同时读是允许的,因为“读-读”不冲突,而且很安全。

要实现以上的ReadWriteLock,简单的使用synchronized就不行,我们必须自己设计一个ReadWriteLock类,在读之前,必须先获得“读锁”,写之前,必须先获得“写锁”。举例说明:

DataHandler对象保存了一个可读写的char[]数组:

package com.crackj2ee.thread;

public class DataHandler {
    // store data:
    private char[] buffer = "AAAAAAAAAA".toCharArray();

    private char[] doRead() {
        char[] ret = new char[buffer.length];
        for(int i=0; i            ret[i] = buffer[i];
            sleep(3);
        }
        return ret;
    }

    private void doWrite(char[] data) {
        if(data!=null) {
            buffer = new char[data.length];
            for(int i=0; i                buffer[i] = data[i];
                sleep(10);
            }
        }
    }

    private void sleep(int ms) {
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException ie) {}
    }
}

doRead()和doWrite()方法是非线程安全的读写方法。为了演示,加入了sleep(),并设置读的速度大约是写的3倍,这符合通常的情况。

为了让多线程能安全读写,我们设计了一个ReadWriteLock:

package com.crackj2ee.thread;
public class ReadWriteLock {
    private int readingThreads = 0;
    private int writingThreads = 0;
    private int waitingThreads = 0; // waiting for write
    private boolean preferWrite = true;

    public synchronized void readLock() throws InterruptedException {
        while(writingThreads>0 || (preferWrite && waitingThreads>0))
            this.wait();
        readingThreads++;
    }

    public synchronized void readUnlock() {
        readingThreads--;
        preferWrite = true;
        notifyAll();
    }

    public synchronized void writeLock() throws InterruptedException {
        waitingThreads++;
        try {
            while(readingThreads>0 || writingThreads>0)
                this.wait();
        }
        finally {
            waitingThreads--;
        }
        writingThreads++;
    }

    public synchronized void writeUnlock() {
        writingThreads--;
        preferWrite = false;
        notifyAll();
    }
}

readLock()用于获得读锁,readUnlock()释放读锁,writeLock()和writeUnlock()一样。由于锁用完必须释放,因此,必须保证lock和unlock匹配。我们修改DataHandler,加入ReadWriteLock:

package com.crackj2ee.thread;
public class DataHandler {
    // store data:
    private char[] buffer = "AAAAAAAAAA".toCharArray();
    // lock:
    private ReadWriteLock lock = new ReadWriteLock();

    public char[] read(String name) throws InterruptedException {
        System.out.println(name + " waiting for read...");
        lock.readLock();
        try {
            char[] data = doRead();
            System.out.println(name + " reads data: " + new String(data));
            return data;
        }
        finally {
            lock.readUnlock();
        }
    }

    public void write(String name, char[] data) throws InterruptedException {
        System.out.println(name + " waiting for write...");
        lock.writeLock();
   &bsp;    try {
            System.out.println(name + " wrote data: " + new String(data));
            doWrite(data);
        }
        finally {
            lock.writeUnlock();
        }
    }

    private char[] doRead() {
        char[] ret = new char[buffer.length];
        for(int i=0; i            ret[i] = buffer[i];
            sleep(3);
        }
        return ret;
    }
    private void doWrite(char[] data) {
        if(data!=null) {
            buffer = new char[data.length];
            for(int i=0; i                buffer[i] = data[i];
                sleep(10);
            }
        }
    }
    private void sleep(int ms) {
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException ie) {}
    }
}

public方法read()和write()完全封装了底层的ReadWriteLock,因此,多线程可以安全地调用这两个方法:

// ReadingThread不断读取数据:
package com.crackj2ee.thread;
public class ReadingThread extends Thread {
    private DataHandler handler;
    public ReadingThread(DataHandler handler) {
        this.handler = handler;
    }
    public void run() {
        for(;;) {
            try {
                char[] data = handler.read(getName());
                Thread.sleep((long)(Math.random()*1000+100));
            }
            catch(InterruptedException ie) {
                break;
            }
        }
    }
}

// WritingThread不断写入数据,每次写入的都是10个相同的字符:
package com.crackj2ee.thread;
public class WritingThread extends Thread {
    private DataHandler handler;
    public WritingThread(DataHandler handler) {
        this.handler = handler;
    }
    public void run() {
        char[] data = new char[10];
        for(;;) {
            try {
                fill(data);
                handler.write(getName(), data);
                Thread.sleep((long)(Math.random()*1000+100));
            }
            catch(InterruptedException ie) {
                break;
            }
        }
    }
    // 产生一个A-Z随机字符,填入char[10]:
    private void fill(char[] data) {
        char c = (char)(Math.random()*26+'A');
        for(int i=0; i            data[i] = c;
    }
}

最后Main负责启动这些线程:

package com.crackj2ee.thread;
public class Main {
    public static void main(String[] args) {
        DataHandler handler = new DataHandler();
        Thread[] ts = new Thread[] {
                new ReadingThread(handler),
                new ReadingThread(handler),
                new ReadingThread(handler),
                new ReadingThread(handler),
                new ReadingThread(handler),
                new WritingThread(handler),
                new WritingThread(handler)
        };
        for(int i=0; i            ts[i].start();
        }
    }
}

我们启动了5个读线程和2个写线程,运行结果如下:

Thread-0 waiting for read...
Thread-1 waiting for read...
Thread-2 waiting for read...
Thread-3 waiting for read...
Thread-4 waiting for read...
Thread-5 waiting for write...
Thread-6 waiting for write...
Thread-4 reads data: AAAAAAAAAA
Thread-3 reads data: AAAAAAAAAA
Thread-2 reads data: AAAAAAAAAA
Thread-1 reads data: AAAAAAAAAA
Thread-0 reads data: AAAAAAAAAA
Thread-5 wrote data: EEEEEEEEEE
Thread-6 wrote data: MMMMMMMMMM
Thread-1 waiting for read...
Thread-4 waiting for read...
Thread-1 reads data: MMMMMMMMMM
Thread-4 reads data: MMMMMMMMMM
Thread-2 waiting for read...
Thread-2 reads data: MMMMMMMMMM
Thread-0 waiting for read...
Thread-0 reads data: MMMMMMMMMM
Thread-4 waiting for read...
Thread-4 reads data: MMMMMMMMMM
Thread-2 waiting for read...
Thread-5 waiting for write...
Thread-2 reads data: MMMMMMMMMM
Thread-5 wrote data: GGGGGGGGGG
Thread-6 waiting for write...
Thread-6 wrote data: AAAAAAAAAA
Thread-3 waiting for read...
Thread-3 reads data: AAAAAAAAAA
......

可以看到,每次读/写都是完整的原子操作,因为我们每次写入都是10个相同字符。并且,每次读出的都是最近一次写入的内容。

如果去掉ReadWriteLock:

package com.crackj2ee.thread;
public class DataHandler {

    // store data:
    private char[] buffer = "AAAAAAAAAA".toCharArray();

    public char[] read(String name) throws InterruptedException {
        char[] data = doRead();
        System.out.println(name + " reads data: " + new String(data));
        return data;
    }
    public void write(String name, char[] data) throws InterruptedException {
        System.out.println(name + " wrote data: " + new String(data));
        doWrite(data);
    }

    private char[] doRead() {
        char[] ret = new char[10];
        for(int i=0; i<10; i++) {
            ret[i] = buffer[i];
            sleep(3);
        }
        return ret;
    }
    private void doWrite(char[] data) {
        for(int i=0; i<10; i++) {
            buffer[i] = data[i];
            sleep(10);
        }
    }
    private void sleep(int ms) {
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException ie) {}
    }
}

运行结果如下:

Thread-5 wrote data: AAAAAAAAAA
Thread-6 wrote data: MMMMMMMMMM
Thread-0 reads data: AAAAAAAAAA
Thread-1 reads data: AAAAAAAAAA
Thread-2 reads data: AAAAAAAAAA
Thread-3 reads data: AAAAAAAAAA
Thread-4 reads data: AAAAAAAAAA
Thread-2 reads data: MAAAAAAAAA
Thread-3 reads data: MAAAAAAAAA
Thread-5 wrote data: CCCCCCCCCC
Thread-1 reads data: MAAAAAAAAA
Thread-0 reads data: MAAAAAAAAA
Thread-4 reads data: MAAAAAAAAA
Thread-6 wrote data: EEEEEEEEEE
Thread-3 reads data: EEEEECCCCC
Thread-4 reads data: EEEEEEEEEC
Thread-1 reads data: EEEEEEEEEE

可以看到在Thread-6写入EEEEEEEEEE的过程中,3个线程读取的内容是不同的。

思考

java的synchronized提供了最底层的物理锁,要在synchronized的基础上,实现自己的逻辑锁,就必须仔细设计ReadWriteLock。

Q: lock.readLock()为什么不放入try{ } 内?
A: 因为readLock()会抛出InterruptedException,导致readingThreads++不执行,而readUnlock()在 finally{ } 中,导致readingThreads--执行,从而使readingThread状态出错。writeLock()也是类似的。

Q: preferWrite有用吗?
A: 如果去掉preferWrite,线程安全不受影响。但是,如果读取线程很多,上一个线程还没有读取完,下一个线程又开始读了,就导致写入线程长时间无法获得writeLock;如果写入线程等待的很多,一个接一个写,也会导致读取线程长时间无法获得readLock。preferWrite的作用是让读 /写交替执行,避免由于读线程繁忙导致写无法进行和由于写线程繁忙导致读无法进行。

Q: notifyAll()换成notify()行不行?
A: 不可以。由于preferWrite的存在,如果一个线程刚读取完毕,此时preferWrite=true,再notify(),若恰好唤醒的是一个读线程,则while(writingThreads>0 || (preferWrite && waitingThreads>0))可能为true导致该读线程继续等待,而等待写入的线程也处于wait()中,结果所有线程都处于wait ()状态,谁也无法唤醒谁。因此,notifyAll()比notify()要来得安全。程序验证notify()带来的死锁:

Thread-0 waiting for read...
Thread-1 waiting for read...
Thread-2 waiting for read...
Thread-3 waiting for read...
Thread-4 waiting for read...
Thread-5 waiting for write...
Thread-6 waiting for write...
Thread-0 reads data: AAAAAAAAAA
Thread-4 reads data: AAAAAAAAAA
Thread-3 reads data: AAAAAAAAAA
Thread-2 reads data: AAAAAAAAAA
Thread-1 reads data: AAAAAAAAAA
Thread-5 wrote data: CCCCCCCCCC
Thread-2 waiting for read...
Thread-1 waiting for read...
Thread-3 waiting for read...
Thread-0 waiting for read...
Thread-4 waiting for read...
Thread-6 wrote data: LLLLLLLLLL
Thread-5 waiting for write...
Thread-6 waiting for write...
Thread-2 reads data: LLLLLLLLLL
Thread-2 waiting for read...
(运行到此不动了)

注意到这种死锁是由于所有线程都在等待别的线程唤醒自己,结果都无法醒过来。这和两个线程希望获得对方已有的锁造成死锁不同。因此多线程设计的难度远远高于单线程应用。

- 作者: 子枫 2005年12月12日, 星期一 10:11  回复(0) |  引用(2) 加入博采

HTML中TABLE锁定问题
摘要:JSP需要有各种各样TABLE锁定格式,我做了很多测试,用CSS实现了TABLE的HEAD部,FOOT部以及特定列的锁定,特定单元格的锁定,同时用javascript在TABLE上实现了TABLE上的方向键功能. 将表格锁定整理一下:点击表格中的第四列第二,三行画面锁定会发生变化. 查看全文

- 作者: 子枫 2005年09月7日, 星期三 10:33  回复(0) |  引用(2) 加入博采