这篇文章,大部分内容,是周五我做的一个关于如何进行Java多线程编程的Knowledge Sharing的一个整理,我希望能对Java从第一个版本开始,在多线程编程方面的大事件和发展脉络有一个描述,并且提及一些在多线程编程方面常见的问题。对于Java程序员来说,如果从历史的角度去了解一门语言一个特性的演进,或许能有不同收获。
引言
首先问这样一个问题,如果提到Java多线程编程,你会想到什么?
好吧,请原谅我在这里卖的关子,其实这些都对,但是又不足够全面,如果我们这样来谈论Java多线程会不会全面一些:
可是,这未免太死板了,不是么?
不如换一个思路,我们少谈一些很容易查到的语法,不妨从历史的角度看看Java在多线程编程方面是怎样进化的,这个过程中,它做了哪些正确的决定,犯了哪些错误,未来又会有怎样的发展趋势?
另外,还有一点要说是,我希望通过大量的实例代码来说明这些事情。Linus说:“Talk is cheap, show me the code.”。
诞生
Java的基因来自于1990年12月Sun公司的一个内部项目,目标设备正是家用电器,但是C++的可移植性和API的易用性都让程序员反感。旨在解决这样的问题,于是又了Java的前身Oak语言,但是知道1995年3月,它正式更名为Java,才算Java语言真正的诞生。
JDK 1.0
1996年1月的JDK1.0版本,从一开始就确立了Java最基础的线程模型,并且,这样的线程模型再后续的修修补补中,并未发生实质性的变更,可以说是一个具有传承性的良好设计。
抢占式和协作式是两种常见的进程/线程调度方式,操作系统非常适合使用抢占式方式来调度它的进程,它给不同的进程分配时间片,对于长期无响应的进程,它有能力剥夺它的资源,甚至将其强行停止(如果采用协作式的方式,需要进程自觉、主动地释放资源,也许就不知道需要等到什么时候了)。Java语言一开始就采用协作式的方式,并且在后面发展的过程中,逐步废弃掉了粗暴的stop/resume/suspend这样的方法,它们是违背协作式的不良设计,转而采用wait/notify/sleep这样的两边线程配合行动的方式。
一种线程间的通信方式是使用中断:
public
class
InterruptCheck
extends
Thread {
@Override
public
void
run() {
System.out.println(
"start"
);
while
(
true
)
if
(Thread.currentThread().isInterrupted())
break
;
System.out.println(
"while exit"
);
}
public
static
void
main(String[] args) {
Thread thread =
new
InterruptCheck();
thread.start();
try
{
sleep(
2000
);
}
catch
(InterruptedException e) {
}
thread.interrupt();
}
}
|
这是中断的一种使用方式,看起来就像是一个标志位,线程A设置这个标志位,线程B时不时地检查这个标志位。另外还有一种使用中断通信的方式,如下:
public
class
InterruptWait
extends
Thread {
public
static
Object lock =
new
Object();
@Override
public
void
run() {
System.out.println(
"start"
);
synchronized
(lock) {
try
{
lock.wait();
}
catch
(InterruptedException e) {
System.out.println(Thread.currentThread().isInterrupted());
Thread.currentThread().interrupt();
// set interrupt flag again
System.out.println(Thread.currentThread().isInterrupted());
e.printStackTrace();
}
}
}
public
static
void
main(String[] args) {
Thread thread =
new
InterruptWait();
thread.start();
try
{
sleep(
2000
);
}
catch
(InterruptedException e) {
}
thread.interrupt();
}
}
|
在这种方式下,如果使用wait方法处于等待中的线程,被另一个线程使用中断唤醒,于是抛出InterruptedException,同时,中断标志清除,这时候我们通常会在捕获该异常的地方重新设置中断,以便后续的逻辑通过检查中断状态来了解该线程是如何结束的。
在比较稳定的JDK 1.0.2版本中,已经可以找到Thread和ThreadUsage这样的类,这也是线程模型中最核心的两个类。整个版本只包含了这样几个包:java.io、 java.util、java.net、java.awt和java.applet,所以说Java从一开始这个非常原始的版本就确立了一个持久的线程模型。
值得一提的是,在这个版本中,原子对象AtomicityXXX已经设计好了,这里给出一个例子,说明i++这种操作时非原子的,而使用原子对象可以保证++操作的原子性:
import
java.util.concurrent.atomic.AtomicInteger;
public
class
Atomicity {
private
static
volatile
int
nonAtomicCounter =
0
;
private
static
volatile
AtomicInteger atomicCounter =
new
AtomicInteger(
0
);
private
static
int
times =
0
;
public
static
void
caculate() {
times++;
for
(
int
i =
0
; i <
1000
; i++) {
new
Thread(
new
Runnable() {
@Override
public
void
run() {
nonAtomicCounter++;
atomicCounter.incrementAndGet();
}
}).start();
}
try
{
Thread.sleep(
1000
);
}
catch
(InterruptedException e) {
}
}
public
static
void
main(String[] args) {
caculate();
while
(nonAtomicCounter ==
1000
) {
nonAtomicCounter =
0
;
atomicCounter.set(
0
);
caculate();
}
System.out.println(
"Non-atomic counter: "
+ times +
":"
+ nonAtomicCounter);
System.out.println(
"Atomic counter: "
+ times +
":"
+ atomicCounter);
}
}
|
上面这个例子你也许需要跑几次才能看到效果,使用非原子性的++操作,结果经常小于1000。
对于锁的使用,网上可以找到各种说明,但表述都不够清晰。请看下面的代码:
public
class
Lock {
private
static
Object o =
new
Object();
static
Lock lock =
new
Lock();
// lock on dynamic method
public
synchronized
void
dynamicMethod() {
System.out.println(
"dynamic method"
);
sleepSilently(
2000
);
}
// lock on static method
public
static
synchronized
void
staticMethod() {
System.out.println(
"static method"
);
sleepSilently(
2000
);
}
// lock on this
public
void
thisBlock() {
synchronized
(
this
) {
System.out.println(
"this block"
);
sleepSilently(
2000
);
}
}
// lock on an object
public
void
objectBlock() {
synchronized
(o) {
System.out.println(
"dynamic block"
);
sleepSilently(
2000
);
}
}
// lock on the class
public
static
void
classBlock() {
synchronized
(Lock.
class
) {
System.out.println(
"static block"
);
sleepSilently(
2000
);
}
}
private
static
void
sleepSilently(
long
millis) {
try
{
Thread.sleep(millis);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
public
static
void
main(String[] args) {
// object lock test
new
Thread() {
@Override
public
void
run() {
lock.dynamicMethod();
}
}.start();
new
Thread() {
@Override
public
void
run() {
lock.thisBlock();
}
}.start();
new
Thread() {
@Override
public
void
run() {
lock.objectBlock();
}
}.start();
sleepSilently(
3000
);
System.out.println();
// class lock test
new
Thread() {
@Override
public
void
run() {
lock.staticMethod();
}
}.start();
new
Thread() {
@Override
public
void
run() {
lock.classBlock();
}
}.start();
}
}
|
上面的例子可以反映对一个锁竞争的现象,结合上面的例子,理解下面这两条,就可以很容易理解synchronized关键字的使用:
JDK 1.2
1998年年底的JDK1.2版本正式把Java划分为J2EE/J2SE/J2ME三个不同方向。在这个版本中,Java试图用Swing修正在AWT中犯的错误,例如使用了太多的同步。可惜的是,Java本身决定了AWT还是Swing性能和响应都难以令人满意,这也是Java桌面应用难以比及其服务端应用的一个原因,在IBM后来的SWT,也不足以令人满意,JDK在这方面到JDK 1.2后似乎反省了自己,停下脚步了。值得注意的是,JDK高版本修复低版本问题的时候,通常遵循这样的原则:
在这个版本中,正式废除了这样三个方法:stop()、suspend()和resume()。下面我就来介绍一下,为什么它们要被废除:
public
class
Stop
extends
Thread {
@Override
public
void
run() {
try
{
while
(
true
)
;
}
catch
(Throwable e) {
e.printStackTrace();
}
}
public
static
void
main(String[] args) {
Thread thread =
new
Stop();
thread.start();
try
{
sleep(
1000
);
}
catch
(InterruptedException e) {
}
thread.stop(
new
Exception(
"stop"
));
// note the stack trace
}
}
|
从上面的代码你应该可以看出两件事情:
很难想象这样的设计出自一个连指针都被废掉的类型安全的编程语言,对不对?再来看看suspend的使用,有引起死锁的隐患:
public
class
Suspend
extends
Thread {
@Override
public
void
run() {
synchronized
(
this
) {
while
(
true
)
;
}
}
public
static
void
main(String[] args) {
Thread thread =
new
Suspend();
thread.start();
try
{
sleep(
1000
);
}
catch
(InterruptedException e) {
}
thread.suspend();
synchronized
(thread) {
// dead lock
System.out.println(
"got the lock"
);
thread.resume();
}
}
}
|
从上面的代码可以看出,Suspend线程被挂起时,依然占有锁,而当main线程期望去获取该线程来唤醒它时,彻底瘫痪了。由于suspend在这里是无期限限制的,这会变成一个彻彻底底的死锁。
相反,看看这三个方法的改进品和替代品:wait()、notify()和sleep(),它们令线程之间的交互就友好得多:
public
class
Wait
extends
Thread {
@Override
public
void
run() {
System.out.println(
"start"
);
synchronized
(
this
) {
// wait/notify/notifyAll use the same
// synchronization resource
try
{
this
.wait();
}
catch
(InterruptedException e) {
e.printStackTrace();
// notify won't throw exception
}
}
}
public
static
void
main(String[] args) {
Thread thread =
new
Wait();
thread.start();
try
{
sleep(
2000
);
}
catch
(InterruptedException e) {
}
synchronized
(thread) {
System.out.println(
"Wait() will release the lock!"
);
thread.notify();
}
}
}
|
在wait和notify搭配使用的过程中,注意需要把它们锁定到同一个资源上(例如对象a),即:
再来看一看sleep方法的使用,回答下面两个问题:
如果我前面说的你都理解了,你应该能回答这两个问题。
public
class
Sleep
extends
Thread {
@Override
public
void
run() {
System.out.println(
"start"
);
synchronized
(
this
) {
// sleep() can use (or not) any synchronization resource
try
{
/**
* Do you know: <br>
* 1. Why sleep() is designed as a static method comparing with
* wait?<br>
* 2. Why sleep() must have a timeout parameter?
*/
this
.sleep(
10000
);
}
catch
(InterruptedException e) {
e.printStackTrace();
// notify won't throw exception
}
}
}
public
static
void
main(String[] args) {
Thread thread =
new
Sleep();
thread.start();
try
{
sleep(
2000
);
}
catch
(InterruptedException e) {
}
synchronized
(thread) {
System.out.println(
"Has sleep() released the lock!"
);
thread.notify();
}
}
}
|
在这个JDK版本中,引入线程变量ThreadLocal这个类:
每一个线程都挂载了一个ThreadLocalMap。ThreadLocal这个类的使用很有意思,get方法没有key传入,原因就在于这个key就是当前你使用的这个ThreadLocal它自己。ThreadLocal的对象生命周期可以伴随着整个线程的生命周期。因此,倘若在线程变量里存放持续增长的对象(最常见是一个不受良好管理的map),很容易导致内存泄露。
public
class
ThreadLocalUsage
extends
Thread {
public
User user =
new
User();
public
User getUser() {
return
user;
}
@Override
public
void
run() {
this
.user.set(
"var1"
);
while
(
true
) {
try
{
sleep(
1000
);
}
catch
(InterruptedException e) {
}
System.out.println(
this
.user.get());
}
}
public
static
void
main(String[] args) {
ThreadLocalUsage thread =
new
ThreadLocalUsage();
thread.start();
try
{
sleep(
4000
);
}
catch
(InterruptedException e) {
}
thread.user.set(
"var2"
);
}
}
class
User {
private
static
ThreadLocal<Object> enclosure =
new
ThreadLocal<Object>();
// is it must be static?
public
void
set(Object object) {
enclosure.set(object);
}
public
Object get() {
return
enclosure.get();
}
}
|
上面的例子会一直打印var1,而不会打印var2,就是因为不同线程中的ThreadLocal是互相独立的。
用jstack工具可以找到锁相关的信息,如果线程占有锁,但是由于执行到wait方法时处于wait状态暂时释放了锁,会打印waiting on的信息:
"Thread-0"
prio=
6
tid=
0x02bc4400
nid=
0xef44
in Object.wait() [
0x02f0f000
]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <
0x22a7c3b8
> (a Wait)
at java.lang.Object.wait(Object.java:
485
)
at Wait.run(Wait.java:
8
)
- locked <
0x22a7c3b8
> (a Wait)
|
如果程序持续占有某个锁(例如sleep方法在sleep期间不会释放锁),会打印locked的信息:
"Thread-0"
prio=
6
tid=
0x02baa800
nid=
0x1ea4
waiting on condition [
0x02f0f000
]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at Wait.run(Wait.java:
8
)
- locked <
0x22a7c398
> (a Wait)
|
而如果是线程希望进入某同步块,而在等待锁的释放,会打印waiting to的信息:
"main"
prio=
6
tid=
0x00847400
nid=
0xf984
waiting
for
monitor entry [
0x0092f000
]
java.lang.Thread.State: BLOCKED (on object monitor)
at Wait.main(Wait.java:
23
)
- waiting to lock <
0x22a7c398
> (a Wait)
|
JDK 1.4
在2002年4月发布的JDK1.4中,正式引入了NIO。JDK在原有标准IO的基础上,提供了一组多路复用IO的解决方案。
通过在一个Selector上挂接多个Channel,通过统一的轮询线程检测,每当有数据到达,触发监听事件,将事件分发出去,而不是让每一个channel长期消耗阻塞一个线程等待数据流到达。所以,只有在对资源争夺剧烈的高并发场景下,才能见到NIO的明显优势。
相较于面向流的传统方式这种面向块的访问方式会丢失一些简易性和灵活性。下面给出一个NIO接口读取文件的简单例子(仅示意用):
import
java.io.FileInputStream;
import
java.io.IOException;
import
java.nio.ByteBuffer;
import
java.nio.channels.FileChannel;
public
class
NIO {
public
static
void
nioRead(String file)
throws
IOException {
FileInputStream in =
new
FileInputStream(file);
FileChannel channel = in.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(
1024
);
channel.read(buffer);
byte
[] b = buffer.array();
System.out.println(
|