Java在语言层次上实现了对线程
支持。它提供了Thread/Runnable/ThreadGroup等一系列封装
类和接口,让程序员可以高效
开发Java多线程应用。为了实现同步,Java提供了synchronize关键字以及object
wait()/notify()机制,可是在简单易用
背后,应藏着更为复杂
玄机,很多问题就是由此而起。
字串6
一、Java内存模型
在了解Java
同步秘密之前,先来看看JMM(Java Memory Model)。
字串1
Java被设计为跨平台
语言,在内存管理上,显然也要有一个统一
模型。而且Java语言最大
特点就是废除了指针,把程序员从痛苦中解脱出来,不用再考虑内存使用和管理方面
问题。 字串9
可惜世事总不尽如人意,虽然JMM设计上方便了程序员,但是它增加了虚拟机
复杂程度,而且还导致某些编程技巧在Java语言中失效。
字串6
JMM主要是为了规定了线程和内存之间
一些关系。对Java程序员来说只需负责用synchronized同步关键字,其它诸如与线程/内存之间进行数据交换/同步等繁琐工作均由虚拟机负责完成。如图1所示:根据JMM
设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享
。每条线程都有自己
工作内存(Working Memory),工作内存中保存
是主存中某些变量
拷贝,线程对所有变量
操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。 字串1
字串6
图1 Java内存模型示例图 字串1
线程若要对某变量进行操作,必须经过一系列步骤:首先从主存复制/刷新数据到工作内存,然后执行代码,进行引用/赋值操作,最后把变量内容写回Main Memory。Java语言规范(JLS)中对线程和主存互操作定义了6个行为,分别为load,save,read,write,assign和use,这些操作行为具有原子性,且相互依赖,有明确
调用先后顺序。具体
描述请参见JLS第17章。
我们在前面
章节介绍了synchronized
作用,现在,从JMM
角度来重新审视synchronized关键字。
字串5
假设某条线程执行一个synchronized代码段,其间对某变量进行操作,JVM会依次执行如下动作:
字串8
(1) 获取同步对象monitor (lock)
字串9
(2) 从主存复制变量到当前工作内存 (read and load)
(3) 执行代码,改变共享变量值 (use and assign)
(4) 用工作内存数据刷新主存相关内容 (store and write)
字串1
(5) 释放同步对象锁 (unlock)
字串4
可见,synchronized
另外一个作用是保证主存内容和线程
工作内存中
数据
一致性。如果没有使用synchronized关键字,JVM不保证第2步和第4步会严格按照上述次序立即执行。因为根据JLS中
规定,线程
工作内存和主存之间
数据交换是松耦合
,什么时候需要刷新工作内存或者更新主内存内容,可以由具体
虚拟机实现自行决定。如果多个线程同时执行一段未经synchronized保护
代码段,很有可能某条线程已经改动了变量
值,但是其他线程却无法看到这个改动,依然在旧
变量值上进行运算,最终导致不可预料
运算结果。
二、DCL失效
这一节我们要讨论
是一个让Java丢脸
话题:DCL失效。在开始讨论之前,先介绍一下LazyLoad,这种技巧很常用,就是指一个类包含某个成员变量,在类初始化
时候并不立即为该变量初始化一个实例,而是等到真正要使用到该变量
时候才初始化之。
例如下面
代码: 字串1
代码1 字串1
class Foo
{
private Resource res = null;
public Resource getResource()
{
if (res == null) res = new Resource();
return res;
}
} 由于LazyLoad可以有效
减少系统资源消耗,提高程序整体
性能,所以被广泛
使用,连Java
缺省类加载器也采用这种方法来加载Java类。
在单线程环境下,一切都相安无事,但如果把上面
代码放到多线程环境下运行,那么就可能会出现问题。假设有2条线程,同时执行到了if(res == null),那么很有可能res被初始化2次,为了避免这样
Race Condition,得用synchronized关键字把上面
方法同步起来。代码如下:
字串6
代码2 字串6
Class Foo
{
Private Resource res = null;
Public synchronized Resource getResource()
{
If (res == null) res = new Resource();
return res;
}
} 现在Race Condition解决了,一切都很
。 字串2
N天过后,
学
你偶然看了一本Refactoring
魔书,深深为之打动,准备自己尝试这重构一些以前写过
程序,于是找到了上面这段代码。你已经不再是以前
Java菜鸟,深知synchronized过
方法在速度上要比未同步
方法慢上100倍,同时你也发现,只有第一次调用该方法
时候才需要同步,而一旦res初始化完成,同步完全没必要。所以你很快就把代码重构成了下面
样子: 字串9
字串3
代码3 字串3
Class Foo
{
Private Resource res = null;
Public Resource getResource()
{
If (res == null)
{
synchronized(this)
{
if(res == null)
{
res = new Resource();
}
}
}
return res;
}
} 这种看起来很完美
优化技巧就是Double-Checked Locking。但是很遗憾,根据Java
语言规范,上面
代码是不可靠
。 字串8
造成DCL失效
原因之一是编译器
优化会调整代码
次序。只要是在单个线程情况下执行结果是正确
,就可以认为编译器这样
“自作主张
调整代码次序”
行为是合法
。JLS在某些方面
规定比较自由,就是为了让JVM有更多余地进行代码优化以提高执行效率。而现在
CPU大多使用超流水线技术来加快代码执行速度,针对这样
CPU,编译器采取
代码优化
方法之一就是在调整某些代码
次序,尽可能保证在程序执行
时候不要让CPU
指令流水线断流,从而提高程序
执行速度。正是这样
代码调整会导致DCL
失效。为了进一步证明这个问题,引用一下《DCL Broken Declaration》文章中
例子: 字串5
设一行Java代码:
字串6
Objects[i].reference = new Object(); 字串8
经过Symantec JIT编译器编译过以后,最终会变成如下汇编码在机器中执行:
0206106A mov eax,0F97E78h
0206106F call 01F6B210 ;为Object申请内存空间
; 返回值放在eax中
02061074 mov dword ptr [ebp],eax ; EBP 中是objects[i].reference
地址
; 将返回
空间地址放入其中
; 此时Object尚未初始化
02061077 mov ecx,dword ptr [eax] ; dereference eax所指向
内容
; 获得新创建对象
起始地址
02061079 mov dword ptr [ecx],100h ; 下面4行是内联
构造函数
0206107F mov dword ptr [ecx 4],200h
02061086 mov dword ptr [ecx 8],400h
0206108D mov dword ptr [ecx 0Ch],0F84030h 可见,Object构造函数尚未调用,但是已经能够通过objects[i].reference获得Object对象实例
引用。 字串3
如果把代码放到多线程环境下运行,某线程在执行到该行代码
时候JVM或者操作系统进行了一次线程切换,其他线程显然会发现msg对象已经不为空,导致Lazy load
判断语句if(objects[i].reference == null)不成立。线程认为对象已经建立成功,随之可能会使用对象
成员变量或者调用该对象实例
方法,最终导致不可预测
错误。 字串9
字串6
原因之二是在共享内存
SMP机上,每个CPU有自己
Cache和寄存器,共享同一个系统内存。所以CPU可能会动态调整指令
执行次序,以更
进行并行运算并且把运算结果与主内存同步。这样
代码次序调整也可能导致DCL失效。回想一下前面对Java内存模型
介绍,我们这里可以把Main Memory看作系统
物理内存,把Thread Working Memory认为是CPU内部
Cache和寄存器,没有synchronized
保护,Cache和寄存器
内容就不会及时和主内存
内容同步,从而导致一条线程无法看到另一条线程对一些变量
改动。
字串8
结合代码3来举例说明,假设Resource类
实现如下: 字串6
Class Resource{ Object obj;} 字串8
即Resource类有一个obj成员变量引用了Object
一个实例。假设2条线程在运行,其状态用如下简化图表示:
字串9
图2 字串8
现在Thread-1构造了Resource实例,初始化过程中改动了obj
一些内容。退出同步代码段后,因为采取了同步机制,Thread-1所做
改动都会反映到主存中。接下来Thread-2获得了新
Resource实例变量res,由于没有使用synchronized保护所以Thread-2不会进行刷新工作内存
操作。假如之前Thread-2
工作内存中已经有了obj实例
一份拷贝,那么Thread-2在对obj执行use操作
时候就不会去执行load操作,这样一来就无法看到Thread-1对obj
改变,这显然会导致错误
运算结果。此外,Thread-1在退出同步代码段
时刻对ref和obj执行
写入主存
操作次序也是不确定
,所以即使Thread-2对obj执行了load操作,也有可能只读到obj
初试状态
数据。(注:这里
load/use均指JMM定义
操作) 字串1
有很多人不死心,试图想出了很多精妙
办法来解决这个问题,但最终都失败了。事实上,无论是目前
JMM还是已经作为JSR提交
JMM模型
增强,DCL都不能正常使用。在William Pugh
论文《Fixing the Java Memory Model》中详细
探讨了JMM
一些硬伤,更尝试给出一个新
内存模型,有兴趣深入研究
读者可以参见文后
参考资料。 字串9
字串2
如果你设计
对象在程序中只有一个实例,即singleton
,有一种可行
解决办法来实现其LazyLoad:就是利用类加载器
LazyLoad特性。代码如下: 字串1
Class ResSingleton {public static Resource res = new Resource();} 这里ResSingleton只有一个静态成员变量。当第一次使用ResSingleton.res
时候,JVM才会初始化一个Resource实例,并且JVM会保证初始化
结果及时写入主存,能让其他线程看到,这样就成功
实现了LazyLoad。
除了这个办法以外,还可以使用ThreadLocal来实现DCL
方法,但是由于ThreadLocal
实现效率比较低,所以这种解决办法会有较大
性能损失,有兴趣
读者可以参考文后
参考资料。 字串7
最后要说明
是,对于DCL是否有效,个人认为更多
是一种带有学究气
推断和讨论。而从纯理论
角度来看,存取任何可能共享
变量(对象引用)都需要同步保护,否则都有可能出错,但是处处用synchronized又会增加死锁
发生几率,苦命
程序员怎么来解决这个矛盾呢?事实上,在很多Java开源项目(比如Ofbiz/Jive等)
代码中都能找到使用DCL
证据,我在具体
实践中也没有碰到过因DCL而发生
程序异常。个人
偏
是:不妨先大胆使用DCL,等出现问题再用synchronized逐步排除之。也许有人偏于保守,认为稳定压倒一切,那就不妨先用synchronized同步起来,我想这是一个见仁见智
问题,而且得针对具体
项目具体分析后才能决定。还有一个办法就是写一个测试案例来测试一下系统是否存在DCL现象,附带
光盘中提供了这样一个例子,感兴趣
读者可以自行编译测试。不管结果怎样,这样
讨论有助于我们更
认识JMM,养成用多线程
思路去分析问题
习惯,提高我们
程序设计能力。
字串3
三、Java线程同步增强包
字串2
相信你已经了解了Java用于同步
3板斧:synchronized/wait/notify,它们
确简单而有效。但是在某些情况下,我们需要更加复杂
同步工具。有些简单
同步工具类,诸如ThreadBarrier,Semaphore,ReadWriteLock等,可以自己编程实现。现在要介绍
是牛人Doug Lea
Concurrent包。这个包专门为实现Java高级并行程序所开发,可以满足我们绝大部分
要求。更令人兴奋
是,这个包公开源代码,可自由下载。且在JDK1.5中该包将作为SDK一部分提供给Java开发人员。 字串6
Concurrent Package提供了一系列基本
操作接口,包括sync,channel,executor,barrier,callable等。这里将对前三种接口及其部分派生类进行简单
介绍。 字串9
字串3
sync接口:专门负责同步操作,用于替代Java提供
synchronized关键字,以实现更加灵活
代码同步。其类关系图如下:
字串7
图3 Concurrent包Sync接口类关系图 字串6
Semaphore:和前面介绍
代码类似,可用于pool类实现资源管理限制。提供了acquire()方法允许在设定时间内尝试锁定信号量,若超时则返回false。 字串3
Mutex:和Java
synchronized类似,与之不同
是,synchronized
同步段只能限制在一个方法内,而Mutex对象可以作为参数在方法间传递,所以可以把同步代码范围扩大到跨方法甚至跨对象。
字串4
NullSync:一个比较奇怪
东西,其方法
内部实现都是空
,可能是作者认为如果你在实际中发现某段代码根本可以不用同步,但是又不想过多改动这段代码,那么就可以用NullSync来替代原来
Sync实例。此外,由于NullSync
方法都是synchronized,所以还是保留了“内存壁垒”
特性。
字串6
ObservableSync:把sync和observer模式结合起来,当sync
方法被调用时,把消息通知给订阅者,可用于同步性能调试。
字串7
TimeoutSync:可以认为是一个adaptor,其构造函数如下: 字串1
public TimeoutSync(Sync sync, long timeout){…} 具体上锁
代码靠构造函数传入
sync实例来完成,其自身只负责监测上锁操作是否超时,可与SyncSet合用。 字串1
Channel接口:代表一种具备同步控制能力
容器,你可以从中存放/读取对象。不同于JDK中
Collection接口,可以把Channel看作是连接对象构造者(Producer)和对象使用者(Consumer)之间
一根管道。如图所示: 字串1
图4 Concurrent包Channel接口示意图 字串3
通过和Sync接口配合,Channel提供了阻塞式
对象存取方法(put/take)以及可设置阻塞等待时间
offer/poll方法。实现Channel接口
类有LinkedQueue,BoundedLinkedQueue,BoundedBuffer,BoundedPriorityQueue,SynchronousChannel,Slot等。 字串9
字串3
图5 Concurrent包Channel接口部分类关系图
使用Channel我们可以很容易
编写具备消息队列功能
代码,示例如下:
字串5
代码4
Package org.javaresearch.j2seimproved.thread;
Import EDU.oswego.cs.dl.util.concurrent.*;
public class TestChannel {
final Channel msgQ = new LinkedQueue(); //log信息队列
public static void main(String[] args) {
TestChannel tc = new TestChannel();
For(int i = 0;i < 10;i ){
Try{
tc.serve();
Thread.sleep(1000);
}catch(InterruptedException ie){
}
}
}
public void serve() throws InterruptedException {
String status = doService();
//把doService()返回状态放入Channel,后台logger线程自动读取之
msgQ.put(status);
}
private String doService() {
// Do service here
return "service completed OK! ";
}
public TestChannel() { // start background thread
Runnable logger = new Runnable() {
public void run() {
try {
for (; ; )
System.out.println("Logger: " msgQ.take());
}
catch (InterruptedException ie) {}
}
};
new Thread(logger).start(); 字串7
}
} Excutor/ThreadFactory接口: 把相关
线程创建/回收/维护/调度等工作封装起来,而让调用者只专心于具体任务
编码工作(即实现Runnable接口),不必显式创建Thread类实例就能异步执行任务。 字串7
字串8
使用Executor还有一个
处,就是实现线程
“轻量级”使用。前面章节曾提到,即使我们实现了Runnable接口,要真正
创建线程,还是得通过new Thread()来完成,在这种情况下,Runnable对象(任务)和Thread对象(线程)是1对1
关系。如果任务多而简单,完全可以给每条线程配备一个任务队列,让Runnable对象(任务)和Executor对象变成n:1
关系。使用了Executor,我们可以把上面两种线程策略都封装到具体
Executor实现中,方便代码
实现和维护。
具体
实现有: PooledExecutor,ThreadedExecutor,QueuedExecutor,FJTaskRunnerGroup等
字串6
类关系图如下:
字串1
图6 Concurrent包Executor/ThreadFactory接口部分类关系图
下面给出一段代码,使用PooledExecutor实现一个简单
多线程服务器 字串8
代码5 字串8
package org.javaresearch.j2seimproved.thread;
import java.net.*;
import EDU.oswego.cs.dl.util.concurrent.*;
public class TestExecutor
{
public static void main(String[] args)
{
PooledExecutor pool = new PooledExecutor(new BoundedBuffer(10), 20);
pool.createThreads(4);
try
{
ServerSocket socket = new ServerSocket(9999);
for (; ; )
{
final Socket connection = socket.accept();
pool.execute(new Runnable()
{
public void run()
{
new Handler().process(connection);
}
});
}
}
catch (Exception e) {}
// die
}
static class Handler { void process(Socket s){ } }
} 字串1
![我要研发网[www.51dev.com]](/templets/images/toplogo.gif)
