SlideShare une entreprise Scribd logo
1  sur  28
Java 多线程编程                 -1-

线程的同步
线 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问
 程
题。Java 语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。
由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套
机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和
synchronized 块。
1. synchronized 方法:通过在方法声明中加入 synchronized 关键字来声明 synchronized 方
法。如:
public synchronized void accessVal(int newVal);
synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法
都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到
从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同
一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态
(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有
可能访问类成员变量的方法均被声明为 synchronized)。
在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为
synchronized ,以控制其对类的静态成员变量的访问。
synchronized 方法的缺陷:若将一个大的方法声明为 synchronized 将会大大影响效率,典型地,
若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此
将导致它对本类任何 synchronized 方法的调用都永远不会成功。         当然我们可以通过将访问类成员变量
的代码放到专门的方法中,将其声明为          synchronized ,并在主方法中调用来解决这一问题,但是
Java 为我们提供了更好的解决办法,那就是 synchronized 块。
2. synchronized 块:通过 synchronized 关键字来声明 synchronized 块。语法如下:
synchronized(syncObject) {
//允许访问控制的代码 }
}许synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以
是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,
故灵活性较高。
六:线程的阻塞
六 为了解决对共享存储区的访问冲突,Java 引入了同步机制,现在让我们来考察多个线程对共享资源
 :
的访问,显然同步机制已经不够了,因为在任意时刻所要求的资源不一定已经准备好了被访问,反过来,
同一时刻准备好了的资源也可能不止一个。为了解决这种情况下的访问控制问题,Java 引入了对阻塞机
制的支持。
制 阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一
 的
定已经很熟悉了。Java 提供了大量方法来支持阻塞,下面让我们逐一分析。
1. sleep() 方法:sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时
间内进入阻塞状态,不能得到 CPU 时间,指定的时间一过,线程重新进入可执行状态。
典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重
新测试,直到条件满足为止。
2. suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且
不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。典型地,
suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,
让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
3. yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处
于可执行状态,随时可能再次分得 CPU 时间。       调用 yield() 的效果等价于调度程序认为该线程已执行
了足够的时间从而转到另一个线程。
4. wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形
式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被
调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。
被 初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。
 调
Java 多线程编程              -2-

区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方
法则相反。上述的核心区别导致了一系列的细节上的区别。
法 首先,前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于 Object 类,也就是说,
 则
所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞
时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对
象上的锁被释放。     而调用 任意对象的 notify()方法则导致因调用该对象的 wait() 方法而阻塞的线程
中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
中 其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或
 随
块中调用,理由也很简单,只有在 synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同
样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方
法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的
对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现
IllegalMonitorStateException 异常。
异 wait() 和 notify() 方法的上述特性决定了它们经常和 synchronized 方法或块一起使用,将
 常
它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchronized 方法或块提供了
类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则相当于 block
和 wakeup 原语(这一对方法均声明为 synchronized)。 它们的结合使得我们可以实现操作系统上一
系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。
关于 wait() 和 notify() 方法最后再说明两点:
方 第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中
 法
随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产
生问题。
生 第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用
 问
notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,
只有获得锁的那一个线程才能进入可执行状态。
只 谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的
 有
wait() 方法的调用都可能产生死锁。    遗憾的是,Java 并不在语言级别上支持死锁的避免,我们在编程
中必须小心地避免死锁。
中 以上我们对 Java 中实现线程阻塞的各种方法作了一番分析,我们重点分析了 wait() 和
 必
notify() 方法,因为它们的功能最强大,使用也最灵活,但是这也导致了它们的效率较低,较容易出
错。实际使用中我们应该灵活使用各种方法,以便更好地达到我们的目的。
七:守护线程
七 守护线程是一类特殊的线程,它和普通线程的区别在于它并不是应用程序的核心部分,当一个应用程
 :
序的所有非守护线程终止运行时,即使仍然有守护线程在运行,应用程序也将终止,反之,只要有一个
非守护线程在运行,应用程序就不会终止。守护线程一般被用于在后台为其它线程提供服务。
可以通过调用方法 isDaemon() 来判断一个线程是否是守护线程,也可以调用方法 setDaemon() 来
将一个线程设为守护线程。
八:线程组
八 线程组是一个 Java 特有的概念,在 Java 中,线程组是类 ThreadGroup 的对象,每个线程都隶
 :
属于唯一一个线程组,这个线程组在线程创建时指定并在线程的整个生命期内都不能更改。你可以通过调
用包含 ThreadGroup 类型参数的 Thread 类构造函数来指定线程属的线程组,若没有指定,则线程
缺省地隶属于名为 system 的系统线程组。
的 在 Java 中,除了预建的系统线程组外,所有线程组都必须显式创建。 Java 中,除系统线程组外
 系                                        在
的每个线程组又隶属于另一个线程组,你可以在创建线程组时指定其所隶属的线程组,若没有指定,则
缺省地隶属于系统线程组。这样,所有线程组组成了一棵以系统线程组为根的树。
缺 Java 允许我们对一个线程组中的所有线程同时进行操作,比如我们可以通过调用线程组的相应方法
 省
来设置其中所有线程的优先级,也可以启动或阻塞其中的所有线程。
Java 的线程组机制的另一个重要作用是线程安全。线程组机制允许我们通过分组来区分有不同安全特性
的线程,对不同组的线程进行不同的处理,还可以通过线程组的分层结构来支持不对等安全措施的采用 。
Java 的 ThreadGroup 类提供了大量的方法来方便我们对线程组树中的每一个线程组以及线程组中的
Java 多线程编程                  -3-

每一个线程进行操作。
九:总结
九 在这一讲中,我们一起学习了 Java 多线程编程的方方面面,包括创建线程,以及对多个线程进行调
 :
度、管理。我们深刻认识到了多线程编程的复杂性,以及线程切换开销带来的多线程程序的低效性,这也
促使我们认真地思考一个问题:我们是否需要多线程?何时需要多线程?
促 多线程的核心在于多个代码块并发执行,本质特点在于各代码块之间的代码是乱序执行的。
 使                                                            我们的程序
是否需要多线程,就是要看这是否也是它的内在特点。
是 假如我们的程序根本不要求多个代码块并发执行,那自然不需要使用多线程;假如我们的程序虽然要
 否
求多个代码块并发执行,但是却不要求乱序,则我们完全可以用一个循环来简单高效地实现,也不需要
使用多线程;只有当它完全符合多线程的特点时,多线程机制对线程间通信和线程管理的强大支持才能
有用武之地,这时使用多线程才是值得的。
  线程的(同步)控制
  一个 Java 程序的多线程之间可以共享数据。当线程以异步方式访问共享数据时,有时候是不安全的
或者不和逻辑的。比如,同一时刻一个线程在读取数据,另外一个线程在处理数据,当处理数据的线程没
有等到读取数据的线程读取完毕就去处理数据,必然得到错误的处理结果。这和我们前面提到的读取数据
和处理数据并行多任务并不矛盾,这儿指的是处理数据的线程不能处理当前还没有读取结束的数据,但
是可以处理其它的数据。
  如果我们采用多线程同步控制机制,等到第一个线程读取完数据,第二个线程才能处理该数据,就
会避免错误。可见,线程同步是多线程编程的一个相当重要的技术。
  在讲线程的同步控制前我们需要交代如下概念:
  1 用 Java 关键字 synchonized 同步对共享数据操作的方法
  在一个对象中,用 synchonized 声明的方法为同步方法。         Java 中有一个同步模型-监视器,负责管
理线程对对象中的同步方法的访问,它的原理是:赋予该对象唯一一把'钥匙',当多个线程进入对象,
只有取得该对象钥匙的线程才可以访问同步方法,其它线程在该对象中等待,直到该线程用 wait()方法
放弃这把钥匙,其它等待的线程抢占该钥匙,抢占到钥匙的线程后才可得以执行,而没有取得钥匙的线
程仍被阻塞在该对象中等待。
  file://声明同步的一种方式:将方法声明同步
  class store
   {  public synchonized void store_in()
    {    ….    }
    public synchonized void store_out(){ ….}
    }
  2 利用 wait()、notify()及 notifyAll()方法发送消息实现线程间的相互联系
  Java 程序中多个线程通过消息来实现互动联系的,这几种方法实现了线程间的消息发送。例如定义
一个对象的 synchonized 方法,同一时刻只能够有一个线程访问该对象中的同步方法,其它线程被阻
塞。 通常可以用 notify()或 notifyAll()方法唤醒其它一个或所有线程。       而使用 wait()方法来使该线
程处于阻塞状态,等待其它的线程用 notify()唤醒。
  一个实际的例子就是生产和销售,生产单元将产品生产出来放在仓库中,销售单元则从仓库中提走
产品,在这个过程中,销售单元必须在仓库中有产品时才能提货;如果仓库中没有产品,则销售单元必
须等待。
  程序中,假如我们定义一个仓库类 store,该类的实例对象就相当于仓库,在 store 类中定义两个
成员方法:store_in(),用来模拟产品制造者往仓库中添加产品;strore_out()方法则用来模拟销
售者从仓库中取走产品。然后定义两个线程类:customer 类,其中的 run()方法通过调用仓库类中的
store_out()从仓库中取走产品,模拟销售者;另外一个线程类 producer 中的 run()方法通过调用
仓库类中的 store_in()方法向仓库添加产品,模拟产品制造者。     在主类中创建并启动线程,实现向仓库
中添加产品或取走产品。
  如果仓库类中的 store_in() 和 store_out()方法不声明同步,这就是个一般的多线程,我们知
道,一个程序中的多线程是交替执行的,运行也是无序的,这样,就可能存在这样的问题:
  仓库中没有产品了,销售者还在不断光顾,而且还不停的在'取'产品,这在现实中是不可思义的,
在程序中就表现为负值;如果将仓库类中的 stroe_in()和 store_out()方法声明同步,如上例所示:
Java 多线程编程                 -4-

就控制了同一时刻只能有一个线程访问仓库对象中的同步方法;即一个生产类线程访问被声明为同步的
store_in()方法时,其它线程将不能够访问对象中的 store_out()同步方法,当然也不能访问
store_in()方法。必须等到该线程调用 wait()方法放弃钥匙,其它线程才有机会访问同步方法。
  这个原理实际中也很好理解,当生产者(producer)取得仓库唯一的钥匙,就向仓库中添放产品,
此时其它的销售者(customer,可以是一个或多个)不可能取得钥匙,只有当生产者添放产品结束,交
还钥匙并且通知销售者,不同的销售者根据取得钥匙的先后与否决定是否可以进入仓库中提走产品。

轻松使用线程: 不共享有时是最好的
利用 ThreadLocal 提高可伸缩性
ThreadLocal 类是悄悄地出现在 Java 平台版本 1.2 中的。虽然支持线程局部变量早就是许多线程
工具(例如 Posix pthreads 工具)的一部分,但 Java Threads API 的最初设计却没有这项有
用的功能。而且,最初的实现也相当低效。由于这些原因,ThreadLocal 极少受到关注,但对简化线程
安全并发程序的开发来说,它却是很方便的。在轻松使用线程的第 3 部分,Java 软件顾问 Brian
Goetz 研究了 ThreadLocal 并提供了一些使用技巧。
参加 Brian 的多线程 Java 编程讨论论坛以获得您工程中的线程和并发问题的帮助。
编写线程安全类是困难的。它不但要求仔细分析在什么条件可以对变量进行读写,而且要求仔细分析其它
类能如何使用某个类。有时,要在不影响类的功能、易用性或性能的情况下使类成为线程安全的是很困难
的。有些类保留从一个方法调用到下一个方法调用的状态信息,要在实践中使这样的类成为线程安全的是
困难的。
管理非线程安全类的使用比试图使类成为线程安全的要更容易些。非线程安全类通常可以安全地在多线程
程序中使用,只要您能确保一个线程所用的类的实例不被其它线程使用。例如,JDBC Connection 类
是非线程安全的 ― 两个线程不能在小粒度级上安全地共享一个 Connection ― 但如果每个线程都有
它自己的 Connection,那么多个线程就可以同时安全地进行数据库操作。
不使用 ThreadLocal 为每个线程维护一个单独的 JDBC 连接(或任何其它对象)当然是可能的;
Thread API 给了我们把对象和线程联系起来所需的所有工具。 ThreadLocal 则使我们能更容易地
                                       而
把线程和它的每线程(per-thread)数据成功地联系起来。
什么是线程局部变量(thread-local variable)?
线程局部变量高效地为每个使用它的线程提供单独的线程局部变量值的副本。每个线程只能看到与自己相
联系的值,而不知道别的线程可能正在使用或修改它们自己的副本。一些编译器(例如 Microsoft
Visual C++ 编译器或 IBM XL FORTRAN 编译器)用存储类别修饰符(像 static 或
volatile)把对线程局部变量的支持集成到了其语言中。Java 编译器对线程局部变量不提供特别的语
言支持;相反地,它用 ThreadLocal 类实现这些支持,核心 Thread 类中有这个类的特别支持。
因为线程局部变量是通过一个类来实现的,而不是作为 Java 语言本身的一部分,所以 Java 语言线
程局部变量的使用语法比内建线程局部变量语言的使用语法要笨拙一些。要创建一个线程局部变量,请实
例化类 ThreadLocal 的一个对象。 ThreadLocal 类的行为与 java.lang.ref 中的各种
Reference 类的行为很相似;ThreadLocal 类充当存储或检索一个值时的间接句柄。清单 1 显示了
ThreadLocal 接口。
清单 1. ThreadLocal 接口
public class ThreadLocal {
  public Object get();
  public void set(Object newValue);
  public Object initialValue();
}
get() 访问器检索变量的当前线程的值;set() 访问器修改当前线程的值。         initialValue() 方法是
可选的,如果线程未使用过某个变量,那么您可以用这个方法来设置这个变量的初始值;它允许延迟初
始化。用一个示例实现来说明 ThreadLocal 的工作方式是最好的方法。清单 2 显示了 ThreadLocal
的一个实现方式。它不是一个特别好的实现(虽然它与最初实现非常相似),所以很可能性能不佳,但它
清楚地说明了 ThreadLocal 的工作方式。
清单 2. ThreadLocal 的糟糕实现
public class ThreadLocal {
Java 多线程编程                     -5-
    private Map values = Collections.synchronizedMap(new HashMap());
    public Object get() {
      Thread curThread = Thread.currentThread();
      Object o = values.get(curThread);
      if (o == null && !values.containsKey(curThread)) {
        o = initialValue();
        values.put(curThread, o);
      }
      return o;
    }

    public void set(Object newValue) {
      values.put(Thread.currentThread(), newValue);
    }

    public Object initialValue() {
      return null;
    }
}
这个实现的性能不会很好,因为每个 get() 和 set() 操作都需要 values 映射表上的同步,而且
如果多个线程同时访问同一个 ThreadLocal,那么将发生争用。 此外,这个实现也是不切实际的,因为
用 Thread 对象做 values 映射表中的关键字将导致无法在线程退出后对 Thread 进行垃圾回收,
而且也无法对死线程的 ThreadLocal 的特定于线程的值进行垃圾回收。
用 ThreadLocal 实现每线程 Singleton
线程局部变量常被用来描绘有状态“单子”(Singleton) 或线程安全的共享对象,或者是通过把不
安全的整个变量封装进 ThreadLocal,或者是通过把对象的特定于线程的状态封装进 ThreadLocal。
例如,在与数据库有紧密联系的应用程序中,程序的很多方法可能都需要访问数据库。在系统的每个方法
中都包含一个 Connection 作为参数是不方便的 ― 用“单子”来访问连接可能是一个虽然更粗糙,
但却方便得多的技术。然而,多个线程不能安全地共享一个 JDBC Connection。如清单 3 所示,通过
使用“单子”中的 ThreadLocal,我们就能让我们的程序中的任何类容易地获取每线程 Connection
的一个引用。这样,我们可以认为 ThreadLocal 允许我们创建每线程单子。
清单 3. 把一个 JDBC 连接存储到一个每线程 Singleton 中
public class ConnectionDispenser {
  private static class ThreadLocalConnection extends ThreadLocal {
    public Object initialValue() {
      return DriverManager.getConnection(ConfigurationSingleton.getDbUrl());
    }
  }
  private ThreadLocalConnection conn = new ThreadLocalConnection();
  public static Connection getConnection() {
    return (Connection) conn.get();
  }
}
任何创建的花费比使用的花费相对昂贵些的有状态或非线程安全的对象,例如 JDBC Connection 或
正则表达式匹配器,都是可以使用每线程单子(singleton)技术的好地方。当然,在类似这样的地方,
您可以使用其它技术,例如用池,来安全地管理共享访问。然而,从可伸缩性角度看,即使是用池也存在
一些潜在缺陷。因为池实现必须使用同步,以维护池数据结构的完整性,如果所有线程使用同一个池,那
么在有很多线程频繁地对池进行访问的系统中,程序性能将因争用而降低。
用 ThreadLocal 简化调试日志纪录
其它适合使用 ThreadLocal 但用池却不能成为很好的替代技术的应用程序包括存储或累积每线程上下
Java 多线程编程                   -6-

文信息以备稍后检索之用这样的应用程序。例如,假设您想创建一个用于管理多线程应用程序调试信息的
工具。  您可以用如清单 4 所示的 DebugLogger 类作为线程局部容器来累积调试信息。             在一个工作单元
的开头,您清空容器,而当一个错误出现时,您查询该容器以检索这个工作单元迄今为止生成的所有调
试信息。
清单 4. 用 ThreadLocal 管理每线程调试日志
public class DebugLogger {
  private static class ThreadLocalList extends ThreadLocal {
    public Object initialValue() {
      return new ArrayList();
    }
    public List getList() {
      return (List) super.get();
    }
  }
  private ThreadLocalList list = new ThreadLocalList();
  private static String[] stringArray = new String[0];
  public void clear() {
    list.getList().clear();
  }
  public void put(String text) {
    list.getList().add(text);
  }
  public String[] get() {
    return list.getList().toArray(stringArray);
  }
}
在您的代码中,您可以调用 DebugLogger.put() 来保存您的程序正在做什么的信息,而且,稍后如
果有必要(例如发生了一个错误),您能够容易地检索与某个特定线程相关的调试信息。 与简单地把所
有信息转储到一个日志文件,然后努力找出哪个日志记录来自哪个线程(还要担心线程争用日志纪录对
象)相比,这种技术简便得多,也有效得多。
ThreadLocal 在基于 servlet 的应用程序或工作单元是一个整体请求的任何多线程应用程序服务器
中也是很有用的,因为在处理请求的整个过程中将要用到单个线程。您可以通过前面讲述的每线程单子技
术用 ThreadLocal 变量来存储各种每请求(per-request)上下文信息。
ThreadLocal 的线程安全性稍差的堂兄弟,InheritableThreadLocal
ThreadLocal 类有一个亲戚,InheritableThreadLocal,它以相似的方式工作,但适用于种类完
全不同的应用程序。    创建一个线程时如果保存了所有 InheritableThreadLocal 对象的值,那么这些
值也将自动传递给子线程。如果一个子线程调用 InheritableThreadLocal 的 get(),那么它将与
它的父线程看到同一个对象。为保护线程安全性,您应该只对不可变对象(一旦创建,其状态就永远不会
被改变的对象)使用 InheritableThreadLocal,因为对象被多个线程共享 。
InheritableThreadLocal 很合适用于把数据从父线程传到子线程,例如用户标识(user id)或
事务标识(transaction id),但不能是有状态对象,例如 JDBC Connection。
ThreadLocal 的性能
虽然线程局部变量早已赫赫有名并被包括 Posix pthreads 规范在内的很多线程框架支持,但最初的
Java 线程设计中却省略了它,只是在 Java 平台的版本 1.2 中才添加上去。在很多方面,
ThreadLocal 仍在发展之中;在版本 1.3 中它被重写,版本 1.4 中又重写了一次,两次都专门是
为了性能问题。

在 JDK 1.2 中,ThreadLocal 的实现方式与清单 2 中的方式非常相似,除了用同步
WeakHashMap 代替 HashMap 来存储 values 之外。(以一些额外的性能开销为代价,使用
WeakHashMap 解决了无法对 Thread 对象进行垃圾回收的问题。   )不用说,ThreadLocal 的性能是
Java 多线程编程                   -7-

相当差的。
Java 平台版本 1.3 提供的 ThreadLocal 版本已经尽量更好了;它不使用任何同步,从而不存在可
伸缩性问题,而且它也不使用弱引用。相反地,人们通过给 Thread 添加一个实例变量(该变量用于保
存当前线程的从线程局部变量到它的值的映射的 HashMap)来修改 Thread 类以支持
ThreadLocal。因为检索或设置一个线程局部变量的过程不涉及对可能被另一个线程读写的数据的读写
操作,所以您可以不用任何同步就实现 ThreadLocal.get() 和 set()。而且,因为每线程值的引用
被存储在自已的 Thread 对象中,所以当对 Thread 进行垃圾回收时,也能对该 Thread 的每线程
值进行垃圾回收。
不幸的是,即使有了这些改进,Java 1.3 中的 ThreadLocal 的性能仍然出奇地慢。          据我的粗略测量,
在双处理器    Linux 系统上的 Sun 1.3 JDK 中进行 ThreadLocal.get() 操作,所耗费的时间大
约是无争用同步的两倍。性能这么差的原因是 Thread.currentThread() 方法的花费非常大,占了
ThreadLocal.get() 运行时间的三分之二还多。虽然有这些缺点,JDK 1.3 ThreadLocal.get()
仍然比争用同步快得多,所以如果在任何存在严重争用的地方(可能是有非常多的线程,或者同步块被
频繁地执行,或者同步块很大),ThreadLocal 可能仍然要高效得多。
在 Java 平台的最新版本,即版本 1.4b2 中,ThreadLocal 和 Thread.currentThread() 的
性能都有了很大提高。有了这些提高,ThreadLocal 应该比其它技术,如用池,更快。由于它比其它技
术更简单,也更不易出错,人们最终将发现它是避免线程间出现不希望的交互的有效途径。
ThreadLocal 的好处
ThreadLocal 能带来很多好处。  它常常是把有状态类描绘成线程安全的,或者封装非线程安全类以使它
们能够在多线程环境中安全地使用的最容易的方式。使用 ThreadLocal 使我们可以绕过为实现线程安
全而对何时需要同步进行判断的复杂过程,而且因为它不需要任何同步,所以也改善了可伸缩性。除简单
之外,用 ThreadLocal 存储每线程单子或每线程上下文信息在归档方面还有一个颇有价值好处 ― 通
过使用 ThreadLocal,存储在 ThreadLocal 中的对象都是不被线程共享的是清晰的,从而简化了判
断一个类是否线程安全的工作。
我希望您从这个系列中得到了乐趣,也学到了知识,我也鼓励您到我的讨论论坛中来深入研究多线程问
题。

Java 多线程学习笔记
一、线程类
  Java 是通过 Java.lang.Thread 类来实现多线程的,第个 Thread 对象描述了一个单独的线程。
要产生一个线程,有两种方法:
     1、需要从 Java.lang.Thread 类继承一个新的线程类,重载它的 run()方法;
     2、通过 Runnalbe 接口实现一个从非线程类继承来类的多线程,重载 Runnalbe 接口的 run()方
法。运行一个新的线程,只需要调用它的 start()方法即可。如:
/**=====================================================================
* 文件:ThreadDemo_01.java
* 描述:产生一个新的线程
* ======================================================================
*/
class ThreadDemo extends Thread{
   Threads()
   {
   }

 Threads(String szName)
 {
   super(szName);
 }

 // 重载 run 函数
Java 多线程编程                     -8-
    public void run()
    {
       for (int count = 1,row = 1; row < 20; row++,count++)
       {
          for (int i = 0; i < count; i++)
          {
             System.out.print('*');
          }
          System.out.println();
       }
    }
}

class ThreadMain{
  public static void main(String argv[]){
    ThreadDemo th = new ThreadDemo();
    // 调用 start()方法执行一个新的线程
    th.start();
  }
}


  线程类的一些常用方法:

  sleep(): 强迫一个线程睡眠N毫秒。
  isAlive(): 判断一个线程是否存活。
  join(): 等待线程终止。
  activeCount(): 程序中活跃的线程数。
  enumerate(): 枚举程序中的线程。
     currentThread(): 得到当前线程。
  isDaemon(): 一个线程是否为守护线程。
  setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依
赖于主线程结束而结束)
  setName(): 为线程设置一个名称。
  wait(): 强迫一个线程等待。
  notify(): 通知一个线程继续运行。
  setPriority(): 设置一个线程的优先级。
二、等待一个线程的结束
  有些时候我们需要等待一个线程终止后再运行我们的另一个线程,这时我们应该怎么办呢?请看下
面的例子:
/**=====================================================================
* 文件:ThreadDemo_02.java
* 描述:等待一个线程的结束
* ======================================================================
*/
class ThreadDemo extends Thread{
   Threads()
   {
   }

    Threads(String szName)
Java 多线程编程                   -9-
    {
        super(szName);
    }

    // 重载 run 函数
    public void run()
    {
       for (int count = 1,row = 1; row < 20; row++,count++)
       {
          for (int i = 0; i < count; i++)
          {
             System.out.print('*');
          }
          System.out.println();
       }
    }
}

class ThreadMain{
  public static void main(String argv[]){
    //产生两个同样的线程
    ThreadDemo th1 = new ThreadDemo();
    ThreadDemo th2 = new ThreadDemo();

     // 我们的目的是先运行第一个线程,再运行第二个线程
     th1.start();
     th2.start();
    }
}
这里我们的目标是要先运行第一个线程,等第一个线程终止后再运行第二个线程,而实际运行的结果是
如何的呢?实际上我们运行的结果并不是两个我们想要的直角三角形,而是一些乱七八糟的*号行,有
的长,有的短。为什么会这样呢?因为线程并没有按照我们的调用顺序来执行,而是产生了线程赛跑现象。
实际上 Java 并不能按我们的调用顺序来执行线程,这也说明了线程是并行执行的单独代码。如果要想得
到我们预期的结果,这里我们就需要判断第一个线程是否已经终止,如果已经终止,再来调用第二个线
程。代码如下:
/**=====================================================================
* 文件:ThreadDemo_03.java
* 描述:等待一个线程的结束的两种方法
* ======================================================================
*/
class ThreadDemo extends Thread{
   Threads()
   {
   }

    Threads(String szName)
    {
      super(szName);
    }
Java 多线程编程                      - 10 -
    // 重载 run 函数
    public void run()
    {
       for (int count = 1,row = 1; row < 20; row++,count++)
       {
          for (int i = 0; i < count; i++)
          {
             System.out.print('*');
          }
          System.out.println();
       }
    }
}

class ThreadMain{
  public static void main(String argv[]){
    ThreadMain test = new ThreadMain();
    test.Method1();
    // test.Method2();
  }

 // 第一种方法:不断查询第一个线程是否已经终止,如果没有,则让主线程睡眠一直到它终止为止
 // 即:while/isAlive/sleep
 public void Method1(){
   ThreadDemo th1 = new ThreadDemo();
   ThreadDemo th2 = new ThreadDemo();
   // 执行第一个线程
   th1.start();
   // 不断查询第一个线程的状态
   while(th1.isAlive()){
     try{
        Thread.sleep(100);
     }catch(InterruptedException e){
     }
   }
   //第一个线程终止,运行第二个线程
   th2.start();
 }

    // 第二种方法:join()
    public void Method2(){
      ThreadDemo th1 = new ThreadDemo();
      ThreadDemo th2 = new ThreadDemo();
      // 执行第一个线程
      th1.start();
      try{
        th1.join();
      }catch(InterruptedException e){
      }
      // 执行第二个线程
Java 多线程编程                      - 11 -

  th2.start();
}
三、线程的同步问题
     有些时候,我们需要很多个线程共享一段代码,比如一个私有成员或一个类中的静态成员,但是由于
线程赛跑的问题,所以我们得到的常常不是正确的输出结果,而相反常常是张冠李戴,与我们预期的结
果大不一样。看下面的例子:
/
**==========================================================================
===
  * 文件:ThreadDemo_04.java
  * 描述:多线程不同步的原因
  *
============================================================================
=
  */
// 共享一个静态数据对象
class ShareData{
  public static String szData = "";
}

class ThreadDemo extends Thread{

 private ShareData oShare;

 ThreadDemo(){
 }

 ThreadDemo(String szName,ShareData oShare){
   super(szName);
   this.oShare = oShare;
 }

 public void run(){
   // 为了更清楚地看到不正确的结果,这里放一个大的循环
  for (int i = 0; i < 50; i++){
      if (this.getName().equals("Thread1")){
        oShare.szData = "这是第 1 个线程";
        // 为了演示产生的问题,这里设置一次睡眠
       try{
          Thread.sleep((int)Math.random() * 100);
        catch(InterruptedException e){
        }
        // 输出结果
        System.out.println(this.getName() + ":" + oShare.szData);
      }else if (this.getName().equals("Thread2")){
        oShare.szData = "这是第 1 个线程";
        // 为了演示产生的问题,这里设置一次睡眠
     try{
          Thread.sleep((int)Math.random() * 100);
        catch(InterruptedException e){
Java 多线程编程                      - 12 -
              }
              // 输出结果
              System.out.println(this.getName() + ":" + oShare.szData);
          }
     }
}

class ThreadMain{
  public static void     main(String argv[]){
    ShareData oShare     = new ShareData();
    ThreadDemo th1 =     new ThreadDemo("Thread1",oShare);
    ThreadDemo th2 =     new ThreadDemo("Thread2",oShare);

        th1.start();
        th2.start();
    }
}
  由于线程的赛跑问题,所以输出的结果往往是 Thread1 对应“这是第 2 个线程”,这样与我们要
输出的结果是不同的。   为了解决这种问题(错误),Java 为我们提供了“锁”的机制来实现线程的同步。
锁的机制要求每个线程在进入共享代码之前都要取得锁,否则不能进入,而退出共享代码之前则释放该
锁,这样就防止了几个或多个线程竞争共享代码的情况,从而解决了线程的不同步的问题。可以这样说,
在运行共享代码时则是最多只有一个线程进入,也就是和我们说的垄断。锁机制的实现方法,则是在共享
代码之前加入 synchronized 段,把共享代码包含在 synchronized 段中。上述问题的解决方法为:

/
**==========================================================================
===
  * 文件:ThreadDemo_05.java
  * 描述:多线程不同步的解决方法--锁
  *
============================================================================
=
  */
// 共享一个静态数据对象
class ShareData{
  public static String szData = "";
}

class ThreadDemo extends Thread{

    private ShareData oShare;

    ThreadDemo(){
    }

    ThreadDemo(String szName,ShareData oShare){
      super(szName);
      this.oShare = oShare;
    }
Java 多线程编程                     - 13 -
  public void run(){
     // 为了更清楚地看到不正确的结果,这里放一个大的循环
  for (int i = 0; i < 50; i++){
        if (this.getName().equals("Thread1")){
          // 锁定 oShare 共享对象
          synchronized (oShare){
            oShare.szData = "这是第 1 个线程";
            // 为了演示产生的问题,这里设置一次睡眠
          try{
              Thread.sleep((int)Math.random() * 100);
            catch(InterruptedException e){
            }
            // 输出结果
            System.out.println(this.getName() + ":" + oShare.szData);
          }
        }else if (this.getName().equals("Thread2")){
          // 锁定共享对象
          synchronized (oShare){
            oShare.szData = "这是第 1 个线程";
            // 为了演示产生的问题,这里设置一次睡眠
      try{
              Thread.sleep((int)Math.random() * 100);
            catch(InterruptedException e){
            }
            // 输出结果
            System.out.println(this.getName() + ":" + oShare.szData);
          }
        }
   }
}

class ThreadMain{
  public static void   main(String argv[]){
    ShareData oShare   = new ShareData();
    ThreadDemo th1 =   new ThreadDemo("Thread1",oShare);
    ThreadDemo th2 =   new ThreadDemo("Thread2",oShare);

        th1.start();
        th2.start();
    }
}
  由于过多的 synchronized 段将会影响程序的运行效率,因此引入了同步方法,同步方法的实现则
是将共享代码单独写在一个方法里,在方法前加上 synchronized 关键字即可。

  在线程同步时的两个需要注意的问题:
  1、无同步问题:即由于两个或多个线程在进入共享代码前,得到了不同的锁而都进入共享代码而造
成。
  2、死锁问题:即由于两个或多个线程都无法得到相应的锁而造成的两个线程都等待的现象。这种现
象主要是因为相互嵌套的 synchronized 代码段而造成,因此,在程序中尽可能少用嵌套的
synchronized 代码段是防止线程死锁的好方法。
Java 多线程编程                       - 14 -


  在写上面的代码遇到的一个没能解决的问题,在这里拿出来,希望大家讨论是什么原因。

/
**==========================================================================
===
  * 文件:ThreadDemo_0 6.java
  * 描述:为什么造成线程的不同步。
  *
============================================================================
=
  */
class ThreadDemo extends Thread{
    //共享一个静态数据成员
    private static String szShareData = "";

  ThreadDemo(){
  }

  ThreadDemo(String szName){
    super(szName);
  }

 public void run(){
    // 为了更清楚地看到不正确的结果,这里放一个大的循环
  for (int i = 0; i < 50; i++){
       if (this.getName().equals("Thread1")){
     synchronized(szShareData){
             szShareData = "这是第 1 个线程";
             // 为了演示产生的问题,这里设置一次睡眠
           try{
               Thread.sleep((int)Math.random() * 100);
             catch(InterruptedException e){
             }
             // 输出结果
             System.out.println(this.getName() + ":" + szShareData);
         }
       }else if (this.getName().equals("Thread2")){
           synchronized(szShareData){
             szShareData = "这是第 1 个线程";
             // 为了演示产生的问题,这里设置一次睡眠
      try{
               Thread.sleep((int)Math.random() * 100);
             catch(InterruptedException e){
             }
             // 输出结果
             System.out.println(this.getName() + ":" + szShareData);
           }
       }
  }
Java 多线程编程                   - 15 -
}

class ThreadMain{
  public static void main(String argv[]){
    ThreadDemo th1 = new ThreadDemo("Thread1");
    ThreadDemo th2 = new ThreadDemo("Thread2");

        th1.start();
        th2.start();
    }
}


  这段代码的共享成员是一个类中的静态成员,按理说,这里进入共享代码时得到的锁应该是同样的
锁,而实际上以上程序的输入却是不同步的,为什么呢??

Java 多线程学习笔记(二)
四、Java 的等待通知机制
  在有些时候,我们需要在几个或多个线程中按照一定的秩序来共享一定的资源。例如生产者--消费
者的关系,在这一对关系中实际情况总是先有生产者生产了产品后,消费者才有可能消费;又如在父-
-子关系中,总是先有父亲,然后才能有儿子。然而在没有引入等待通知机制前,我们得到的情况却常常
是错误的。这里我引入《用线程获得强大的功能》一文中的生产者--消费者的例子:
/*
============================================================================
======
 * 文件:ThreadDemo07.java
 * 描述:生产者--消费者
 * 注:其中的一些注释是我根据自己的理解加注的
 *
============================================================================
======
 */

// 共享的数据对象
 class ShareData{
  private char c;

    public void setShareChar(char c){
      this.c = c;
    }

    public char getShareChar(){
      return this.c;
    }
 }

 // 生产者线程
 class Producer extends Thread{

    private ShareData s;
Java 多线程编程                     - 16 -
  Producer(ShareData s){
    this.s = s;
  }

  public void run(){
   for (char ch = 'A'; ch <= 'Z'; ch++){
    try{
     Thread.sleep((int)Math.random() * 4000);
    }catch(InterruptedException e){}

      // 生产
      s.setShareChar(ch);
      System.out.println(ch + " producer by producer.");
     }
  }
 }

 // 消费者线程
 class Consumer extends Thread{

  private ShareData s;

  Consumer(ShareData s){
    this.s = s;
  }

  public void run(){
   char ch;

     do{
      try{
       Thread.sleep((int)Math.random() * 4000);
      }catch(InterruptedException e){}
      // 消费
      ch = s.getShareChar();
      System.out.println(ch + " consumer by consumer.");
     }while(ch != 'Z');
  }
 }

class Test{
  public static void main(String argv[]){
    ShareData s = new ShareData();
    new Consumer(s).start();
    new Producer(s).start();
  }
}


  在以上的程序中,模拟了生产者和消费者的关系,生产者在一个循环中不断生产了从A-Z的共享
数据,而消费者则不断地消费生产者生产的A-Z的共享数据。我们开始已经说过,在这一对关系中,必
Java 多线程编程                      - 17 -

须先有生产者生产,才能有消费者消费。但如果运行我们上面这个程序,结果却出现了在生产者没有生产
之前,消费都就已经开始消费了或者是生产者生产了却未能被消费者消费这种反常现象。为了解决这一问
题,引入了等待通知(wait/notify)机制如下:
  1、在生产者没有生产之前,通知消费者等待;在生产者生产之后,马上通知消费者消费。
  2、在消费者消费了之后,通知生产者已经消费完,需要生产。
下面修改以上的例子(源自《用线程获得强大的功能》一文):

/*
============================================================================
======
 * 文件:ThreadDemo08.java
 * 描述:生产者--消费者
 * 注:其中的一些注释是我根据自己的理解加注的
 *
============================================================================
======
 */

class ShareData{

private char c;
// 通知变量
private boolean writeable = true;

 //
-------------------------------------------------------------------------
 // 需要注意的是:在调用 wait()方法时,需要把它放到一个同步段里,否则将会出现
 // "java.lang.IllegalMonitorStateException: current thread not owner"的异常。
 //
-------------------------------------------------------------------------
 public synchronized void setShareChar(char c){
  if (!writeable){
    try{
     // 未消费等待
     wait();
    }catch(InterruptedException e){}
  }

 this.c = c;
 // 标记已经生产
 writeable = false;
 // 通知消费者已经生产,可以消费
 notify();
}

public synchronized char getShareChar(){
 if (writeable){
  try{
   // 未生产等待
   wait();
Java 多线程编程                    - 18 -
     }catch(InterruptedException e){}
    }
    // 标记已经消费
    writeable = true;
    // 通知需要生产
    notify();
    return this.c;
 }
}

// 生产者线程
class Producer extends Thread{

 private ShareData s;

 Producer(ShareData s){
   this.s = s;
 }

 public void run(){
  for (char ch = 'A'; ch <= 'Z'; ch++){
   try{
    Thread.sleep((int)Math.random() * 400);
   }catch(InterruptedException e){}

     s.setShareChar(ch);
     System.out.println(ch + " producer by producer.");
    }
 }
}

// 消费者线程
class Consumer extends Thread{

 private ShareData s;

 Consumer(ShareData s){
   this.s = s;
 }

 public void run(){
  char ch;

    do{
     try{
      Thread.sleep((int)Math.random() * 400);
     }catch(InterruptedException e){}

     ch = s.getShareChar();
     System.out.println(ch + " consumer by consumer.**");
Java 多线程编程                        - 19 -
    }while (ch != 'Z');
 }
}

class Test{
  public static void main(String argv[]){
    ShareData s = new ShareData();
    new Consumer(s).start();
    new Producer(s).start();
  }
}


  在以上程序中,设置了一个通知变量,每次在生产者生产和消费者消费之前,都测试通知变量,检
查是否可以生产或消费。最开始设置通知变量为 true,表示还未生产,在这时候,消费者需要消费,于
时修改了通知变量,调用 notify()发出通知。这时由于生产者得到通知,生产出第一个产品,修改通知
变量,向消费者发出通知。 这时如果生产者想要继续生产,但因为检测到通知变量为 false,得知消费者
还没有生产,所以调用 wait()进入等待状态。因此,最后的结果,是生产者每生产一个,就通知消费者
消费一个;消费者每消费一个,就通知生产者生产一个,所以不会出现未生产就消费或生产过剩的情况。

五、线程的中断
  在很多时候,我们需要在一个线程中调控另一个线程,这时我们就要用到线程的中断。用最简单的话
也许可以说它就相当于播放机中的暂停一样,当第一次按下暂停时,播放器停止播放,再一次按下暂停
时,继续从刚才暂停的地方开始重新播放。     而在 Java 中,这个暂停按钮就是 Interrupt()方法。在第一
次调用 interrupt()方法时,线程中断;当再一次调用 interrupt()方法时,线程继续运行直到终止。
这里依然引用《用线程获得强大功能》一文中的程序片断,但为了更方便看到中断的过程,我在原程序的
基础上作了些改进,程序如下:

/*
============================================================================
=======
 * 文件:ThreadDemo09.java
 * 描述:线程的中断
 *
============================================================================
=======
 */
class ThreadA extends Thread{

 private Thread thdOther;

 ThreadA(Thread thdOther){
   this.thdOther = thdOther;
 }

 public void run(){

    System.out.println(getName() + " 运行...");

    int sleepTime = (int)(Math.random() * 10000);
    System.out.println(getName() + " 睡眠 " + sleepTime
Java 多线程编程                   - 20 -
     + " 毫秒。");

    try{
     Thread.sleep(sleepTime);
    }catch(InterruptedException e){}

    System.out.println(getName() + " 觉醒,即将中断线程 B。");
    // 中断线程 B,线程B暂停运行
    thdOther.interrupt();
 }
}

class ThreadB extends Thread{
 int count = 0;

 public void run(){

    System.out.println(getName() + " 运行...");

    while (!this.isInterrupted()){
     System.out.println(getName() + " 运行中 " + count++);

     try{
      Thread.sleep(10);
     }catch(InterruptedException e){
      int sleepTime = (int)(Math.random() * 10000);
      System.out.println(getName() + " 睡眠" + sleepTime
       + " 毫秒。觉醒后立即运行直到终止。");

        try{
         Thread.sleep(sleepTime);
        }catch(InterruptedException m){}

        System.out.println(getName() + " 已经觉醒,运行终止...");
        // 重新设置标记,继续运行
        this.interrupt();
     }
    }

    System.out.println(getName() + " 终止。");
 }
}

class Test{
 public static void main(String argv[]){
  ThreadB thdb = new ThreadB();
  thdb.setName("ThreadB");

    ThreadA thda = new ThreadA(thdb);
    thda.setName("ThreadA");
Java 多线程编程          - 21 -


    thdb.start();
    thda.start();
 }
}
  运行以上程序,你可以清楚地看到中断的过程。首先线程B开始运行,接着运行线程A,在线程A睡
眠一段时间觉醒后,调用 interrupt()方法中断线程B,此是可能B正在睡眠,觉醒后掏出一个
InterruptedException 异常,执行其中的语句,为了更清楚地看到线程的中断恢复,我在
InterruptedException 异常后增加了一次睡眠,当睡眠结束后,线程B调用自身的 interrupt()
方法恢复中断,这时测试 isInterrupt()返回 true,线程退出。
线程和进程(Threads and Processes)
第一个关键的系统级概念,究竟什么是线程或者说究竟什么是进程?她们其实就是操作系统内部的一种
数据结构。
进程数据结构掌握着所有与内存相关的东西:全局地址空间、文件句柄等等诸如此类的东西。当一个进程
放弃执行(准确的说是放弃占有 CPU),而被操作系统交换到硬盘上,使别的进程有机会运行的时候,
在那个进程里的所有数据也将被写到硬盘上,甚至包括整个系统的核心(core memory)。可以这么说,
当你想到进程(process),就应该想到内存(memory) (进程 == 内存)。如上所述,切换进程的
代价非常大,总有那么一大堆的内存要移来移去。你必须用秒这个单位来计量进程切换(上下文切换),
对于用户来说秒意味着明显的等待和硬盘灯的狂闪(对于作者的我,就意味着 IBM 龙腾 3 代的烂掉,
5555555)。 言归正传,对于 Java 而言,JVM 就几乎相当于一个进程(process),因为只有进程才能
拥有堆内存(heap,也就是我们平时用 new 操作符,分出来的内存空间)。
那么线程是什么呢?你可以把它看成“一段代码的执行”---- 也就是一系列由 JVM 执行的二进制指令。
这里面没有对象(Object)甚至没有方法(Method)的概念。指令执行的序列可以重叠,并且并行的执
行。后面,我会更加详细的论述这个问题。但是请记住,线程是有序的指令,而不是方法(method)。
线程的数据结构,与进程相反,仅仅只包括执行这些指令的信息。它包含当前的运行上下文
(context):如寄存器(register)的内容、当前指令的在运行引擎的指令流中的位置、保存方法
(methods)本地参数和变量的运行时堆栈。如果发生线程切换,OS 只需把寄存器的值压进栈,然后把
线程包含的数据结构放到某个类是列表(LIST)的地方;把另一个线程的数据从列表中取出,并且用栈里
的值重新设置寄存器。      切换线程更加有效率,时间单位是毫秒。   对于 Java 而言,一个线程可以看作是 JVM
的一个状态。
运行时堆栈(也就是前面说的存储本地变量和参数的地方)是线程数据结构一部分。这是因为多个线程,
每一个都有自己的运行时堆栈,也就是说存储在这里面的数据是绝对线程安全(后面将会详细解释这个
概念)的。因为可以肯定一个线程是无法修改另一个线程的系统级的数据结构的。也可以这么说一个不访
问堆内存的(只读写堆栈内存)方法,是线程安全的(Thread Safe)。
线程安全和同步
线程安全,是指一个方法(method)可以在多线程的环境下安全的有效的访问进程级的数据(这些数据
是与其他线程共享的)。事实上,线程安全是个很难达到的目标。
线程安全的核心概念就是同步,它保证多个线程:
同时开始执行,并行运行
不同时访问相同的对象实例
不同时执行同一段代码
我将会在后面的章节,一一细诉这些问题。但现在还是让我们来看看同步的一种经典的
实现方法——信号量。      信号量是任何可以让两个线程为了同步它们的操作而相互通信的对象。      Java 也是通
过信号量来实现线程间通信的。
不要被微软的文档所暗示的信号量仅仅是 Dijksta 提出的计数型信号量所迷惑。信号量其实包含任何可
以用来同步的对象。
如果没有 synchronized 关键字,就无法用 JAVA 实现信号量,但是仅仅只依靠它也不足够。我将会在
后面为大家演示一种用 Java 实现的信号量。
同步的代价很高哟!
       同步(或者说信号量,随你喜欢啦)的一个很让人头痛的问题就是代价。          考虑一下,下面的代码:
Java 多线程编程                      - 22 -
Listing 1.2:
import java.util.*;
import java.text.NumberFormat;
class Synch
{
    private static long[ ] locking_time = new long[100];
    private static long[ ] not_locking_time = new long[100];
    private static final long ITERATIONS = 10000000;
    synchronized long locking     (long a, long b){return a + b;}
    long              not_locking (long a, long b){return a + b;}

       private void test( int id )
       {
         long start = System.currentTimeMillis();
         for(long i = ITERATIONS; --i >= 0 ;)
              {     locking(i,i);             }
         locking_time[id] = System.currentTimeMillis() - start;
         start = System.currentTimeMillis();
           for(long i = ITERATIONS; --i >= 0 ;)
           {     not_locking(i,i);              }
             not_locking_time[id] = System.currentTimeMillis() - start;
          }
         static void print_results( int id )
         {      NumberFormat compositor = NumberFormat.getInstance();
                compositor.setMaximumFractionDigits( 2 );
                double time_in_synchronization = locking_time[id] -
not_locking_time[id];
           System.out.println( "Pass " + id + ": Time lost: "
                           + compositor.format( time_in_synchronization
)
                          + " ms. "
                           +
compositor.format( ((double)locking_time[id]/not_locking_time[id])*100.0 )
                             + "% increase"                      );
         }
     static public void main(String[ ] args) throws InterruptedException
   {
                final Synch tester = new Synch();
                tester.test(0); print_results(0);
                tester.test(1); print_results(1);
                tester.test(2); print_results(2);
                tester.test(3); print_results(3);
                tester.test(4); print_results(4);
                tester.test(5); print_results(5);
                tester.test(6); print_results(6);
              final Object start_gate = new Object();
                 Thread t1 = new Thread()
                 {     public void run()
                        {       try{ synchronized(start_gate)
{      start_gate.wait(); } }
Java 多线程编程                      - 23 -
                             catch( InterruptedException e ){}
                              tester.test(7);
                      }
                };
                Thread t2 = new Thread()
                {     public void run()
                       {     try{ synchronized(start_gate)
{       start_gate.wait(); } }
                               catch( InterruptedException e ){}
                                tester.test(8);
                       }
                };
                Thread.currentThread().setPriority( Thread.MIN_PRIORITY );
                t1.start();
                t2.start();
                synchronized(start_gate){ start_gate.notifyAll(); }
                t1.join();
                t2.join();
                print_results( 7 );
                print_results( 8 );
    }
}
     这是一个简单的基准测试程序,她清楚的向大家揭示了同步的代价是多么的大。                       test(…)方法调用
2 个方法 1,000,000,0 次。   其中一个是同步的,另一个则否。          下面是在我的机器上输出的结果(CPU:
P4 2.4G(B); Memory: 1GB; OS: windows 2000 server(sp3); JDK: Ver1.4.01 and
HotSpot 1.4.01-b01):
C:>java -verbose:gc Synch
Pass 0: Time lost: 251 ms. 727.5% increase
Pass 1: Time lost: 250 ms. 725% increase
Pass 2: Time lost: 251 ms. 602% increase
Pass 3: Time lost: 250 ms. 725% increase
Pass 4: Time lost: 261 ms. 752.5% increase
Pass 5: Time lost: 260 ms. 750% increase
Pass 6: Time lost: 261 ms. 752.5% increase
Pass 7: Time lost: 1,953 ms. 1,248.82% increase
Pass 8: Time lost: 3,475 ms. 8,787.5% increase
这里为了使 HotSpot JVM 充分的发挥其威力,test( )方法被多次反复调用。              一旦这段程序被彻底优化
以后,也就是大约在 Pass 6 时,同步的代价达到最大。            Pass 7 和 Pass 8 与前面的区别在于,我 new
了两个新的线程来并行执行        test 方法,两个线程竞争执行(后面是适当的地方,我会解释什么是“竞争
”,如果你已经等不及了,买本大学的操作系统课本看看吧! J),这使结果更加接近真实。同步的代价
是如此之高的,应该尽量避免无谓的同步代价。
现在是时候我们更深入的讨论一下同步的代价了。HotSpot JVM 一般会使用一到两个方法来实现同步,
这主要取决于是否存在线程的竞争。当没有竞争的时候,计算机的汇编指令顺序的执行,这些指令的执行
是不被打断。指令试图测试一个比特(bit),然后设置各种二进制位来表示测试的结果,如果这个 bit
没有被设置,指令就设置它。这可以说是非常原始的信号量,因为当两个线程同步的企图设置一个 bit 的
值时,只有一个线程可以成功,两个线程都会检查结果,看看是不是自己设成功了。
如果 bit 已经被设置(这里说的是有线程竞争的情况下),失败的 JVM(线程)不得不离开操作系统的
核心进程等待这个 bit 位被清零。这样来回的在系统核心中切换是非常耗时的。在 NT 系统下,需要 600
次机械指令循环来进入一次系统内核,这还仅仅是进入所耗费的时间还不包括做操作的时间。
  是不是觉得很无聊了,呵呵!今天似乎都是些不顶用的东西。但这是必须的,为了使你能够读懂后
Java 多线程编程                - 24 -

面的内容。下一篇,我将会谈到一些更有趣的话题,例如如何避免同步,如果大家不反对,我还想讲一些
设计模式的东西。下回见!
避免同步
大部分显示的同步都可以避免。一般不操作对象状态信息(例如数据成员)的方法都不需要同步,例如:
一些方法只访问本地变量(也就是说在方法内部声明的变量),而不操作类级别的数据成员,并且这些
方法不会通过传入的引用参数来修改外部的对象。符合这些条件的方法都不需要使用 synchronization
这种重量级的操作。除此之外,还可以使用一些设计模式(Design Pattern)来避免同步(我将会在
后面提到)。
你甚至可以通过适当的组织你的代码来避免同步。相对于同步的一个重要的概念就是原子性。一个原子性
的操作事不能被其他线程中断的,通常的原子性操作是不需要同步的。
Java 定义一些原子性的操作。一般的给变量付值的操作是原子的,除了 long 和 double。看下面的代码:
class Unreliable
{
private long x;
public long get_x( ) {return x;}
public void set_x(long value) { x = value; }
}
  线程 1 调用:
obj.set_x( 0 );
线程 2 调用:
obj.set_x( 0x123456789abcdef )
问题在于下面这行代码:
x = value;
JVM 为了效率的问题,并没有把 x 当作一个 64 位的长整型数来使用,而是把它分为两个 32-bit,分别
付值:
x.hgh_word = value.high_word;
x.low_word = value.low_word;
    因此,存在一个线程设置了高位之后被另一个线程切换出去,而改变了其高位或低位的值。所以,x
的值最终可能为 0x0123456789abcdef、0x01234567000000、0x00000000abcdef 和
0x00000000000000。你根本无法确定它的值,唯一的解决方法是,为 set_x( )和 get_x()方法加
上 synchronized 这个关键字或者把这个付值操作封装在一个确保原子性的代码段里。
所以,在操作的 long 型数据的时候,千万不要想当然。强迫自己记住吧:只有直接付值操作是原子的
(除了上面的例子)。其它,任何表达式,象 x = ++y、x += y 都是不安全的,不管 x 或 y 的数据类型
是否是小于 64 位的。很可能在付值之前,自增之后,被其它线程抢先了(preempted)。
竞争条件
       在术语中,对于前面我提到的多线程问题——两个线程同步操作同一个对象,使这个对象的最
终状态不明——叫做竞争条件。竞争条件可以在任何应该由程序员保证原子操作的,而又忘记使用
synchronized 的地方。在这个意义上,可以把 synchronized 看作一种保证复杂的、顺序一定的操作
具有原子性的工具,例如给一个 boolean 值变量付值,就是一个隐式的同步操作。
不变性
一种有效的语言级的避免同步的方法就是不变性(immutability)。          一个自从产生那一刻起就无法再改
变的对象就是不变性对象,例如一个       String 对象。但是要注意类似这样的表达式:string1 +=
string2;本质上等同于 string1 = string1 + string2;其实第三个包含 string1 和 string2
的 string 对象被隐式的产生,最后,把 string1 的引用指向第三个 string。这样的操作,并不是原
子的。
由于不变对象的值无法发生改变,所以可以为多个线程安全的同步操作,不需要 synchronized。
把一个类的所有数据成员都声明为 final 就可以创建一个不变类型了。那些被声明为 final 的数据成员
并不是必须在声明的时候就写死,但必须在类的构造函数中,全部明确的初始化。例如:
Class I_am_immutable
{
Java 多线程编程                   - 25 -
private final int MAX_VALUE = 10;
priate final int blank_final;
public I_am_immutable( int_initial_value )
{
      blank_final = initial_value;
}
}
  一个由构造函数进行初始化的 final 型变量叫做 blank final。一般的,如果你频繁的只读访问一个
对象,把它声明成一个不变对象是个保证同步的好办法,而且可以提高 JVM 的效率,因为 HotSpot 会把
它放到堆栈里以供使用。
同步封装器(Synchronization Wrappers)
       同步还是不同步,是问题的所在。让我们跳出这样的思维模式吧,世事无绝对。有什么办法可以
使你的类灵活的在同步与不同步之间切换呢? 有一个非常好的现成例子,就是新近引入 JAVA 的
Collection 框架,它是用来取代原本散乱的、繁重的 Vector 等类型。Vector 的任何方法都是同步的,
这就是为什么说它繁重了。而对于 collections 对象,在需要保证同步的时候,一般会由访问它方法来
保证同步,因此没有必要两次锁定(一次是锁定包含使用 collection 对象的方法的对象,一次是锁定
collection 对象自身)。Java 的解决方案是使用同步封装器。其基本原理来自四人帮(Gang-of-
Four)的 Decorator 模式,一个 Decorator 自身就实现某个接口,而且又包含了实现同样接口的数
据成员,但是在通过外部类方法调用内部成员的相同方法的时候,控制或者修改传入的变量。java.io
这个包里的所有类都是 Decorator:一个 BufferedInputStream 既实现了虚类 InputStream 的所
有方法,又包含了一个 InputStream 引用所指向的成员变量。程序员调用外部容器类的方法,实际上是
变相的调用内部对象的方法。
       我们可以利用这种设计模式。来实现灵活的同步方法。如下例:
Interface Some_interface
{Object message( ); }
class Not_thread_safe implements Some_interface
{public Object message( )
{     //实现该方法的代码,省~~~~~~~~~~~
     return null;
}
}
class Thread_safe_wrapper implements Some_interface
{Some_interface not_thread_safe;
public Thread_safe_wrapper(Some_interface not_thread_safe)
{     this.not_thread_safe = not_thread_safe;}
public Some_interface extract( )
{   return not_thread_safe;}
public synchronized Object message( )
{ return not_thread_safe.message( );}
}
  当不存在线程安全的时候,你可以直接使用 Not_thread_safe 类对象。当需要考虑线程安全的时候,
只需要把它包装一下:
    Some_interface object = new Not_thread_safe( );
    ……………
    object = new Thread_safe_wrapper(object); //object 现在变成线程安全了
当你不需要考虑线程安全的时候,你可以还原 object 对象:
     object = ((Thread_safe_Wrapper)object).extract( );
 下一回,我们又要深入底层机制了。呵呵!千万不要闷着大家呀!下回见!
线程的并发性
     下一个与 OS 平台相关的问题(这也是编写与平台无关的 Java 程序要面对的问题)是必须确定
Java 多线程编程                    - 26 -

并发性和并行在该平台的定义。并发的多线程系统总会给人多个任务同时运行的感觉,其实这些任务是被
分割为许多的块交错在一起执行的。在一个并行的系统中,两个任务实际上是同时(这里的同时是真正的
同时,而不是快速交错执行所产生的并行假象)运行的,这就要求有多个 CPU。如图 1.1:
   多线程其实并不能加快的程序速度。如果你的程序并不需要频繁的等待 IO 操作完成,那么多线程程
序还会比单线程程序更慢些。但在多 CPU 系统下则反之。
        Java 线程系统非平台独立的主要原因就是要实现彻底的平行运行的线程,如果不使用 OS 提供
的系统线程模型,是不可能的。对于 JAVA 而言,在理论上,允许由 JVM 来模仿整个线程系统,从而避免
我在前一篇文章(驯服 JAVA 线程 2)中,所提到的进入 OS 核心的时间消耗。但是,这样也排除了程序中
的并行性,因为如果不使用任何操作系统级的线程(这样是为了保持平台独立性),OS 会把 JVM 的实例
当成一个单线程的程序来看待,也就只会分配单个 CPU 来执行它,从而导致就算运行在多 CPU 的机器上,
而且只有一个 JVM 实例在单独运行,也不可能出现两个 Java 线程真正的并行运行(充分的利用两个
CPU)。
        所以,要真正的实现并行运行,只有存在两个 JVM 实例,分别运行不同的程序。做的再好一点
就是让 JVM 把 Java 的线程映射到 OS 级的线程上去(一个 Java 线程就是一个系统的线程,让系统进行
调配,充分发挥系统对资源的操控能力,这样就不存在只能在一个 CPU 上运行的问题了)。        不幸的是,不
同的操作系统实现的线程机制也不同,而且这些区别已经到了在编程时不能忽视的地步了。
由于平台不同而导致的问题
下面,我将会通过比较 Solaris 和 WindowsNT 对线程机制实现不同之处,来说明前面提到的问题。
Java,在理论上,至少有 10 个线程优先等级划分(如果有两个或两个以上的线程都处在 on ready 状
态下,那么拥有高优先级的线程将会先执行)。在 Solaris 里,支持 231 个优先等级,当然对于支持
Java 那 10 个的等级是没问题的。


在 NT 下,最多只有 7 优先级划分,却必须映射到 Java 的 10 个等级。这就会出现很多的可能性(可能
Java 里面的优先级 1、 就等同于 NT 里的优先级 1,优先级 8、 、 则等于 NT 里的 7 级,还有很多的可
                 2                            9 10
能性)。因此,在 NT 下依靠优先级来调度线程时存在很多问题。
更不幸的还在后面呢!NT 下的线程优先级竟然还不是固定的!这就更加复杂了!NT 提供了一个名叫优先
级助推(Priority Boosting)的机制。这个机制使程序员可以通过调用一个 C 语言的系统
Call(Windows NT/2000/XP: You can disable the priority-boosting feature by
calling the SetProcessPriorityBoost or SetThreadPriorityBoost function. To
determine whether this feature has been disabled, call the
GetProcessPriorityBoost or GetThreadPriorityBoost function.)来改变线程优先级,
但 Java 不能这样做。当打开了 Priority Boosting 功能的时候,NT 依据线程每次执行 I/O 相关的系
统调用的大概时间来提高该线程的优先级。在实践中,这意味着一个线程的优先级可能高过你的想象,因
为这个线程碰巧在一个繁忙的时刻进行了一次 I/O 操作。    线程优先级助推机制的目的是为了防止一个后台
进程(或线程)影响了前台的 UI 显示进程。其它的操作系统同样有着复杂的算法来降低后台进程的优先
级。这个机制的一个严重的副作用就是使我们无法通过优先级来判断即将运行的就绪太线程。
在这种情况下,事态往往会变得更糟。
在 Solaris 中,也就意味着在所有的 Unix 系统中,或者说在所有当代的操作系统中,除了微软的以外,
每个进程或者线程都有优先级。高优先级的进程时不会被低优先级的进程打断的,此外,一个进程的优先
级可以由管理员限制和设定,以防止一个用户进程是打断 OS 核心进程或者服务。NT 对此都无法支持。一
个 NT 的进程就是一个内存的地址空间。    它没有固定优先级,也不能被预先编排。  而全部交由系统来调度,
如果一个线程运行在一个不再内存中的进程下,这个进程将会被切换进内存。         NT 中进程的优先级被简化
为几个分布在实际优先级范围内的优先级类,也就是说他们是不固定的,由系统核心干预调配。 1.2 图:   如
上图中的列,代表线程的优先级,只有 22 级是有所有的程序所使用(其它的只能为 NT 自己使用)。     行代
表前面提过的优先级类。
     一个运行在“Idle”级进程上的线程,只能使用 1-6 和 15,这七个优先级别,当然具体的是那
一级,还要取决于线程的设定。运行在“Normal”级进程里并且没有得到焦点的一个线程将可能会使用
1,6 — 10 或 15 的优先级。如果有焦点而且所在进程使还是“Normal”级,这样里面的线程将会运行在
1,7 — 11 或者 15 级。这也就意味着一个拥有搞优先级但在“Idle”进程内的线程,有可能被一个低
优先级但是运行在“Normal”级的线程抢先(Preempt),但这只限于后台进程。还应该注意到一个运行
Java 多线程编程                   - 27 -

在“High”优先级类的进程只有 6 个优先级等级而其它优先级类都有 7。
    NT 不对进程的优先级类进行任何限制。运行在任意进程上的任意线程,可以通过优先级助推机制完
全控制整个系统, OS 核心没有任何的防御。          另一方面,Solaris 完全支持进程优先级的机制,因为你可
能需要设定你的屏幕保护程序的优先级,以防止它阻碍系统重要进程的运行 J。因为在一台关键的服务器
中,优先级低的进程就不应该也不能占用高优先级的线程执行。由此可见,微软的操作系统根本不适合做
高可靠性服务器。
   那么我们在编程的时候,怎样避免呢?对于 NT 的这种无限制的优先级设定和无法控制的优先级助推
机制(对于 Java 程序),事实上就没有绝对安全的方法来使 Java 程序依赖优先级调度线程的执行。一
个折衷的方法,就是在用 setPriority( )函数设定线程优先级的时候,只使用
Thread.MAX_PRIORITY, Thread.MIN_PRIORITY 和 Thread.NORM_PRIORITY 这几个没有具体指
明优先级的参数。这个限制至少可以避免把 10 级映射为 7 级的问题。另外,还建议可以通过 os.name 的
系统属性来判定是否是 NT,如果是就通过调用一个本地函数来关闭优先级助推机制,但是那对于运行在
没有使用 Sun 的 JVM plug-in 的 IE 的 Java 程序,也是毫无作用的(微软的 JVM 使用了一个非标准的,
本地实现)。最后,还是建议大家在编程的时候,把大多数的线程的优先级设置为 NORM_PRIORITY,并
且依靠线程调度机制(scheduling)。(我将会再后面的谈到这个问题)
协作!(Cooperate)
       一般来说,有两种线程模式:协作式和抢先式。
协作式多线程模型
       在一个协作式的系统中,一个线程包留对处理器的控制直到它自己决定放弃(也许它永远不会
放弃控制权)。多个线程之间不得不相互合作否则可能只有一个线程能够执行,其它都处于饥饿状态。在
大多数的协作式系统中,线程的调度一般由优先级决定。当当前的线程放弃控制权,等待的线程中优先级
高的将会得到控制权(一个特例,就是 Window 3.x 系统,它也是协作式系统,但却没有很多的线程执
行进度调节,得到焦点的窗体获得控制权)。
    协作式系统相对于抢先式系统的一个主要优点就是它运行速度快、                    代价低。  比如,一个上下文切换—
—控制权由一个线程转换到另一个线程——可以完全由用户模式子系统库完成,而不需进入系统核心态
(在 NT 下,就相当于 600 个机械指令时间)。在协作式系统下,用户态上下文切换就相当于 C 语言调用,
setjump / longjump。大量的协作式线程同时运行,也不会影响性能,因为一切由程序员编程掌握,
更加不需要考虑同步的问题。程序员只要保证它的线程在没有完成目标以前不放弃对 CPU 的控制。但世界
终归不是完美的,人生总充满了遗憾。协同式线程模型也有自己硬伤:
    在协作式线程模型下,用户编程非常麻烦(其实就是系统把复杂度转移到用户身上)。把很长的操
作分割成许多小块,是一件需要很小心的事。
2. 协作式线程无法并行执行。
抢先式多线程模型
另一种选择就是抢先式模型,这种模式就好象是有一个系统内部的时钟,由它来触发线程的切换。也就是
说,系统可以任意的从线程中夺回对 CPU 的控制权,在把控制权分给其它的线程。                     两次切换之间的时间间
隔就叫做时间切片(Time Slice)。
抢先式系统的效率不如协作式高,因为 OS 核心必须负责管理线程,但是这样使用户在编程的时候不用考
虑那么多的其它问题,简化了用户的工作,而且使程序更加可靠,因为线程饥饿不再是一个问题。最关键
的优势在于抢先式模型是并行的。通过前面的介绍,你可以知道协作式线程调度是由用户子程序完成,而
不是 OS,因此,你最多可以做到使你的程序具有并发性(如图 1.1)。为了达到真正并行的目的,必须
有操作系统介入。四个线程并行的运行在四个 CPU 上的效率要比四个线程并发的运行高的多。
一些操作系统,象 Windows 3.1,只支持协作式模型;还有一些,象 NT 只支持抢先式模型(当然了你
也可以通过使用用户模式的库调用在 NT 上模拟协作式模型。NT 就有这么一个库叫“fiber”,但是遗憾
的是 fiber 充满的了 Bugs,并且没有彻底的整合到底层系统中去。)Solaris 提供了世界上可能是最
好的(当然也可能是最差的)线程模型,它既支持协作式,又支持抢先式。
从核心线程到用户进程的映射
  最后一个要解决的问题就是核心线程到用户态进程的映射。NT 使用的是一对一的映射模式,
    NT 的用户线程就相当于系统核心线程。          他们被 OS 直接映射到每一个处理器上,并且总是抢先式的。
所有的线程操作和同步都是通过核心调用完成的。这是一个非常直接的模型,但是它既不灵活又低效。
    图 1.4 表现的 Solaris 模型更加有趣。Solaris 增加了一个叫轻量级进程(LWP —
Java 多线程编程              - 28 -
lightweight process)的概念。LWP 是可以运行一个或多个线程的可调度单元。只在 LWP 这个程次上
进行并行处理。一般的,LWP 都存放在缓冲池中,并且按需分配给相应的处理器。如果一个 LWP 要执行某
些时序性要求很高的任务时,它一定要绑定特定处理器,以阻止其它的 LWPs 使用它。
    从用户的角度来看,这是一个既协作又抢先的线程模型。简单来说,一个进程至少有一个 LWP 供它
所包含的所有线程共享使用。每个线程必须通过出让使用权(yield)来让其它线程执行(协作),但是
单个 LWP 又可以为其它的进程的 LWP 抢先。这样在进程一级上就达到了并行的效果,同时在里面线程又
处于协作的工作模式。
    一个进程并不限死只有一个 LWP,这个进程下的线程们可以共享整个 LWP 池。一个线程可以通过以
下两种方法绑定到一个 LWP 上:
1. 通过编程显示的绑定一个或多个线程到一个指定的 LWP。在这个情况下,同一个 LWP 下的线程必须协
作式工作,但是这些线程又可以抢先其它 LWP 下的线程。这也就是说如果限制一个 LWP 只能绑定一个线
程,那就变成了 NT 的抢先式线程系统了。
2. 通过用户态的调度器自动绑定。从编程的角度看,这是一个比较混乱的情况,因为你并不能假设环境
究竟是协作式的,还是抢先式的。
Solaris 线程系统给于用户最大的灵活性。你可以在速度极快的并发协作式系统和较慢但
确是并行的抢先式系统,或者这两者的折衷中选择。但是,Solaris 的世界真的是那么完美吗?(我的
一贯论调又出现了,呵呵!)这一切一切的灵活性对于一个 Java 程序员来说等于没有,因为你并不能决
定 JVM 采用的线程模型。例如,早期的 Solaris JVM 采取严格的协作式机制。JVM 相当于一个 LWP,所
有 Java 线程共享这唯一一个 LWP。现在 Solaris JVM 又采用彻底的抢先式模型了,所有的线程独占各
自的 LWP。
    那么我们这些可怜的程序员怎么办?我们在这个世上是如此的渺小,就连 JVM 采用那种模式的线程
机制都无法确定。为了写出平台独立的代码,必须做出两个表面上矛盾的假设:
一个线程可以被另一个线程在任何时候抢先。因此,你必须小心的使用 synchronized 关键字来保证非
原子性的操作运行正确。
2. 一个线程永远不会被抢先除非它自己放弃控制权。因此,你必须偶然的执行一些放弃控制权的操作来
给机会别的线程运行,适当的使用 yield( )和 sleep( )或者利用阻塞性的 I/O 调用。例如当你的线程
每进行 100 次遍例或者相当长度的密集操作后,你应该主动的使你的线程休眠几百个毫秒,来给低优先
级的线程以机会运行。注意!yield( )方法只会把控制权交给与你线程的优先级相当或者更高的线程。
总结
       由于诸多的 OS 级因素导致了 Java 程序员在编写彻底平台独立的多线程程序时,麻烦频频(唉
~~~~~~我又想发牢骚了,忍住!)。我们只能按最糟糕的情况打算,例如只能假设你的线程随时都会被
抢先,所以必须适当的使用 synchronized;又不得不假设你的线程永远不会被抢先,如果你不自己放
弃,所以你又必须偶尔使用 yield( )和 sleep( )或阻塞的 I/O 操作来让出控制权。还有就是我一开始
就介绍的:永远不要相信线程优先级,如果你想真正做到平台独立!

Contenu connexe

Tendances

Inside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutor
Inside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutorInside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutor
Inside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutorAdy Liu
 
Java程序员面试之葵花宝典
Java程序员面试之葵花宝典Java程序员面试之葵花宝典
Java程序员面试之葵花宝典yiditushe
 
线程编程方面
线程编程方面线程编程方面
线程编程方面yiditushe
 
Hash map导致cpu100% 的分析
Hash map导致cpu100% 的分析Hash map导致cpu100% 的分析
Hash map导致cpu100% 的分析wang hongjiang
 
Java面试宝典
Java面试宝典Java面试宝典
Java面试宝典ma tao
 
Java面试32题
Java面试32题Java面试32题
Java面试32题yiditushe
 
Java并发编程实践
Java并发编程实践Java并发编程实践
Java并发编程实践sharewind
 
千呼萬喚始出來的Java SE 7
千呼萬喚始出來的Java SE 7千呼萬喚始出來的Java SE 7
千呼萬喚始出來的Java SE 7javatwo2011
 
Java面试题集
Java面试题集Java面试题集
Java面试题集yiditushe
 
大公司的Java面试题集
大公司的Java面试题集大公司的Java面试题集
大公司的Java面试题集yiditushe
 
Java华为面试题
Java华为面试题Java华为面试题
Java华为面试题yiditushe
 
Java并发核心编程
Java并发核心编程Java并发核心编程
Java并发核心编程wavefly
 
Java 6中的线程优化真的有效么?
Java 6中的线程优化真的有效么?Java 6中的线程优化真的有效么?
Java 6中的线程优化真的有效么?wensheng wei
 
最新Java技术内存模型
最新Java技术内存模型最新Java技术内存模型
最新Java技术内存模型yiditushe
 
IOS入门分享
IOS入门分享IOS入门分享
IOS入门分享zenyuhao
 
Hibernate的高级操作
Hibernate的高级操作Hibernate的高级操作
Hibernate的高级操作yiditushe
 
Free rtos workshop1@nuu
Free rtos workshop1@nuuFree rtos workshop1@nuu
Free rtos workshop1@nuu紀榮 陳
 

Tendances (20)

Inside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutor
Inside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutorInside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutor
Inside.java.concurrency 35.thread pool.part8_future.scheduledthreadpoolexecutor
 
Java程序员面试之葵花宝典
Java程序员面试之葵花宝典Java程序员面试之葵花宝典
Java程序员面试之葵花宝典
 
线程编程方面
线程编程方面线程编程方面
线程编程方面
 
Hash map导致cpu100% 的分析
Hash map导致cpu100% 的分析Hash map导致cpu100% 的分析
Hash map导致cpu100% 的分析
 
Java面试宝典
Java面试宝典Java面试宝典
Java面试宝典
 
Java面试32题
Java面试32题Java面试32题
Java面试32题
 
Java并发编程实践
Java并发编程实践Java并发编程实践
Java并发编程实践
 
千呼萬喚始出來的Java SE 7
千呼萬喚始出來的Java SE 7千呼萬喚始出來的Java SE 7
千呼萬喚始出來的Java SE 7
 
泛型总结
泛型总结泛型总结
泛型总结
 
Java面试题集
Java面试题集Java面试题集
Java面试题集
 
大公司的Java面试题集
大公司的Java面试题集大公司的Java面试题集
大公司的Java面试题集
 
Java华为面试题
Java华为面试题Java华为面试题
Java华为面试题
 
Java并发核心编程
Java并发核心编程Java并发核心编程
Java并发核心编程
 
Java 6中的线程优化真的有效么?
Java 6中的线程优化真的有效么?Java 6中的线程优化真的有效么?
Java 6中的线程优化真的有效么?
 
最新Java技术内存模型
最新Java技术内存模型最新Java技术内存模型
最新Java技术内存模型
 
IOS入门分享
IOS入门分享IOS入门分享
IOS入门分享
 
Hibernate的高级操作
Hibernate的高级操作Hibernate的高级操作
Hibernate的高级操作
 
Javascript 闭包
Javascript 闭包Javascript 闭包
Javascript 闭包
 
Free rtos workshop1@nuu
Free rtos workshop1@nuuFree rtos workshop1@nuu
Free rtos workshop1@nuu
 
Refactoring 重构
Refactoring 重构Refactoring 重构
Refactoring 重构
 

En vedette

Using WordPress as a Social Media Dashboard
Using WordPress as a Social Media DashboardUsing WordPress as a Social Media Dashboard
Using WordPress as a Social Media DashboardConvertiv
 
sex ed hw J.ppt
sex ed hw J.pptsex ed hw J.ppt
sex ed hw J.pptmrmeredith
 
Indicator #3
Indicator #3Indicator #3
Indicator #3kirkbaby
 
As Media Coursework 1
As Media Coursework 1As Media Coursework 1
As Media Coursework 1Neill Ford
 
Healthstory Update To MTIA Board
Healthstory Update To MTIA BoardHealthstory Update To MTIA Board
Healthstory Update To MTIA BoardNick van Terheyden
 
11 M Attacks Adrià Flores Moliner
11 M Attacks Adrià Flores Moliner11 M Attacks Adrià Flores Moliner
11 M Attacks Adrià Flores MolinerDaniversus
 
Mobile Device Sales Training
Mobile Device Sales TrainingMobile Device Sales Training
Mobile Device Sales Trainingemaginative
 
J Boss+J Bpm+J Pdl用户开发手册 3.2.3
J Boss+J Bpm+J Pdl用户开发手册 3.2.3J Boss+J Bpm+J Pdl用户开发手册 3.2.3
J Boss+J Bpm+J Pdl用户开发手册 3.2.3yiditushe
 
Sales Presentation
Sales PresentationSales Presentation
Sales Presentationjosesmeke
 
R E V O L U T I O N D R
R E V O L U T I O N  D RR E V O L U T I O N  D R
R E V O L U T I O N D Rbanothkishan
 
M U S I C A N D S U P E R H E A L T H D R
M U S I C  A N D  S U P E R H E A L T H  D RM U S I C  A N D  S U P E R H E A L T H  D R
M U S I C A N D S U P E R H E A L T H D Rbanothkishan
 
  NEW BORN DISGUISED KILLINGS 56 PERCENT          
  NEW BORN DISGUISED KILLINGS 56 PERCENT            NEW BORN DISGUISED KILLINGS 56 PERCENT          
  NEW BORN DISGUISED KILLINGS 56 PERCENT          Prayer Warriors Institute
 
I M P O R T A N C E O F M A N A G I N G S T R E S S D R
I M P O R T A N C E  O F  M A N A G I N G  S T R E S S  D RI M P O R T A N C E  O F  M A N A G I N G  S T R E S S  D R
I M P O R T A N C E O F M A N A G I N G S T R E S S D Rbanothkishan
 

En vedette (19)

hw4_specifications
hw4_specificationshw4_specifications
hw4_specifications
 
Using WordPress as a Social Media Dashboard
Using WordPress as a Social Media DashboardUsing WordPress as a Social Media Dashboard
Using WordPress as a Social Media Dashboard
 
sex ed hw J.ppt
sex ed hw J.pptsex ed hw J.ppt
sex ed hw J.ppt
 
Indicator #3
Indicator #3Indicator #3
Indicator #3
 
As Media Coursework 1
As Media Coursework 1As Media Coursework 1
As Media Coursework 1
 
Healthstory Update To MTIA Board
Healthstory Update To MTIA BoardHealthstory Update To MTIA Board
Healthstory Update To MTIA Board
 
AFT SERIAL KILLER BOYDEN GRAY'S
AFT SERIAL KILLER BOYDEN GRAY'SAFT SERIAL KILLER BOYDEN GRAY'S
AFT SERIAL KILLER BOYDEN GRAY'S
 
1 6 Networking Forestry
1 6 Networking   Forestry1 6 Networking   Forestry
1 6 Networking Forestry
 
11 M Attacks Adrià Flores Moliner
11 M Attacks Adrià Flores Moliner11 M Attacks Adrià Flores Moliner
11 M Attacks Adrià Flores Moliner
 
Mobile Device Sales Training
Mobile Device Sales TrainingMobile Device Sales Training
Mobile Device Sales Training
 
3 november 2009 KHN Congres "Innoveren met je website"
3 november 2009  KHN Congres "Innoveren met je website"3 november 2009  KHN Congres "Innoveren met je website"
3 november 2009 KHN Congres "Innoveren met je website"
 
CatConf2001
CatConf2001CatConf2001
CatConf2001
 
J Boss+J Bpm+J Pdl用户开发手册 3.2.3
J Boss+J Bpm+J Pdl用户开发手册 3.2.3J Boss+J Bpm+J Pdl用户开发手册 3.2.3
J Boss+J Bpm+J Pdl用户开发手册 3.2.3
 
Sales Presentation
Sales PresentationSales Presentation
Sales Presentation
 
R E V O L U T I O N D R
R E V O L U T I O N  D RR E V O L U T I O N  D R
R E V O L U T I O N D R
 
M U S I C A N D S U P E R H E A L T H D R
M U S I C  A N D  S U P E R H E A L T H  D RM U S I C  A N D  S U P E R H E A L T H  D R
M U S I C A N D S U P E R H E A L T H D R
 
Brent-Linsteadt
Brent-LinsteadtBrent-Linsteadt
Brent-Linsteadt
 
  NEW BORN DISGUISED KILLINGS 56 PERCENT          
  NEW BORN DISGUISED KILLINGS 56 PERCENT            NEW BORN DISGUISED KILLINGS 56 PERCENT          
  NEW BORN DISGUISED KILLINGS 56 PERCENT          
 
I M P O R T A N C E O F M A N A G I N G S T R E S S D R
I M P O R T A N C E  O F  M A N A G I N G  S T R E S S  D RI M P O R T A N C E  O F  M A N A G I N G  S T R E S S  D R
I M P O R T A N C E O F M A N A G I N G S T R E S S D R
 

Similaire à Java多线程编程详解

Java并发编程培训
Java并发编程培训Java并发编程培训
Java并发编程培训longhao
 
J2ee经典学习笔记
J2ee经典学习笔记J2ee经典学习笔记
J2ee经典学习笔记yiditushe
 
Java相关基础知识
Java相关基础知识Java相关基础知识
Java相关基础知识yiditushe
 
Spring 2.0 技術手冊第五章 - JDBC、交易支援
Spring 2.0 技術手冊第五章 - JDBC、交易支援Spring 2.0 技術手冊第五章 - JDBC、交易支援
Spring 2.0 技術手冊第五章 - JDBC、交易支援Justin Lin
 
IKVM.NET 深入敵營的 Java
IKVM.NET 深入敵營的 JavaIKVM.NET 深入敵營的 Java
IKVM.NET 深入敵營的 Java建興 王
 
4.2第四章 swarm代码剖析 可选补充课程
4.2第四章 swarm代码剖析 可选补充课程4.2第四章 swarm代码剖析 可选补充课程
4.2第四章 swarm代码剖析 可选补充课程zhang shuren
 
學好 node.js 不可不知的事
學好 node.js 不可不知的事學好 node.js 不可不知的事
學好 node.js 不可不知的事Ben Lue
 
JVM内容管理和垃圾回收
JVM内容管理和垃圾回收JVM内容管理和垃圾回收
JVM内容管理和垃圾回收Tony Deng
 
Row Set初步学习V1.1
Row Set初步学习V1.1Row Set初步学习V1.1
Row Set初步学习V1.1Zianed Hou
 
Java SE 7 技術手冊投影片第 11 章 - 執行緒與並行API
Java SE 7 技術手冊投影片第 11 章 - 執行緒與並行APIJava SE 7 技術手冊投影片第 11 章 - 執行緒與並行API
Java SE 7 技術手冊投影片第 11 章 - 執行緒與並行APIJustin Lin
 
Scala分享第二期
Scala分享第二期Scala分享第二期
Scala分享第二期Tony Deng
 
Jni攻略之十一――启动虚拟机调用Java类
Jni攻略之十一――启动虚拟机调用Java类Jni攻略之十一――启动虚拟机调用Java类
Jni攻略之十一――启动虚拟机调用Java类yiditushe
 
Java面试笔试题大汇总
Java面试笔试题大汇总Java面试笔试题大汇总
Java面试笔试题大汇总yiditushe
 
《Java程序设计》期末考试试题 (六)
《Java程序设计》期末考试试题 (六)《Java程序设计》期末考试试题 (六)
《Java程序设计》期末考试试题 (六)jane2006
 
第01章 绪论(java版)
第01章  绪论(java版)第01章  绪论(java版)
第01章 绪论(java版)Yan Li
 

Similaire à Java多线程编程详解 (20)

Java并发编程培训
Java并发编程培训Java并发编程培训
Java并发编程培训
 
Sun java
Sun javaSun java
Sun java
 
J2ee经典学习笔记
J2ee经典学习笔记J2ee经典学习笔记
J2ee经典学习笔记
 
Java相关基础知识
Java相关基础知识Java相关基础知识
Java相关基础知识
 
Spring 2.0 技術手冊第五章 - JDBC、交易支援
Spring 2.0 技術手冊第五章 - JDBC、交易支援Spring 2.0 技術手冊第五章 - JDBC、交易支援
Spring 2.0 技術手冊第五章 - JDBC、交易支援
 
IKVM.NET 深入敵營的 Java
IKVM.NET 深入敵營的 JavaIKVM.NET 深入敵營的 Java
IKVM.NET 深入敵營的 Java
 
4.2第四章 swarm代码剖析 可选补充课程
4.2第四章 swarm代码剖析 可选补充课程4.2第四章 swarm代码剖析 可选补充课程
4.2第四章 swarm代码剖析 可选补充课程
 
Exodus2 大局观
Exodus2 大局观Exodus2 大局观
Exodus2 大局观
 
學好 node.js 不可不知的事
學好 node.js 不可不知的事學好 node.js 不可不知的事
學好 node.js 不可不知的事
 
JVM内容管理和垃圾回收
JVM内容管理和垃圾回收JVM内容管理和垃圾回收
JVM内容管理和垃圾回收
 
Row Set初步学习V1.1
Row Set初步学习V1.1Row Set初步学习V1.1
Row Set初步学习V1.1
 
Java SE 7 技術手冊投影片第 11 章 - 執行緒與並行API
Java SE 7 技術手冊投影片第 11 章 - 執行緒與並行APIJava SE 7 技術手冊投影片第 11 章 - 執行緒與並行API
Java SE 7 技術手冊投影片第 11 章 - 執行緒與並行API
 
Scala分享第二期
Scala分享第二期Scala分享第二期
Scala分享第二期
 
Java物件導向
Java物件導向Java物件導向
Java物件導向
 
Jni攻略之十一――启动虚拟机调用Java类
Jni攻略之十一――启动虚拟机调用Java类Jni攻略之十一――启动虚拟机调用Java类
Jni攻略之十一――启动虚拟机调用Java类
 
Js培训
Js培训Js培训
Js培训
 
beidakejian
beidakejianbeidakejian
beidakejian
 
Java面试笔试题大汇总
Java面试笔试题大汇总Java面试笔试题大汇总
Java面试笔试题大汇总
 
《Java程序设计》期末考试试题 (六)
《Java程序设计》期末考试试题 (六)《Java程序设计》期末考试试题 (六)
《Java程序设计》期末考试试题 (六)
 
第01章 绪论(java版)
第01章  绪论(java版)第01章  绪论(java版)
第01章 绪论(java版)
 

Plus de yiditushe

Spring入门纲要
Spring入门纲要Spring入门纲要
Spring入门纲要yiditushe
 
J Bpm4 1中文用户手册
J Bpm4 1中文用户手册J Bpm4 1中文用户手册
J Bpm4 1中文用户手册yiditushe
 
性能测试实践2
性能测试实践2性能测试实践2
性能测试实践2yiditushe
 
性能测试实践1
性能测试实践1性能测试实践1
性能测试实践1yiditushe
 
性能测试技术
性能测试技术性能测试技术
性能测试技术yiditushe
 
Load runner测试技术
Load runner测试技术Load runner测试技术
Load runner测试技术yiditushe
 
J2 ee性能测试
J2 ee性能测试J2 ee性能测试
J2 ee性能测试yiditushe
 
面向对象的Js培训
面向对象的Js培训面向对象的Js培训
面向对象的Js培训yiditushe
 
Flex3中文教程
Flex3中文教程Flex3中文教程
Flex3中文教程yiditushe
 
开放源代码的全文检索Lucene
开放源代码的全文检索Lucene开放源代码的全文检索Lucene
开放源代码的全文检索Luceneyiditushe
 
基于分词索引的全文检索技术介绍
基于分词索引的全文检索技术介绍基于分词索引的全文检索技术介绍
基于分词索引的全文检索技术介绍yiditushe
 
Lucene In Action
Lucene In ActionLucene In Action
Lucene In Actionyiditushe
 
Lucene2 4学习笔记1
Lucene2 4学习笔记1Lucene2 4学习笔记1
Lucene2 4学习笔记1yiditushe
 
Lucene2 4 Demo
Lucene2 4 DemoLucene2 4 Demo
Lucene2 4 Demoyiditushe
 
Lucene 全文检索实践
Lucene 全文检索实践Lucene 全文检索实践
Lucene 全文检索实践yiditushe
 
Lucene 3[1] 0 原理与代码分析
Lucene 3[1] 0 原理与代码分析Lucene 3[1] 0 原理与代码分析
Lucene 3[1] 0 原理与代码分析yiditushe
 
7 面向对象设计原则
7 面向对象设计原则7 面向对象设计原则
7 面向对象设计原则yiditushe
 
10 团队开发
10  团队开发10  团队开发
10 团队开发yiditushe
 
9 对象持久化与数据建模
9  对象持久化与数据建模9  对象持久化与数据建模
9 对象持久化与数据建模yiditushe
 
8 Uml构架建模
8  Uml构架建模8  Uml构架建模
8 Uml构架建模yiditushe
 

Plus de yiditushe (20)

Spring入门纲要
Spring入门纲要Spring入门纲要
Spring入门纲要
 
J Bpm4 1中文用户手册
J Bpm4 1中文用户手册J Bpm4 1中文用户手册
J Bpm4 1中文用户手册
 
性能测试实践2
性能测试实践2性能测试实践2
性能测试实践2
 
性能测试实践1
性能测试实践1性能测试实践1
性能测试实践1
 
性能测试技术
性能测试技术性能测试技术
性能测试技术
 
Load runner测试技术
Load runner测试技术Load runner测试技术
Load runner测试技术
 
J2 ee性能测试
J2 ee性能测试J2 ee性能测试
J2 ee性能测试
 
面向对象的Js培训
面向对象的Js培训面向对象的Js培训
面向对象的Js培训
 
Flex3中文教程
Flex3中文教程Flex3中文教程
Flex3中文教程
 
开放源代码的全文检索Lucene
开放源代码的全文检索Lucene开放源代码的全文检索Lucene
开放源代码的全文检索Lucene
 
基于分词索引的全文检索技术介绍
基于分词索引的全文检索技术介绍基于分词索引的全文检索技术介绍
基于分词索引的全文检索技术介绍
 
Lucene In Action
Lucene In ActionLucene In Action
Lucene In Action
 
Lucene2 4学习笔记1
Lucene2 4学习笔记1Lucene2 4学习笔记1
Lucene2 4学习笔记1
 
Lucene2 4 Demo
Lucene2 4 DemoLucene2 4 Demo
Lucene2 4 Demo
 
Lucene 全文检索实践
Lucene 全文检索实践Lucene 全文检索实践
Lucene 全文检索实践
 
Lucene 3[1] 0 原理与代码分析
Lucene 3[1] 0 原理与代码分析Lucene 3[1] 0 原理与代码分析
Lucene 3[1] 0 原理与代码分析
 
7 面向对象设计原则
7 面向对象设计原则7 面向对象设计原则
7 面向对象设计原则
 
10 团队开发
10  团队开发10  团队开发
10 团队开发
 
9 对象持久化与数据建模
9  对象持久化与数据建模9  对象持久化与数据建模
9 对象持久化与数据建模
 
8 Uml构架建模
8  Uml构架建模8  Uml构架建模
8 Uml构架建模
 

Java多线程编程详解

  • 1. Java 多线程编程 -1- 线程的同步 线 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问 程 题。Java 语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。 由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套 机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块。 1. synchronized 方法:通过在方法声明中加入 synchronized 关键字来声明 synchronized 方 法。如: public synchronized void accessVal(int newVal); synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法 都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到 从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同 一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态 (因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有 可能访问类成员变量的方法均被声明为 synchronized)。 在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。 synchronized 方法的缺陷:若将一个大的方法声明为 synchronized 将会大大影响效率,典型地, 若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此 将导致它对本类任何 synchronized 方法的调用都永远不会成功。 当然我们可以通过将访问类成员变量 的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。 2. synchronized 块:通过 synchronized 关键字来声明 synchronized 块。语法如下: synchronized(syncObject) { //允许访问控制的代码 } }许synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以 是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象, 故灵活性较高。 六:线程的阻塞 六 为了解决对共享存储区的访问冲突,Java 引入了同步机制,现在让我们来考察多个线程对共享资源 : 的访问,显然同步机制已经不够了,因为在任意时刻所要求的资源不一定已经准备好了被访问,反过来, 同一时刻准备好了的资源也可能不止一个。为了解决这种情况下的访问控制问题,Java 引入了对阻塞机 制的支持。 制 阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一 的 定已经很熟悉了。Java 提供了大量方法来支持阻塞,下面让我们逐一分析。 1. sleep() 方法:sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时 间内进入阻塞状态,不能得到 CPU 时间,指定的时间一过,线程重新进入可执行状态。 典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重 新测试,直到条件满足为止。 2. suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且 不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。典型地, suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后, 让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。 3. yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处 于可执行状态,随时可能再次分得 CPU 时间。 调用 yield() 的效果等价于调度程序认为该线程已执行 了足够的时间从而转到另一个线程。 4. wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形 式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被 调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。 被 初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。 调
  • 2. Java 多线程编程 -2- 区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方 法则相反。上述的核心区别导致了一系列的细节上的区别。 法 首先,前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于 Object 类,也就是说, 则 所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞 时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对 象上的锁被释放。 而调用 任意对象的 notify()方法则导致因调用该对象的 wait() 方法而阻塞的线程 中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。 中 其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或 随 块中调用,理由也很简单,只有在 synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同 样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方 法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的 对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现 IllegalMonitorStateException 异常。 异 wait() 和 notify() 方法的上述特性决定了它们经常和 synchronized 方法或块一起使用,将 常 它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchronized 方法或块提供了 类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则相当于 block 和 wakeup 原语(这一对方法均声明为 synchronized)。 它们的结合使得我们可以实现操作系统上一 系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。 关于 wait() 和 notify() 方法最后再说明两点: 方 第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中 法 随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产 生问题。 生 第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 问 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然, 只有获得锁的那一个线程才能进入可执行状态。 只 谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的 有 wait() 方法的调用都可能产生死锁。 遗憾的是,Java 并不在语言级别上支持死锁的避免,我们在编程 中必须小心地避免死锁。 中 以上我们对 Java 中实现线程阻塞的各种方法作了一番分析,我们重点分析了 wait() 和 必 notify() 方法,因为它们的功能最强大,使用也最灵活,但是这也导致了它们的效率较低,较容易出 错。实际使用中我们应该灵活使用各种方法,以便更好地达到我们的目的。 七:守护线程 七 守护线程是一类特殊的线程,它和普通线程的区别在于它并不是应用程序的核心部分,当一个应用程 : 序的所有非守护线程终止运行时,即使仍然有守护线程在运行,应用程序也将终止,反之,只要有一个 非守护线程在运行,应用程序就不会终止。守护线程一般被用于在后台为其它线程提供服务。 可以通过调用方法 isDaemon() 来判断一个线程是否是守护线程,也可以调用方法 setDaemon() 来 将一个线程设为守护线程。 八:线程组 八 线程组是一个 Java 特有的概念,在 Java 中,线程组是类 ThreadGroup 的对象,每个线程都隶 : 属于唯一一个线程组,这个线程组在线程创建时指定并在线程的整个生命期内都不能更改。你可以通过调 用包含 ThreadGroup 类型参数的 Thread 类构造函数来指定线程属的线程组,若没有指定,则线程 缺省地隶属于名为 system 的系统线程组。 的 在 Java 中,除了预建的系统线程组外,所有线程组都必须显式创建。 Java 中,除系统线程组外 系 在 的每个线程组又隶属于另一个线程组,你可以在创建线程组时指定其所隶属的线程组,若没有指定,则 缺省地隶属于系统线程组。这样,所有线程组组成了一棵以系统线程组为根的树。 缺 Java 允许我们对一个线程组中的所有线程同时进行操作,比如我们可以通过调用线程组的相应方法 省 来设置其中所有线程的优先级,也可以启动或阻塞其中的所有线程。 Java 的线程组机制的另一个重要作用是线程安全。线程组机制允许我们通过分组来区分有不同安全特性 的线程,对不同组的线程进行不同的处理,还可以通过线程组的分层结构来支持不对等安全措施的采用 。 Java 的 ThreadGroup 类提供了大量的方法来方便我们对线程组树中的每一个线程组以及线程组中的
  • 3. Java 多线程编程 -3- 每一个线程进行操作。 九:总结 九 在这一讲中,我们一起学习了 Java 多线程编程的方方面面,包括创建线程,以及对多个线程进行调 : 度、管理。我们深刻认识到了多线程编程的复杂性,以及线程切换开销带来的多线程程序的低效性,这也 促使我们认真地思考一个问题:我们是否需要多线程?何时需要多线程? 促 多线程的核心在于多个代码块并发执行,本质特点在于各代码块之间的代码是乱序执行的。 使 我们的程序 是否需要多线程,就是要看这是否也是它的内在特点。 是 假如我们的程序根本不要求多个代码块并发执行,那自然不需要使用多线程;假如我们的程序虽然要 否 求多个代码块并发执行,但是却不要求乱序,则我们完全可以用一个循环来简单高效地实现,也不需要 使用多线程;只有当它完全符合多线程的特点时,多线程机制对线程间通信和线程管理的强大支持才能 有用武之地,这时使用多线程才是值得的。   线程的(同步)控制   一个 Java 程序的多线程之间可以共享数据。当线程以异步方式访问共享数据时,有时候是不安全的 或者不和逻辑的。比如,同一时刻一个线程在读取数据,另外一个线程在处理数据,当处理数据的线程没 有等到读取数据的线程读取完毕就去处理数据,必然得到错误的处理结果。这和我们前面提到的读取数据 和处理数据并行多任务并不矛盾,这儿指的是处理数据的线程不能处理当前还没有读取结束的数据,但 是可以处理其它的数据。   如果我们采用多线程同步控制机制,等到第一个线程读取完数据,第二个线程才能处理该数据,就 会避免错误。可见,线程同步是多线程编程的一个相当重要的技术。   在讲线程的同步控制前我们需要交代如下概念:   1 用 Java 关键字 synchonized 同步对共享数据操作的方法   在一个对象中,用 synchonized 声明的方法为同步方法。 Java 中有一个同步模型-监视器,负责管 理线程对对象中的同步方法的访问,它的原理是:赋予该对象唯一一把'钥匙',当多个线程进入对象, 只有取得该对象钥匙的线程才可以访问同步方法,其它线程在该对象中等待,直到该线程用 wait()方法 放弃这把钥匙,其它等待的线程抢占该钥匙,抢占到钥匙的线程后才可得以执行,而没有取得钥匙的线 程仍被阻塞在该对象中等待。   file://声明同步的一种方式:将方法声明同步   class store    {  public synchonized void store_in()     {    ….    }     public synchonized void store_out(){ ….}     }   2 利用 wait()、notify()及 notifyAll()方法发送消息实现线程间的相互联系   Java 程序中多个线程通过消息来实现互动联系的,这几种方法实现了线程间的消息发送。例如定义 一个对象的 synchonized 方法,同一时刻只能够有一个线程访问该对象中的同步方法,其它线程被阻 塞。 通常可以用 notify()或 notifyAll()方法唤醒其它一个或所有线程。 而使用 wait()方法来使该线 程处于阻塞状态,等待其它的线程用 notify()唤醒。   一个实际的例子就是生产和销售,生产单元将产品生产出来放在仓库中,销售单元则从仓库中提走 产品,在这个过程中,销售单元必须在仓库中有产品时才能提货;如果仓库中没有产品,则销售单元必 须等待。   程序中,假如我们定义一个仓库类 store,该类的实例对象就相当于仓库,在 store 类中定义两个 成员方法:store_in(),用来模拟产品制造者往仓库中添加产品;strore_out()方法则用来模拟销 售者从仓库中取走产品。然后定义两个线程类:customer 类,其中的 run()方法通过调用仓库类中的 store_out()从仓库中取走产品,模拟销售者;另外一个线程类 producer 中的 run()方法通过调用 仓库类中的 store_in()方法向仓库添加产品,模拟产品制造者。 在主类中创建并启动线程,实现向仓库 中添加产品或取走产品。   如果仓库类中的 store_in() 和 store_out()方法不声明同步,这就是个一般的多线程,我们知 道,一个程序中的多线程是交替执行的,运行也是无序的,这样,就可能存在这样的问题:   仓库中没有产品了,销售者还在不断光顾,而且还不停的在'取'产品,这在现实中是不可思义的, 在程序中就表现为负值;如果将仓库类中的 stroe_in()和 store_out()方法声明同步,如上例所示:
  • 4. Java 多线程编程 -4- 就控制了同一时刻只能有一个线程访问仓库对象中的同步方法;即一个生产类线程访问被声明为同步的 store_in()方法时,其它线程将不能够访问对象中的 store_out()同步方法,当然也不能访问 store_in()方法。必须等到该线程调用 wait()方法放弃钥匙,其它线程才有机会访问同步方法。   这个原理实际中也很好理解,当生产者(producer)取得仓库唯一的钥匙,就向仓库中添放产品, 此时其它的销售者(customer,可以是一个或多个)不可能取得钥匙,只有当生产者添放产品结束,交 还钥匙并且通知销售者,不同的销售者根据取得钥匙的先后与否决定是否可以进入仓库中提走产品。 轻松使用线程: 不共享有时是最好的 利用 ThreadLocal 提高可伸缩性 ThreadLocal 类是悄悄地出现在 Java 平台版本 1.2 中的。虽然支持线程局部变量早就是许多线程 工具(例如 Posix pthreads 工具)的一部分,但 Java Threads API 的最初设计却没有这项有 用的功能。而且,最初的实现也相当低效。由于这些原因,ThreadLocal 极少受到关注,但对简化线程 安全并发程序的开发来说,它却是很方便的。在轻松使用线程的第 3 部分,Java 软件顾问 Brian Goetz 研究了 ThreadLocal 并提供了一些使用技巧。 参加 Brian 的多线程 Java 编程讨论论坛以获得您工程中的线程和并发问题的帮助。 编写线程安全类是困难的。它不但要求仔细分析在什么条件可以对变量进行读写,而且要求仔细分析其它 类能如何使用某个类。有时,要在不影响类的功能、易用性或性能的情况下使类成为线程安全的是很困难 的。有些类保留从一个方法调用到下一个方法调用的状态信息,要在实践中使这样的类成为线程安全的是 困难的。 管理非线程安全类的使用比试图使类成为线程安全的要更容易些。非线程安全类通常可以安全地在多线程 程序中使用,只要您能确保一个线程所用的类的实例不被其它线程使用。例如,JDBC Connection 类 是非线程安全的 ― 两个线程不能在小粒度级上安全地共享一个 Connection ― 但如果每个线程都有 它自己的 Connection,那么多个线程就可以同时安全地进行数据库操作。 不使用 ThreadLocal 为每个线程维护一个单独的 JDBC 连接(或任何其它对象)当然是可能的; Thread API 给了我们把对象和线程联系起来所需的所有工具。 ThreadLocal 则使我们能更容易地 而 把线程和它的每线程(per-thread)数据成功地联系起来。 什么是线程局部变量(thread-local variable)? 线程局部变量高效地为每个使用它的线程提供单独的线程局部变量值的副本。每个线程只能看到与自己相 联系的值,而不知道别的线程可能正在使用或修改它们自己的副本。一些编译器(例如 Microsoft Visual C++ 编译器或 IBM XL FORTRAN 编译器)用存储类别修饰符(像 static 或 volatile)把对线程局部变量的支持集成到了其语言中。Java 编译器对线程局部变量不提供特别的语 言支持;相反地,它用 ThreadLocal 类实现这些支持,核心 Thread 类中有这个类的特别支持。 因为线程局部变量是通过一个类来实现的,而不是作为 Java 语言本身的一部分,所以 Java 语言线 程局部变量的使用语法比内建线程局部变量语言的使用语法要笨拙一些。要创建一个线程局部变量,请实 例化类 ThreadLocal 的一个对象。 ThreadLocal 类的行为与 java.lang.ref 中的各种 Reference 类的行为很相似;ThreadLocal 类充当存储或检索一个值时的间接句柄。清单 1 显示了 ThreadLocal 接口。 清单 1. ThreadLocal 接口 public class ThreadLocal { public Object get(); public void set(Object newValue); public Object initialValue(); } get() 访问器检索变量的当前线程的值;set() 访问器修改当前线程的值。 initialValue() 方法是 可选的,如果线程未使用过某个变量,那么您可以用这个方法来设置这个变量的初始值;它允许延迟初 始化。用一个示例实现来说明 ThreadLocal 的工作方式是最好的方法。清单 2 显示了 ThreadLocal 的一个实现方式。它不是一个特别好的实现(虽然它与最初实现非常相似),所以很可能性能不佳,但它 清楚地说明了 ThreadLocal 的工作方式。 清单 2. ThreadLocal 的糟糕实现 public class ThreadLocal {
  • 5. Java 多线程编程 -5- private Map values = Collections.synchronizedMap(new HashMap()); public Object get() { Thread curThread = Thread.currentThread(); Object o = values.get(curThread); if (o == null && !values.containsKey(curThread)) { o = initialValue(); values.put(curThread, o); } return o; } public void set(Object newValue) { values.put(Thread.currentThread(), newValue); } public Object initialValue() { return null; } } 这个实现的性能不会很好,因为每个 get() 和 set() 操作都需要 values 映射表上的同步,而且 如果多个线程同时访问同一个 ThreadLocal,那么将发生争用。 此外,这个实现也是不切实际的,因为 用 Thread 对象做 values 映射表中的关键字将导致无法在线程退出后对 Thread 进行垃圾回收, 而且也无法对死线程的 ThreadLocal 的特定于线程的值进行垃圾回收。 用 ThreadLocal 实现每线程 Singleton 线程局部变量常被用来描绘有状态“单子”(Singleton) 或线程安全的共享对象,或者是通过把不 安全的整个变量封装进 ThreadLocal,或者是通过把对象的特定于线程的状态封装进 ThreadLocal。 例如,在与数据库有紧密联系的应用程序中,程序的很多方法可能都需要访问数据库。在系统的每个方法 中都包含一个 Connection 作为参数是不方便的 ― 用“单子”来访问连接可能是一个虽然更粗糙, 但却方便得多的技术。然而,多个线程不能安全地共享一个 JDBC Connection。如清单 3 所示,通过 使用“单子”中的 ThreadLocal,我们就能让我们的程序中的任何类容易地获取每线程 Connection 的一个引用。这样,我们可以认为 ThreadLocal 允许我们创建每线程单子。 清单 3. 把一个 JDBC 连接存储到一个每线程 Singleton 中 public class ConnectionDispenser { private static class ThreadLocalConnection extends ThreadLocal { public Object initialValue() { return DriverManager.getConnection(ConfigurationSingleton.getDbUrl()); } } private ThreadLocalConnection conn = new ThreadLocalConnection(); public static Connection getConnection() { return (Connection) conn.get(); } } 任何创建的花费比使用的花费相对昂贵些的有状态或非线程安全的对象,例如 JDBC Connection 或 正则表达式匹配器,都是可以使用每线程单子(singleton)技术的好地方。当然,在类似这样的地方, 您可以使用其它技术,例如用池,来安全地管理共享访问。然而,从可伸缩性角度看,即使是用池也存在 一些潜在缺陷。因为池实现必须使用同步,以维护池数据结构的完整性,如果所有线程使用同一个池,那 么在有很多线程频繁地对池进行访问的系统中,程序性能将因争用而降低。 用 ThreadLocal 简化调试日志纪录 其它适合使用 ThreadLocal 但用池却不能成为很好的替代技术的应用程序包括存储或累积每线程上下
  • 6. Java 多线程编程 -6- 文信息以备稍后检索之用这样的应用程序。例如,假设您想创建一个用于管理多线程应用程序调试信息的 工具。 您可以用如清单 4 所示的 DebugLogger 类作为线程局部容器来累积调试信息。 在一个工作单元 的开头,您清空容器,而当一个错误出现时,您查询该容器以检索这个工作单元迄今为止生成的所有调 试信息。 清单 4. 用 ThreadLocal 管理每线程调试日志 public class DebugLogger { private static class ThreadLocalList extends ThreadLocal { public Object initialValue() { return new ArrayList(); } public List getList() { return (List) super.get(); } } private ThreadLocalList list = new ThreadLocalList(); private static String[] stringArray = new String[0]; public void clear() { list.getList().clear(); } public void put(String text) { list.getList().add(text); } public String[] get() { return list.getList().toArray(stringArray); } } 在您的代码中,您可以调用 DebugLogger.put() 来保存您的程序正在做什么的信息,而且,稍后如 果有必要(例如发生了一个错误),您能够容易地检索与某个特定线程相关的调试信息。 与简单地把所 有信息转储到一个日志文件,然后努力找出哪个日志记录来自哪个线程(还要担心线程争用日志纪录对 象)相比,这种技术简便得多,也有效得多。 ThreadLocal 在基于 servlet 的应用程序或工作单元是一个整体请求的任何多线程应用程序服务器 中也是很有用的,因为在处理请求的整个过程中将要用到单个线程。您可以通过前面讲述的每线程单子技 术用 ThreadLocal 变量来存储各种每请求(per-request)上下文信息。 ThreadLocal 的线程安全性稍差的堂兄弟,InheritableThreadLocal ThreadLocal 类有一个亲戚,InheritableThreadLocal,它以相似的方式工作,但适用于种类完 全不同的应用程序。 创建一个线程时如果保存了所有 InheritableThreadLocal 对象的值,那么这些 值也将自动传递给子线程。如果一个子线程调用 InheritableThreadLocal 的 get(),那么它将与 它的父线程看到同一个对象。为保护线程安全性,您应该只对不可变对象(一旦创建,其状态就永远不会 被改变的对象)使用 InheritableThreadLocal,因为对象被多个线程共享 。 InheritableThreadLocal 很合适用于把数据从父线程传到子线程,例如用户标识(user id)或 事务标识(transaction id),但不能是有状态对象,例如 JDBC Connection。 ThreadLocal 的性能 虽然线程局部变量早已赫赫有名并被包括 Posix pthreads 规范在内的很多线程框架支持,但最初的 Java 线程设计中却省略了它,只是在 Java 平台的版本 1.2 中才添加上去。在很多方面, ThreadLocal 仍在发展之中;在版本 1.3 中它被重写,版本 1.4 中又重写了一次,两次都专门是 为了性能问题。 在 JDK 1.2 中,ThreadLocal 的实现方式与清单 2 中的方式非常相似,除了用同步 WeakHashMap 代替 HashMap 来存储 values 之外。(以一些额外的性能开销为代价,使用 WeakHashMap 解决了无法对 Thread 对象进行垃圾回收的问题。 )不用说,ThreadLocal 的性能是
  • 7. Java 多线程编程 -7- 相当差的。 Java 平台版本 1.3 提供的 ThreadLocal 版本已经尽量更好了;它不使用任何同步,从而不存在可 伸缩性问题,而且它也不使用弱引用。相反地,人们通过给 Thread 添加一个实例变量(该变量用于保 存当前线程的从线程局部变量到它的值的映射的 HashMap)来修改 Thread 类以支持 ThreadLocal。因为检索或设置一个线程局部变量的过程不涉及对可能被另一个线程读写的数据的读写 操作,所以您可以不用任何同步就实现 ThreadLocal.get() 和 set()。而且,因为每线程值的引用 被存储在自已的 Thread 对象中,所以当对 Thread 进行垃圾回收时,也能对该 Thread 的每线程 值进行垃圾回收。 不幸的是,即使有了这些改进,Java 1.3 中的 ThreadLocal 的性能仍然出奇地慢。 据我的粗略测量, 在双处理器 Linux 系统上的 Sun 1.3 JDK 中进行 ThreadLocal.get() 操作,所耗费的时间大 约是无争用同步的两倍。性能这么差的原因是 Thread.currentThread() 方法的花费非常大,占了 ThreadLocal.get() 运行时间的三分之二还多。虽然有这些缺点,JDK 1.3 ThreadLocal.get() 仍然比争用同步快得多,所以如果在任何存在严重争用的地方(可能是有非常多的线程,或者同步块被 频繁地执行,或者同步块很大),ThreadLocal 可能仍然要高效得多。 在 Java 平台的最新版本,即版本 1.4b2 中,ThreadLocal 和 Thread.currentThread() 的 性能都有了很大提高。有了这些提高,ThreadLocal 应该比其它技术,如用池,更快。由于它比其它技 术更简单,也更不易出错,人们最终将发现它是避免线程间出现不希望的交互的有效途径。 ThreadLocal 的好处 ThreadLocal 能带来很多好处。 它常常是把有状态类描绘成线程安全的,或者封装非线程安全类以使它 们能够在多线程环境中安全地使用的最容易的方式。使用 ThreadLocal 使我们可以绕过为实现线程安 全而对何时需要同步进行判断的复杂过程,而且因为它不需要任何同步,所以也改善了可伸缩性。除简单 之外,用 ThreadLocal 存储每线程单子或每线程上下文信息在归档方面还有一个颇有价值好处 ― 通 过使用 ThreadLocal,存储在 ThreadLocal 中的对象都是不被线程共享的是清晰的,从而简化了判 断一个类是否线程安全的工作。 我希望您从这个系列中得到了乐趣,也学到了知识,我也鼓励您到我的讨论论坛中来深入研究多线程问 题。 Java 多线程学习笔记 一、线程类   Java 是通过 Java.lang.Thread 类来实现多线程的,第个 Thread 对象描述了一个单独的线程。 要产生一个线程,有两种方法: 1、需要从 Java.lang.Thread 类继承一个新的线程类,重载它的 run()方法; 2、通过 Runnalbe 接口实现一个从非线程类继承来类的多线程,重载 Runnalbe 接口的 run()方 法。运行一个新的线程,只需要调用它的 start()方法即可。如: /**===================================================================== * 文件:ThreadDemo_01.java * 描述:产生一个新的线程 * ====================================================================== */ class ThreadDemo extends Thread{ Threads() { } Threads(String szName) { super(szName); } // 重载 run 函数
  • 8. Java 多线程编程 -8- public void run() { for (int count = 1,row = 1; row < 20; row++,count++) { for (int i = 0; i < count; i++) { System.out.print('*'); } System.out.println(); } } } class ThreadMain{ public static void main(String argv[]){ ThreadDemo th = new ThreadDemo(); // 调用 start()方法执行一个新的线程 th.start(); } }   线程类的一些常用方法:   sleep(): 强迫一个线程睡眠N毫秒。   isAlive(): 判断一个线程是否存活。   join(): 等待线程终止。   activeCount(): 程序中活跃的线程数。   enumerate(): 枚举程序中的线程。 currentThread(): 得到当前线程。   isDaemon(): 一个线程是否为守护线程。   setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依 赖于主线程结束而结束)   setName(): 为线程设置一个名称。   wait(): 强迫一个线程等待。   notify(): 通知一个线程继续运行。   setPriority(): 设置一个线程的优先级。 二、等待一个线程的结束   有些时候我们需要等待一个线程终止后再运行我们的另一个线程,这时我们应该怎么办呢?请看下 面的例子: /**===================================================================== * 文件:ThreadDemo_02.java * 描述:等待一个线程的结束 * ====================================================================== */ class ThreadDemo extends Thread{ Threads() { } Threads(String szName)
  • 9. Java 多线程编程 -9- { super(szName); } // 重载 run 函数 public void run() { for (int count = 1,row = 1; row < 20; row++,count++) { for (int i = 0; i < count; i++) { System.out.print('*'); } System.out.println(); } } } class ThreadMain{ public static void main(String argv[]){ //产生两个同样的线程 ThreadDemo th1 = new ThreadDemo(); ThreadDemo th2 = new ThreadDemo(); // 我们的目的是先运行第一个线程,再运行第二个线程 th1.start(); th2.start(); } } 这里我们的目标是要先运行第一个线程,等第一个线程终止后再运行第二个线程,而实际运行的结果是 如何的呢?实际上我们运行的结果并不是两个我们想要的直角三角形,而是一些乱七八糟的*号行,有 的长,有的短。为什么会这样呢?因为线程并没有按照我们的调用顺序来执行,而是产生了线程赛跑现象。 实际上 Java 并不能按我们的调用顺序来执行线程,这也说明了线程是并行执行的单独代码。如果要想得 到我们预期的结果,这里我们就需要判断第一个线程是否已经终止,如果已经终止,再来调用第二个线 程。代码如下: /**===================================================================== * 文件:ThreadDemo_03.java * 描述:等待一个线程的结束的两种方法 * ====================================================================== */ class ThreadDemo extends Thread{ Threads() { } Threads(String szName) { super(szName); }
  • 10. Java 多线程编程 - 10 - // 重载 run 函数 public void run() { for (int count = 1,row = 1; row < 20; row++,count++) { for (int i = 0; i < count; i++) { System.out.print('*'); } System.out.println(); } } } class ThreadMain{ public static void main(String argv[]){ ThreadMain test = new ThreadMain(); test.Method1(); // test.Method2(); } // 第一种方法:不断查询第一个线程是否已经终止,如果没有,则让主线程睡眠一直到它终止为止  // 即:while/isAlive/sleep  public void Method1(){ ThreadDemo th1 = new ThreadDemo(); ThreadDemo th2 = new ThreadDemo(); // 执行第一个线程 th1.start(); // 不断查询第一个线程的状态 while(th1.isAlive()){ try{ Thread.sleep(100); }catch(InterruptedException e){ } } //第一个线程终止,运行第二个线程 th2.start(); } // 第二种方法:join() public void Method2(){ ThreadDemo th1 = new ThreadDemo(); ThreadDemo th2 = new ThreadDemo(); // 执行第一个线程 th1.start(); try{ th1.join(); }catch(InterruptedException e){ } // 执行第二个线程
  • 11. Java 多线程编程 - 11 -   th2.start(); } 三、线程的同步问题 有些时候,我们需要很多个线程共享一段代码,比如一个私有成员或一个类中的静态成员,但是由于 线程赛跑的问题,所以我们得到的常常不是正确的输出结果,而相反常常是张冠李戴,与我们预期的结 果大不一样。看下面的例子: / **========================================================================== === * 文件:ThreadDemo_04.java * 描述:多线程不同步的原因 * ============================================================================ = */ // 共享一个静态数据对象 class ShareData{ public static String szData = ""; } class ThreadDemo extends Thread{ private ShareData oShare; ThreadDemo(){ } ThreadDemo(String szName,ShareData oShare){ super(szName); this.oShare = oShare; } public void run(){ // 为了更清楚地看到不正确的结果,这里放一个大的循环   for (int i = 0; i < 50; i++){ if (this.getName().equals("Thread1")){ oShare.szData = "这是第 1 个线程"; // 为了演示产生的问题,这里设置一次睡眠      try{ Thread.sleep((int)Math.random() * 100); catch(InterruptedException e){ } // 输出结果 System.out.println(this.getName() + ":" + oShare.szData); }else if (this.getName().equals("Thread2")){ oShare.szData = "这是第 1 个线程"; // 为了演示产生的问题,这里设置一次睡眠      try{ Thread.sleep((int)Math.random() * 100); catch(InterruptedException e){
  • 12. Java 多线程编程 - 12 - } // 输出结果 System.out.println(this.getName() + ":" + oShare.szData); } } } class ThreadMain{ public static void main(String argv[]){ ShareData oShare = new ShareData(); ThreadDemo th1 = new ThreadDemo("Thread1",oShare); ThreadDemo th2 = new ThreadDemo("Thread2",oShare); th1.start(); th2.start(); } }   由于线程的赛跑问题,所以输出的结果往往是 Thread1 对应“这是第 2 个线程”,这样与我们要 输出的结果是不同的。 为了解决这种问题(错误),Java 为我们提供了“锁”的机制来实现线程的同步。 锁的机制要求每个线程在进入共享代码之前都要取得锁,否则不能进入,而退出共享代码之前则释放该 锁,这样就防止了几个或多个线程竞争共享代码的情况,从而解决了线程的不同步的问题。可以这样说, 在运行共享代码时则是最多只有一个线程进入,也就是和我们说的垄断。锁机制的实现方法,则是在共享 代码之前加入 synchronized 段,把共享代码包含在 synchronized 段中。上述问题的解决方法为: / **========================================================================== === * 文件:ThreadDemo_05.java * 描述:多线程不同步的解决方法--锁 * ============================================================================ = */ // 共享一个静态数据对象 class ShareData{ public static String szData = ""; } class ThreadDemo extends Thread{ private ShareData oShare; ThreadDemo(){ } ThreadDemo(String szName,ShareData oShare){ super(szName); this.oShare = oShare; }
  • 13. Java 多线程编程 - 13 - public void run(){ // 为了更清楚地看到不正确的结果,这里放一个大的循环   for (int i = 0; i < 50; i++){ if (this.getName().equals("Thread1")){ // 锁定 oShare 共享对象 synchronized (oShare){ oShare.szData = "这是第 1 个线程"; // 为了演示产生的问题,这里设置一次睡眠       try{ Thread.sleep((int)Math.random() * 100); catch(InterruptedException e){ } // 输出结果 System.out.println(this.getName() + ":" + oShare.szData); } }else if (this.getName().equals("Thread2")){ // 锁定共享对象 synchronized (oShare){ oShare.szData = "这是第 1 个线程"; // 为了演示产生的问题,这里设置一次睡眠       try{ Thread.sleep((int)Math.random() * 100); catch(InterruptedException e){ } // 输出结果 System.out.println(this.getName() + ":" + oShare.szData); } } } } class ThreadMain{ public static void main(String argv[]){ ShareData oShare = new ShareData(); ThreadDemo th1 = new ThreadDemo("Thread1",oShare); ThreadDemo th2 = new ThreadDemo("Thread2",oShare); th1.start(); th2.start(); } }   由于过多的 synchronized 段将会影响程序的运行效率,因此引入了同步方法,同步方法的实现则 是将共享代码单独写在一个方法里,在方法前加上 synchronized 关键字即可。   在线程同步时的两个需要注意的问题:   1、无同步问题:即由于两个或多个线程在进入共享代码前,得到了不同的锁而都进入共享代码而造 成。   2、死锁问题:即由于两个或多个线程都无法得到相应的锁而造成的两个线程都等待的现象。这种现 象主要是因为相互嵌套的 synchronized 代码段而造成,因此,在程序中尽可能少用嵌套的 synchronized 代码段是防止线程死锁的好方法。
  • 14. Java 多线程编程 - 14 -   在写上面的代码遇到的一个没能解决的问题,在这里拿出来,希望大家讨论是什么原因。 / **========================================================================== === * 文件:ThreadDemo_0 6.java * 描述:为什么造成线程的不同步。 * ============================================================================ = */ class ThreadDemo extends Thread{ //共享一个静态数据成员 private static String szShareData = ""; ThreadDemo(){ } ThreadDemo(String szName){ super(szName); } public void run(){ // 为了更清楚地看到不正确的结果,这里放一个大的循环   for (int i = 0; i < 50; i++){ if (this.getName().equals("Thread1")){      synchronized(szShareData){ szShareData = "这是第 1 个线程"; // 为了演示产生的问题,这里设置一次睡眠      try{ Thread.sleep((int)Math.random() * 100); catch(InterruptedException e){ } // 输出结果 System.out.println(this.getName() + ":" + szShareData); } }else if (this.getName().equals("Thread2")){ synchronized(szShareData){ szShareData = "这是第 1 个线程"; // 为了演示产生的问题,这里设置一次睡眠       try{ Thread.sleep((int)Math.random() * 100); catch(InterruptedException e){ } // 输出结果 System.out.println(this.getName() + ":" + szShareData); } } }
  • 15. Java 多线程编程 - 15 - } class ThreadMain{ public static void main(String argv[]){ ThreadDemo th1 = new ThreadDemo("Thread1"); ThreadDemo th2 = new ThreadDemo("Thread2"); th1.start(); th2.start(); } }   这段代码的共享成员是一个类中的静态成员,按理说,这里进入共享代码时得到的锁应该是同样的 锁,而实际上以上程序的输入却是不同步的,为什么呢?? Java 多线程学习笔记(二) 四、Java 的等待通知机制   在有些时候,我们需要在几个或多个线程中按照一定的秩序来共享一定的资源。例如生产者--消费 者的关系,在这一对关系中实际情况总是先有生产者生产了产品后,消费者才有可能消费;又如在父- -子关系中,总是先有父亲,然后才能有儿子。然而在没有引入等待通知机制前,我们得到的情况却常常 是错误的。这里我引入《用线程获得强大的功能》一文中的生产者--消费者的例子: /* ============================================================================ ====== * 文件:ThreadDemo07.java * 描述:生产者--消费者 * 注:其中的一些注释是我根据自己的理解加注的 * ============================================================================ ====== */ // 共享的数据对象 class ShareData{ private char c; public void setShareChar(char c){ this.c = c; } public char getShareChar(){ return this.c; } } // 生产者线程 class Producer extends Thread{ private ShareData s;
  • 16. Java 多线程编程 - 16 - Producer(ShareData s){ this.s = s; } public void run(){ for (char ch = 'A'; ch <= 'Z'; ch++){ try{ Thread.sleep((int)Math.random() * 4000); }catch(InterruptedException e){} // 生产 s.setShareChar(ch); System.out.println(ch + " producer by producer."); } } } // 消费者线程 class Consumer extends Thread{ private ShareData s; Consumer(ShareData s){ this.s = s; } public void run(){ char ch; do{ try{ Thread.sleep((int)Math.random() * 4000); }catch(InterruptedException e){} // 消费 ch = s.getShareChar(); System.out.println(ch + " consumer by consumer."); }while(ch != 'Z'); } } class Test{ public static void main(String argv[]){ ShareData s = new ShareData(); new Consumer(s).start(); new Producer(s).start(); } }   在以上的程序中,模拟了生产者和消费者的关系,生产者在一个循环中不断生产了从A-Z的共享 数据,而消费者则不断地消费生产者生产的A-Z的共享数据。我们开始已经说过,在这一对关系中,必
  • 17. Java 多线程编程 - 17 - 须先有生产者生产,才能有消费者消费。但如果运行我们上面这个程序,结果却出现了在生产者没有生产 之前,消费都就已经开始消费了或者是生产者生产了却未能被消费者消费这种反常现象。为了解决这一问 题,引入了等待通知(wait/notify)机制如下:   1、在生产者没有生产之前,通知消费者等待;在生产者生产之后,马上通知消费者消费。   2、在消费者消费了之后,通知生产者已经消费完,需要生产。 下面修改以上的例子(源自《用线程获得强大的功能》一文): /* ============================================================================ ====== * 文件:ThreadDemo08.java * 描述:生产者--消费者 * 注:其中的一些注释是我根据自己的理解加注的 * ============================================================================ ====== */ class ShareData{ private char c; // 通知变量 private boolean writeable = true; // ------------------------------------------------------------------------- // 需要注意的是:在调用 wait()方法时,需要把它放到一个同步段里,否则将会出现 // "java.lang.IllegalMonitorStateException: current thread not owner"的异常。 // ------------------------------------------------------------------------- public synchronized void setShareChar(char c){ if (!writeable){ try{ // 未消费等待 wait(); }catch(InterruptedException e){} } this.c = c; // 标记已经生产 writeable = false; // 通知消费者已经生产,可以消费 notify(); } public synchronized char getShareChar(){ if (writeable){ try{ // 未生产等待 wait();
  • 18. Java 多线程编程 - 18 - }catch(InterruptedException e){} } // 标记已经消费 writeable = true; // 通知需要生产 notify(); return this.c; } } // 生产者线程 class Producer extends Thread{ private ShareData s; Producer(ShareData s){ this.s = s; } public void run(){ for (char ch = 'A'; ch <= 'Z'; ch++){ try{ Thread.sleep((int)Math.random() * 400); }catch(InterruptedException e){} s.setShareChar(ch); System.out.println(ch + " producer by producer."); } } } // 消费者线程 class Consumer extends Thread{ private ShareData s; Consumer(ShareData s){ this.s = s; } public void run(){ char ch; do{ try{ Thread.sleep((int)Math.random() * 400); }catch(InterruptedException e){} ch = s.getShareChar(); System.out.println(ch + " consumer by consumer.**");
  • 19. Java 多线程编程 - 19 - }while (ch != 'Z'); } } class Test{ public static void main(String argv[]){ ShareData s = new ShareData(); new Consumer(s).start(); new Producer(s).start(); } }   在以上程序中,设置了一个通知变量,每次在生产者生产和消费者消费之前,都测试通知变量,检 查是否可以生产或消费。最开始设置通知变量为 true,表示还未生产,在这时候,消费者需要消费,于 时修改了通知变量,调用 notify()发出通知。这时由于生产者得到通知,生产出第一个产品,修改通知 变量,向消费者发出通知。 这时如果生产者想要继续生产,但因为检测到通知变量为 false,得知消费者 还没有生产,所以调用 wait()进入等待状态。因此,最后的结果,是生产者每生产一个,就通知消费者 消费一个;消费者每消费一个,就通知生产者生产一个,所以不会出现未生产就消费或生产过剩的情况。 五、线程的中断   在很多时候,我们需要在一个线程中调控另一个线程,这时我们就要用到线程的中断。用最简单的话 也许可以说它就相当于播放机中的暂停一样,当第一次按下暂停时,播放器停止播放,再一次按下暂停 时,继续从刚才暂停的地方开始重新播放。 而在 Java 中,这个暂停按钮就是 Interrupt()方法。在第一 次调用 interrupt()方法时,线程中断;当再一次调用 interrupt()方法时,线程继续运行直到终止。 这里依然引用《用线程获得强大功能》一文中的程序片断,但为了更方便看到中断的过程,我在原程序的 基础上作了些改进,程序如下: /* ============================================================================ ======= * 文件:ThreadDemo09.java * 描述:线程的中断 * ============================================================================ ======= */ class ThreadA extends Thread{ private Thread thdOther; ThreadA(Thread thdOther){ this.thdOther = thdOther; } public void run(){ System.out.println(getName() + " 运行..."); int sleepTime = (int)(Math.random() * 10000); System.out.println(getName() + " 睡眠 " + sleepTime
  • 20. Java 多线程编程 - 20 - + " 毫秒。"); try{ Thread.sleep(sleepTime); }catch(InterruptedException e){} System.out.println(getName() + " 觉醒,即将中断线程 B。"); // 中断线程 B,线程B暂停运行 thdOther.interrupt(); } } class ThreadB extends Thread{ int count = 0; public void run(){ System.out.println(getName() + " 运行..."); while (!this.isInterrupted()){ System.out.println(getName() + " 运行中 " + count++); try{ Thread.sleep(10); }catch(InterruptedException e){ int sleepTime = (int)(Math.random() * 10000); System.out.println(getName() + " 睡眠" + sleepTime + " 毫秒。觉醒后立即运行直到终止。"); try{ Thread.sleep(sleepTime); }catch(InterruptedException m){} System.out.println(getName() + " 已经觉醒,运行终止..."); // 重新设置标记,继续运行 this.interrupt(); } } System.out.println(getName() + " 终止。"); } } class Test{ public static void main(String argv[]){ ThreadB thdb = new ThreadB(); thdb.setName("ThreadB"); ThreadA thda = new ThreadA(thdb); thda.setName("ThreadA");
  • 21. Java 多线程编程 - 21 - thdb.start(); thda.start(); } }   运行以上程序,你可以清楚地看到中断的过程。首先线程B开始运行,接着运行线程A,在线程A睡 眠一段时间觉醒后,调用 interrupt()方法中断线程B,此是可能B正在睡眠,觉醒后掏出一个 InterruptedException 异常,执行其中的语句,为了更清楚地看到线程的中断恢复,我在 InterruptedException 异常后增加了一次睡眠,当睡眠结束后,线程B调用自身的 interrupt() 方法恢复中断,这时测试 isInterrupt()返回 true,线程退出。 线程和进程(Threads and Processes) 第一个关键的系统级概念,究竟什么是线程或者说究竟什么是进程?她们其实就是操作系统内部的一种 数据结构。 进程数据结构掌握着所有与内存相关的东西:全局地址空间、文件句柄等等诸如此类的东西。当一个进程 放弃执行(准确的说是放弃占有 CPU),而被操作系统交换到硬盘上,使别的进程有机会运行的时候, 在那个进程里的所有数据也将被写到硬盘上,甚至包括整个系统的核心(core memory)。可以这么说, 当你想到进程(process),就应该想到内存(memory) (进程 == 内存)。如上所述,切换进程的 代价非常大,总有那么一大堆的内存要移来移去。你必须用秒这个单位来计量进程切换(上下文切换), 对于用户来说秒意味着明显的等待和硬盘灯的狂闪(对于作者的我,就意味着 IBM 龙腾 3 代的烂掉, 5555555)。 言归正传,对于 Java 而言,JVM 就几乎相当于一个进程(process),因为只有进程才能 拥有堆内存(heap,也就是我们平时用 new 操作符,分出来的内存空间)。 那么线程是什么呢?你可以把它看成“一段代码的执行”---- 也就是一系列由 JVM 执行的二进制指令。 这里面没有对象(Object)甚至没有方法(Method)的概念。指令执行的序列可以重叠,并且并行的执 行。后面,我会更加详细的论述这个问题。但是请记住,线程是有序的指令,而不是方法(method)。 线程的数据结构,与进程相反,仅仅只包括执行这些指令的信息。它包含当前的运行上下文 (context):如寄存器(register)的内容、当前指令的在运行引擎的指令流中的位置、保存方法 (methods)本地参数和变量的运行时堆栈。如果发生线程切换,OS 只需把寄存器的值压进栈,然后把 线程包含的数据结构放到某个类是列表(LIST)的地方;把另一个线程的数据从列表中取出,并且用栈里 的值重新设置寄存器。 切换线程更加有效率,时间单位是毫秒。 对于 Java 而言,一个线程可以看作是 JVM 的一个状态。 运行时堆栈(也就是前面说的存储本地变量和参数的地方)是线程数据结构一部分。这是因为多个线程, 每一个都有自己的运行时堆栈,也就是说存储在这里面的数据是绝对线程安全(后面将会详细解释这个 概念)的。因为可以肯定一个线程是无法修改另一个线程的系统级的数据结构的。也可以这么说一个不访 问堆内存的(只读写堆栈内存)方法,是线程安全的(Thread Safe)。 线程安全和同步 线程安全,是指一个方法(method)可以在多线程的环境下安全的有效的访问进程级的数据(这些数据 是与其他线程共享的)。事实上,线程安全是个很难达到的目标。 线程安全的核心概念就是同步,它保证多个线程: 同时开始执行,并行运行 不同时访问相同的对象实例 不同时执行同一段代码 我将会在后面的章节,一一细诉这些问题。但现在还是让我们来看看同步的一种经典的 实现方法——信号量。 信号量是任何可以让两个线程为了同步它们的操作而相互通信的对象。 Java 也是通 过信号量来实现线程间通信的。 不要被微软的文档所暗示的信号量仅仅是 Dijksta 提出的计数型信号量所迷惑。信号量其实包含任何可 以用来同步的对象。 如果没有 synchronized 关键字,就无法用 JAVA 实现信号量,但是仅仅只依靠它也不足够。我将会在 后面为大家演示一种用 Java 实现的信号量。 同步的代价很高哟! 同步(或者说信号量,随你喜欢啦)的一个很让人头痛的问题就是代价。 考虑一下,下面的代码:
  • 22. Java 多线程编程 - 22 - Listing 1.2: import java.util.*; import java.text.NumberFormat; class Synch { private static long[ ] locking_time = new long[100]; private static long[ ] not_locking_time = new long[100]; private static final long ITERATIONS = 10000000; synchronized long locking (long a, long b){return a + b;} long not_locking (long a, long b){return a + b;} private void test( int id ) { long start = System.currentTimeMillis(); for(long i = ITERATIONS; --i >= 0 ;) { locking(i,i); } locking_time[id] = System.currentTimeMillis() - start; start = System.currentTimeMillis(); for(long i = ITERATIONS; --i >= 0 ;) { not_locking(i,i); } not_locking_time[id] = System.currentTimeMillis() - start; } static void print_results( int id ) { NumberFormat compositor = NumberFormat.getInstance(); compositor.setMaximumFractionDigits( 2 ); double time_in_synchronization = locking_time[id] - not_locking_time[id]; System.out.println( "Pass " + id + ": Time lost: " + compositor.format( time_in_synchronization ) + " ms. " + compositor.format( ((double)locking_time[id]/not_locking_time[id])*100.0 ) + "% increase" ); } static public void main(String[ ] args) throws InterruptedException { final Synch tester = new Synch(); tester.test(0); print_results(0); tester.test(1); print_results(1); tester.test(2); print_results(2); tester.test(3); print_results(3); tester.test(4); print_results(4); tester.test(5); print_results(5); tester.test(6); print_results(6); final Object start_gate = new Object(); Thread t1 = new Thread() { public void run() { try{ synchronized(start_gate) { start_gate.wait(); } }
  • 23. Java 多线程编程 - 23 - catch( InterruptedException e ){} tester.test(7); } }; Thread t2 = new Thread() { public void run() { try{ synchronized(start_gate) { start_gate.wait(); } } catch( InterruptedException e ){} tester.test(8); } }; Thread.currentThread().setPriority( Thread.MIN_PRIORITY ); t1.start(); t2.start(); synchronized(start_gate){ start_gate.notifyAll(); } t1.join(); t2.join(); print_results( 7 ); print_results( 8 ); } } 这是一个简单的基准测试程序,她清楚的向大家揭示了同步的代价是多么的大。 test(…)方法调用 2 个方法 1,000,000,0 次。 其中一个是同步的,另一个则否。 下面是在我的机器上输出的结果(CPU: P4 2.4G(B); Memory: 1GB; OS: windows 2000 server(sp3); JDK: Ver1.4.01 and HotSpot 1.4.01-b01): C:>java -verbose:gc Synch Pass 0: Time lost: 251 ms. 727.5% increase Pass 1: Time lost: 250 ms. 725% increase Pass 2: Time lost: 251 ms. 602% increase Pass 3: Time lost: 250 ms. 725% increase Pass 4: Time lost: 261 ms. 752.5% increase Pass 5: Time lost: 260 ms. 750% increase Pass 6: Time lost: 261 ms. 752.5% increase Pass 7: Time lost: 1,953 ms. 1,248.82% increase Pass 8: Time lost: 3,475 ms. 8,787.5% increase 这里为了使 HotSpot JVM 充分的发挥其威力,test( )方法被多次反复调用。 一旦这段程序被彻底优化 以后,也就是大约在 Pass 6 时,同步的代价达到最大。 Pass 7 和 Pass 8 与前面的区别在于,我 new 了两个新的线程来并行执行 test 方法,两个线程竞争执行(后面是适当的地方,我会解释什么是“竞争 ”,如果你已经等不及了,买本大学的操作系统课本看看吧! J),这使结果更加接近真实。同步的代价 是如此之高的,应该尽量避免无谓的同步代价。 现在是时候我们更深入的讨论一下同步的代价了。HotSpot JVM 一般会使用一到两个方法来实现同步, 这主要取决于是否存在线程的竞争。当没有竞争的时候,计算机的汇编指令顺序的执行,这些指令的执行 是不被打断。指令试图测试一个比特(bit),然后设置各种二进制位来表示测试的结果,如果这个 bit 没有被设置,指令就设置它。这可以说是非常原始的信号量,因为当两个线程同步的企图设置一个 bit 的 值时,只有一个线程可以成功,两个线程都会检查结果,看看是不是自己设成功了。 如果 bit 已经被设置(这里说的是有线程竞争的情况下),失败的 JVM(线程)不得不离开操作系统的 核心进程等待这个 bit 位被清零。这样来回的在系统核心中切换是非常耗时的。在 NT 系统下,需要 600 次机械指令循环来进入一次系统内核,这还仅仅是进入所耗费的时间还不包括做操作的时间。 是不是觉得很无聊了,呵呵!今天似乎都是些不顶用的东西。但这是必须的,为了使你能够读懂后
  • 24. Java 多线程编程 - 24 - 面的内容。下一篇,我将会谈到一些更有趣的话题,例如如何避免同步,如果大家不反对,我还想讲一些 设计模式的东西。下回见! 避免同步 大部分显示的同步都可以避免。一般不操作对象状态信息(例如数据成员)的方法都不需要同步,例如: 一些方法只访问本地变量(也就是说在方法内部声明的变量),而不操作类级别的数据成员,并且这些 方法不会通过传入的引用参数来修改外部的对象。符合这些条件的方法都不需要使用 synchronization 这种重量级的操作。除此之外,还可以使用一些设计模式(Design Pattern)来避免同步(我将会在 后面提到)。 你甚至可以通过适当的组织你的代码来避免同步。相对于同步的一个重要的概念就是原子性。一个原子性 的操作事不能被其他线程中断的,通常的原子性操作是不需要同步的。 Java 定义一些原子性的操作。一般的给变量付值的操作是原子的,除了 long 和 double。看下面的代码: class Unreliable { private long x; public long get_x( ) {return x;} public void set_x(long value) { x = value; } } 线程 1 调用: obj.set_x( 0 ); 线程 2 调用: obj.set_x( 0x123456789abcdef ) 问题在于下面这行代码: x = value; JVM 为了效率的问题,并没有把 x 当作一个 64 位的长整型数来使用,而是把它分为两个 32-bit,分别 付值: x.hgh_word = value.high_word; x.low_word = value.low_word; 因此,存在一个线程设置了高位之后被另一个线程切换出去,而改变了其高位或低位的值。所以,x 的值最终可能为 0x0123456789abcdef、0x01234567000000、0x00000000abcdef 和 0x00000000000000。你根本无法确定它的值,唯一的解决方法是,为 set_x( )和 get_x()方法加 上 synchronized 这个关键字或者把这个付值操作封装在一个确保原子性的代码段里。 所以,在操作的 long 型数据的时候,千万不要想当然。强迫自己记住吧:只有直接付值操作是原子的 (除了上面的例子)。其它,任何表达式,象 x = ++y、x += y 都是不安全的,不管 x 或 y 的数据类型 是否是小于 64 位的。很可能在付值之前,自增之后,被其它线程抢先了(preempted)。 竞争条件 在术语中,对于前面我提到的多线程问题——两个线程同步操作同一个对象,使这个对象的最 终状态不明——叫做竞争条件。竞争条件可以在任何应该由程序员保证原子操作的,而又忘记使用 synchronized 的地方。在这个意义上,可以把 synchronized 看作一种保证复杂的、顺序一定的操作 具有原子性的工具,例如给一个 boolean 值变量付值,就是一个隐式的同步操作。 不变性 一种有效的语言级的避免同步的方法就是不变性(immutability)。 一个自从产生那一刻起就无法再改 变的对象就是不变性对象,例如一个 String 对象。但是要注意类似这样的表达式:string1 += string2;本质上等同于 string1 = string1 + string2;其实第三个包含 string1 和 string2 的 string 对象被隐式的产生,最后,把 string1 的引用指向第三个 string。这样的操作,并不是原 子的。 由于不变对象的值无法发生改变,所以可以为多个线程安全的同步操作,不需要 synchronized。 把一个类的所有数据成员都声明为 final 就可以创建一个不变类型了。那些被声明为 final 的数据成员 并不是必须在声明的时候就写死,但必须在类的构造函数中,全部明确的初始化。例如: Class I_am_immutable {
  • 25. Java 多线程编程 - 25 - private final int MAX_VALUE = 10; priate final int blank_final; public I_am_immutable( int_initial_value ) { blank_final = initial_value; } } 一个由构造函数进行初始化的 final 型变量叫做 blank final。一般的,如果你频繁的只读访问一个 对象,把它声明成一个不变对象是个保证同步的好办法,而且可以提高 JVM 的效率,因为 HotSpot 会把 它放到堆栈里以供使用。 同步封装器(Synchronization Wrappers) 同步还是不同步,是问题的所在。让我们跳出这样的思维模式吧,世事无绝对。有什么办法可以 使你的类灵活的在同步与不同步之间切换呢? 有一个非常好的现成例子,就是新近引入 JAVA 的 Collection 框架,它是用来取代原本散乱的、繁重的 Vector 等类型。Vector 的任何方法都是同步的, 这就是为什么说它繁重了。而对于 collections 对象,在需要保证同步的时候,一般会由访问它方法来 保证同步,因此没有必要两次锁定(一次是锁定包含使用 collection 对象的方法的对象,一次是锁定 collection 对象自身)。Java 的解决方案是使用同步封装器。其基本原理来自四人帮(Gang-of- Four)的 Decorator 模式,一个 Decorator 自身就实现某个接口,而且又包含了实现同样接口的数 据成员,但是在通过外部类方法调用内部成员的相同方法的时候,控制或者修改传入的变量。java.io 这个包里的所有类都是 Decorator:一个 BufferedInputStream 既实现了虚类 InputStream 的所 有方法,又包含了一个 InputStream 引用所指向的成员变量。程序员调用外部容器类的方法,实际上是 变相的调用内部对象的方法。 我们可以利用这种设计模式。来实现灵活的同步方法。如下例: Interface Some_interface {Object message( ); } class Not_thread_safe implements Some_interface {public Object message( ) { //实现该方法的代码,省~~~~~~~~~~~ return null; } } class Thread_safe_wrapper implements Some_interface {Some_interface not_thread_safe; public Thread_safe_wrapper(Some_interface not_thread_safe) { this.not_thread_safe = not_thread_safe;} public Some_interface extract( ) { return not_thread_safe;} public synchronized Object message( ) { return not_thread_safe.message( );} } 当不存在线程安全的时候,你可以直接使用 Not_thread_safe 类对象。当需要考虑线程安全的时候, 只需要把它包装一下: Some_interface object = new Not_thread_safe( ); …………… object = new Thread_safe_wrapper(object); //object 现在变成线程安全了 当你不需要考虑线程安全的时候,你可以还原 object 对象: object = ((Thread_safe_Wrapper)object).extract( ); 下一回,我们又要深入底层机制了。呵呵!千万不要闷着大家呀!下回见! 线程的并发性 下一个与 OS 平台相关的问题(这也是编写与平台无关的 Java 程序要面对的问题)是必须确定
  • 26. Java 多线程编程 - 26 - 并发性和并行在该平台的定义。并发的多线程系统总会给人多个任务同时运行的感觉,其实这些任务是被 分割为许多的块交错在一起执行的。在一个并行的系统中,两个任务实际上是同时(这里的同时是真正的 同时,而不是快速交错执行所产生的并行假象)运行的,这就要求有多个 CPU。如图 1.1: 多线程其实并不能加快的程序速度。如果你的程序并不需要频繁的等待 IO 操作完成,那么多线程程 序还会比单线程程序更慢些。但在多 CPU 系统下则反之。 Java 线程系统非平台独立的主要原因就是要实现彻底的平行运行的线程,如果不使用 OS 提供 的系统线程模型,是不可能的。对于 JAVA 而言,在理论上,允许由 JVM 来模仿整个线程系统,从而避免 我在前一篇文章(驯服 JAVA 线程 2)中,所提到的进入 OS 核心的时间消耗。但是,这样也排除了程序中 的并行性,因为如果不使用任何操作系统级的线程(这样是为了保持平台独立性),OS 会把 JVM 的实例 当成一个单线程的程序来看待,也就只会分配单个 CPU 来执行它,从而导致就算运行在多 CPU 的机器上, 而且只有一个 JVM 实例在单独运行,也不可能出现两个 Java 线程真正的并行运行(充分的利用两个 CPU)。 所以,要真正的实现并行运行,只有存在两个 JVM 实例,分别运行不同的程序。做的再好一点 就是让 JVM 把 Java 的线程映射到 OS 级的线程上去(一个 Java 线程就是一个系统的线程,让系统进行 调配,充分发挥系统对资源的操控能力,这样就不存在只能在一个 CPU 上运行的问题了)。 不幸的是,不 同的操作系统实现的线程机制也不同,而且这些区别已经到了在编程时不能忽视的地步了。 由于平台不同而导致的问题 下面,我将会通过比较 Solaris 和 WindowsNT 对线程机制实现不同之处,来说明前面提到的问题。 Java,在理论上,至少有 10 个线程优先等级划分(如果有两个或两个以上的线程都处在 on ready 状 态下,那么拥有高优先级的线程将会先执行)。在 Solaris 里,支持 231 个优先等级,当然对于支持 Java 那 10 个的等级是没问题的。 在 NT 下,最多只有 7 优先级划分,却必须映射到 Java 的 10 个等级。这就会出现很多的可能性(可能 Java 里面的优先级 1、 就等同于 NT 里的优先级 1,优先级 8、 、 则等于 NT 里的 7 级,还有很多的可 2 9 10 能性)。因此,在 NT 下依靠优先级来调度线程时存在很多问题。 更不幸的还在后面呢!NT 下的线程优先级竟然还不是固定的!这就更加复杂了!NT 提供了一个名叫优先 级助推(Priority Boosting)的机制。这个机制使程序员可以通过调用一个 C 语言的系统 Call(Windows NT/2000/XP: You can disable the priority-boosting feature by calling the SetProcessPriorityBoost or SetThreadPriorityBoost function. To determine whether this feature has been disabled, call the GetProcessPriorityBoost or GetThreadPriorityBoost function.)来改变线程优先级, 但 Java 不能这样做。当打开了 Priority Boosting 功能的时候,NT 依据线程每次执行 I/O 相关的系 统调用的大概时间来提高该线程的优先级。在实践中,这意味着一个线程的优先级可能高过你的想象,因 为这个线程碰巧在一个繁忙的时刻进行了一次 I/O 操作。 线程优先级助推机制的目的是为了防止一个后台 进程(或线程)影响了前台的 UI 显示进程。其它的操作系统同样有着复杂的算法来降低后台进程的优先 级。这个机制的一个严重的副作用就是使我们无法通过优先级来判断即将运行的就绪太线程。 在这种情况下,事态往往会变得更糟。 在 Solaris 中,也就意味着在所有的 Unix 系统中,或者说在所有当代的操作系统中,除了微软的以外, 每个进程或者线程都有优先级。高优先级的进程时不会被低优先级的进程打断的,此外,一个进程的优先 级可以由管理员限制和设定,以防止一个用户进程是打断 OS 核心进程或者服务。NT 对此都无法支持。一 个 NT 的进程就是一个内存的地址空间。 它没有固定优先级,也不能被预先编排。 而全部交由系统来调度, 如果一个线程运行在一个不再内存中的进程下,这个进程将会被切换进内存。 NT 中进程的优先级被简化 为几个分布在实际优先级范围内的优先级类,也就是说他们是不固定的,由系统核心干预调配。 1.2 图: 如 上图中的列,代表线程的优先级,只有 22 级是有所有的程序所使用(其它的只能为 NT 自己使用)。 行代 表前面提过的优先级类。 一个运行在“Idle”级进程上的线程,只能使用 1-6 和 15,这七个优先级别,当然具体的是那 一级,还要取决于线程的设定。运行在“Normal”级进程里并且没有得到焦点的一个线程将可能会使用 1,6 — 10 或 15 的优先级。如果有焦点而且所在进程使还是“Normal”级,这样里面的线程将会运行在 1,7 — 11 或者 15 级。这也就意味着一个拥有搞优先级但在“Idle”进程内的线程,有可能被一个低 优先级但是运行在“Normal”级的线程抢先(Preempt),但这只限于后台进程。还应该注意到一个运行
  • 27. Java 多线程编程 - 27 - 在“High”优先级类的进程只有 6 个优先级等级而其它优先级类都有 7。 NT 不对进程的优先级类进行任何限制。运行在任意进程上的任意线程,可以通过优先级助推机制完 全控制整个系统, OS 核心没有任何的防御。 另一方面,Solaris 完全支持进程优先级的机制,因为你可 能需要设定你的屏幕保护程序的优先级,以防止它阻碍系统重要进程的运行 J。因为在一台关键的服务器 中,优先级低的进程就不应该也不能占用高优先级的线程执行。由此可见,微软的操作系统根本不适合做 高可靠性服务器。 那么我们在编程的时候,怎样避免呢?对于 NT 的这种无限制的优先级设定和无法控制的优先级助推 机制(对于 Java 程序),事实上就没有绝对安全的方法来使 Java 程序依赖优先级调度线程的执行。一 个折衷的方法,就是在用 setPriority( )函数设定线程优先级的时候,只使用 Thread.MAX_PRIORITY, Thread.MIN_PRIORITY 和 Thread.NORM_PRIORITY 这几个没有具体指 明优先级的参数。这个限制至少可以避免把 10 级映射为 7 级的问题。另外,还建议可以通过 os.name 的 系统属性来判定是否是 NT,如果是就通过调用一个本地函数来关闭优先级助推机制,但是那对于运行在 没有使用 Sun 的 JVM plug-in 的 IE 的 Java 程序,也是毫无作用的(微软的 JVM 使用了一个非标准的, 本地实现)。最后,还是建议大家在编程的时候,把大多数的线程的优先级设置为 NORM_PRIORITY,并 且依靠线程调度机制(scheduling)。(我将会再后面的谈到这个问题) 协作!(Cooperate) 一般来说,有两种线程模式:协作式和抢先式。 协作式多线程模型 在一个协作式的系统中,一个线程包留对处理器的控制直到它自己决定放弃(也许它永远不会 放弃控制权)。多个线程之间不得不相互合作否则可能只有一个线程能够执行,其它都处于饥饿状态。在 大多数的协作式系统中,线程的调度一般由优先级决定。当当前的线程放弃控制权,等待的线程中优先级 高的将会得到控制权(一个特例,就是 Window 3.x 系统,它也是协作式系统,但却没有很多的线程执 行进度调节,得到焦点的窗体获得控制权)。 协作式系统相对于抢先式系统的一个主要优点就是它运行速度快、 代价低。 比如,一个上下文切换— —控制权由一个线程转换到另一个线程——可以完全由用户模式子系统库完成,而不需进入系统核心态 (在 NT 下,就相当于 600 个机械指令时间)。在协作式系统下,用户态上下文切换就相当于 C 语言调用, setjump / longjump。大量的协作式线程同时运行,也不会影响性能,因为一切由程序员编程掌握, 更加不需要考虑同步的问题。程序员只要保证它的线程在没有完成目标以前不放弃对 CPU 的控制。但世界 终归不是完美的,人生总充满了遗憾。协同式线程模型也有自己硬伤: 在协作式线程模型下,用户编程非常麻烦(其实就是系统把复杂度转移到用户身上)。把很长的操 作分割成许多小块,是一件需要很小心的事。 2. 协作式线程无法并行执行。 抢先式多线程模型 另一种选择就是抢先式模型,这种模式就好象是有一个系统内部的时钟,由它来触发线程的切换。也就是 说,系统可以任意的从线程中夺回对 CPU 的控制权,在把控制权分给其它的线程。 两次切换之间的时间间 隔就叫做时间切片(Time Slice)。 抢先式系统的效率不如协作式高,因为 OS 核心必须负责管理线程,但是这样使用户在编程的时候不用考 虑那么多的其它问题,简化了用户的工作,而且使程序更加可靠,因为线程饥饿不再是一个问题。最关键 的优势在于抢先式模型是并行的。通过前面的介绍,你可以知道协作式线程调度是由用户子程序完成,而 不是 OS,因此,你最多可以做到使你的程序具有并发性(如图 1.1)。为了达到真正并行的目的,必须 有操作系统介入。四个线程并行的运行在四个 CPU 上的效率要比四个线程并发的运行高的多。 一些操作系统,象 Windows 3.1,只支持协作式模型;还有一些,象 NT 只支持抢先式模型(当然了你 也可以通过使用用户模式的库调用在 NT 上模拟协作式模型。NT 就有这么一个库叫“fiber”,但是遗憾 的是 fiber 充满的了 Bugs,并且没有彻底的整合到底层系统中去。)Solaris 提供了世界上可能是最 好的(当然也可能是最差的)线程模型,它既支持协作式,又支持抢先式。 从核心线程到用户进程的映射 最后一个要解决的问题就是核心线程到用户态进程的映射。NT 使用的是一对一的映射模式, NT 的用户线程就相当于系统核心线程。 他们被 OS 直接映射到每一个处理器上,并且总是抢先式的。 所有的线程操作和同步都是通过核心调用完成的。这是一个非常直接的模型,但是它既不灵活又低效。 图 1.4 表现的 Solaris 模型更加有趣。Solaris 增加了一个叫轻量级进程(LWP —
  • 28. Java 多线程编程 - 28 - lightweight process)的概念。LWP 是可以运行一个或多个线程的可调度单元。只在 LWP 这个程次上 进行并行处理。一般的,LWP 都存放在缓冲池中,并且按需分配给相应的处理器。如果一个 LWP 要执行某 些时序性要求很高的任务时,它一定要绑定特定处理器,以阻止其它的 LWPs 使用它。 从用户的角度来看,这是一个既协作又抢先的线程模型。简单来说,一个进程至少有一个 LWP 供它 所包含的所有线程共享使用。每个线程必须通过出让使用权(yield)来让其它线程执行(协作),但是 单个 LWP 又可以为其它的进程的 LWP 抢先。这样在进程一级上就达到了并行的效果,同时在里面线程又 处于协作的工作模式。 一个进程并不限死只有一个 LWP,这个进程下的线程们可以共享整个 LWP 池。一个线程可以通过以 下两种方法绑定到一个 LWP 上: 1. 通过编程显示的绑定一个或多个线程到一个指定的 LWP。在这个情况下,同一个 LWP 下的线程必须协 作式工作,但是这些线程又可以抢先其它 LWP 下的线程。这也就是说如果限制一个 LWP 只能绑定一个线 程,那就变成了 NT 的抢先式线程系统了。 2. 通过用户态的调度器自动绑定。从编程的角度看,这是一个比较混乱的情况,因为你并不能假设环境 究竟是协作式的,还是抢先式的。 Solaris 线程系统给于用户最大的灵活性。你可以在速度极快的并发协作式系统和较慢但 确是并行的抢先式系统,或者这两者的折衷中选择。但是,Solaris 的世界真的是那么完美吗?(我的 一贯论调又出现了,呵呵!)这一切一切的灵活性对于一个 Java 程序员来说等于没有,因为你并不能决 定 JVM 采用的线程模型。例如,早期的 Solaris JVM 采取严格的协作式机制。JVM 相当于一个 LWP,所 有 Java 线程共享这唯一一个 LWP。现在 Solaris JVM 又采用彻底的抢先式模型了,所有的线程独占各 自的 LWP。 那么我们这些可怜的程序员怎么办?我们在这个世上是如此的渺小,就连 JVM 采用那种模式的线程 机制都无法确定。为了写出平台独立的代码,必须做出两个表面上矛盾的假设: 一个线程可以被另一个线程在任何时候抢先。因此,你必须小心的使用 synchronized 关键字来保证非 原子性的操作运行正确。 2. 一个线程永远不会被抢先除非它自己放弃控制权。因此,你必须偶然的执行一些放弃控制权的操作来 给机会别的线程运行,适当的使用 yield( )和 sleep( )或者利用阻塞性的 I/O 调用。例如当你的线程 每进行 100 次遍例或者相当长度的密集操作后,你应该主动的使你的线程休眠几百个毫秒,来给低优先 级的线程以机会运行。注意!yield( )方法只会把控制权交给与你线程的优先级相当或者更高的线程。 总结 由于诸多的 OS 级因素导致了 Java 程序员在编写彻底平台独立的多线程程序时,麻烦频频(唉 ~~~~~~我又想发牢骚了,忍住!)。我们只能按最糟糕的情况打算,例如只能假设你的线程随时都会被 抢先,所以必须适当的使用 synchronized;又不得不假设你的线程永远不会被抢先,如果你不自己放 弃,所以你又必须偶尔使用 yield( )和 sleep( )或阻塞的 I/O 操作来让出控制权。还有就是我一开始 就介绍的:永远不要相信线程优先级,如果你想真正做到平台独立!