Java多线程编程详解
Java多线程编程详解
线程的同步
由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲
突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对
象被多个线程同时访问。
由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方
法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized
方法和 synchronized 块。
1. synchronized 方法:通过在方法声明中加入 synchronized关键字来声明 synchroniz
ed 方法。如:
public synchronized void accessVal(int newVal);
synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchroniz
ed 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行
,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新
进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchroni
zed 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对
应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法
均被声明为 synchronized)。
在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数
声明为 synchronized ,以控制其对类的静态成员变量的访问。
synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,
典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它
一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然
我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,
并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 s
ynchronized 块。
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() 方法对没有什么分别,但是事实上它们是截
然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占
用了的话),而这一对方法则相反。上述的核心区别导致了一系列的细节上的区别。
首先,前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于 Object
类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是
很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意
对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。而调用 任意对象的notif
y()方法则导致因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但
要等到获得锁后才真正可执行)。
其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchro
nized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有
锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有
,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或
块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽
然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。
wait() 和 notify() 方法的上述特性决定了它们经常和synchronized 方法或块一起
使用,将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchron
ized方法或块提供了类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰
,而这一对方法则相当于 block 和wakeup 原语(这一对方法均声明为 synchronized)。
它们的结合使得我们可以实现操作系统上一系列精妙的进程间通信的算法(如信号量算法
),并用于解决各种复杂的线程间通信问题。
关于 wait() 和 notify() 方法最后再说明两点:
第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而
阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心
,避免因这种不确定性而产生问题。
第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别
在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性
全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时
期限的 wait() 方法的调用都可能产生死锁。遗憾的是,Java 并不在语言级别上支持死锁
的避免,我们在编程中必须小心地避免死锁。
以上我们对 Java 中实现线程阻塞的各种方法作了一番分析,我们重点分析了 wait(
) 和 notify() 方法,因为它们的功能最强大,使用也最灵活,但是这也导致了它们的效
率较低,较容易出错。实际使用中我们应该灵活使用各种方法,以便更好地达到我们的目
的。
七:守护线程
守护线程是一类特殊的线程,它和普通线程的区别在于它并不是应用程序的核心部分
,当一个应用程序的所有非守护线程终止运行时,即使仍然有守护线程在运行,应用程序
也将终止,反之,只要有一个非守护线程在运行,应用程序就不会终止。守护线程一般被
用于在后台为其它线程提供服务。
可以通过调用方法 isDaemon() 来判断一个线程是否是守护线程,也可以调用方法 setDa
emon() 来将一个线程设为守护线程。
八:线程组
线程组是一个 Java 特有的概念,在 Java 中,线程组是类ThreadGroup 的对象,每
个线程都隶属于唯一一个线程组,这个线程组在线程创建时指定并在线程的整个生命期内
都不能更改。你可以通过调用包含 ThreadGroup 类型参数的 Thread 类构造函数来指定线
程属的线程组,若没有指定,则线程缺省地隶属于名为 system 的系统线程组。
在 Java 中,除了预建的系统线程组外,所有线程组都必须显式创建。在 Java 中,
除系统线程组外的每个线程组又隶属于另一个线程组,你可以在创建线程组时指定其所隶
属的线程组,若没有指定,则缺省地隶属于系统线程组。这样,所有线程组组成了一棵以
系统线程组为根的树。
Java 允许我们对一个线程组中的所有线程同时进行操作,比如我们可以通过调用线程
组的相应方法来设置其中所有线程的优先级,也可以启动或阻塞其中的所有线程。
Java 的线程组机制的另一个重要作用是线程安全。线程组机制允许我们通过分组来区分有
不同安全特性的线程,对不同组的线程进行不同的处理,还可以通过线程组的分层结构来
支持不对等安全措施的采用。Java 的 ThreadGroup 类提供了大量的方法来方便我们对线
程组树中的每一个线程组以及线程组中的每一个线程进行操作。
九:总结
在这一讲中,我们一起学习了 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()方法声
明同步,如上例所示:就控制了同一时刻只能有一个线程访问仓库对象中的同步方法;即
一个生产类线程访问被声明为同步的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 语言本身的一部分,所以 J
ava 语言线程局部变量的使用语法比内建线程局部变量语言的使用语法要笨拙一些。要创
建一个线程局部变量,请实例化类 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 {
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。
文章出处:http://www.diybl.com/course/3_program/java/javashl/20081216/153971.html