所有的编程语言都有一些共用的习语。了解和使用一些习语很有用,程序员们花费宝贵的时间来创建、学习和实现这些习语。问题是,稍后经过证明,一些习语并不完全如其所声称的那样,或者仅仅是与描述的功能不符。在 Java 编程语言中,双重检查锁定就是这样的一个绝不应该使用的习语。在本文中,Peter Haggar 介绍了双重检查锁定习语的渊源,开发它的原因和它失效的原因。
编辑注:本文在针对 Java 5.0 修订前参考了 Java 内存模型;关于内存排序的描述也许不再正确。尽管如此,在新的内存模型中,双重检查锁定习语仍旧是无效的。
单例创建模式是一个通用的编程习语。和多线程一起使用时,必需使用某种类型的同步。在努力创建更有效的代码时,Java 程序员们创建了双重检查锁定习语,将其和单例创建模式一起使用,从而限制同步代码量。然而,由于一些不太常见的 Java 内存模型细节的原因,并不能保证这个双重检查锁定习语有效。它偶尔会失败,而不是总失败。此外,它失败的原因并不明显,还包含 Java 内存模型的一些隐秘细节。这些事实将导致代码失败,原因是双重检查锁定难于跟踪。在本文余下的部分里,我们将详细介绍双重检查锁定习语,从而理解它在何处失效。
单例创建习语
要理解双重检查锁定习语是从哪里起源的,就必须理解通用单例创建习语,如清单 1 中的阐释:
清单 1. 单例创建习语
1. import java.util.*;
2. class Singleton
3. {
4. private static Singleton instance;
5. private Vector v;
6. private boolean inUse;
7.
8. private Singleton()
9. {
10. v = new Vector();
11. v.addElement( new Object());
12. inUse = true ;
13. }
14.
15. public static Singleton getInstance()
16. {
17. if (instance == null) //1
18. instance = new Singleton(); //2
19. return instance; //3
20. }
21. }
此类的设计确保只创建一个 Singleton 对象。构造函数被声明为private ,getInstance() 方法只创建一个对象。这个实现适合于单线程程序。然而,当引入多线程时,就必须通过同步来保护 getInstance() 方法。如果不保护 getInstance() 方法,则可能返回Singleton 对象的两个不同的实例。假设两个线程并发调用 getInstance() 方法并且按以下顺序执行调用:
线程 1 调用getInstance() 方法并决定 instance 在 //1 处为 null 。
线程 1 进入 if 代码块,但在执行 //2 处的代码行时被线程 2 预占。
线程 2 调用getInstance() 方法并在 //1 处决定 instance为 null 。
线程 2 进入 if 代码块并创建一个新的 Singleton 对象并在 //2 处将变量 instance 分配给这个新对象。
线程 2 在 //3 处返回 Singleton 对象引用。
线程 2 被线程 1 预占。
线程 1 在它停止的地方启动,并执行//2 代码行,这导致创建另一个 Singleton 对象。
线程 1 在 //3 处返回这个对象。
结果是 getInstance() 方法创建了两个Singleton 对象,而它本该只创建一个对象。通过同步 getInstance() 方法从而在同一时间只允许一个线程执行代码,这个问题得以改正,如清单 2 所示:
清单 2. 线程安全的 getInstance() 方法
1. public static synchronized Singleton getInstance()
2. {
3. if (instance == null) //1
4. instance = new Singleton(); //2
5. return instance; //3
6. }
清单 2 中的代码针对多线程访问getInstance() 方法运行得很好。然而,当分析这段代码时,您会意识到只有在第一次调用方法时才需要同步。由于只有第一次调用执行了 //2 处的代码,而只有此行代码需要同步,因此就无需对后续调用使用同步。所有其他调用用于决定 instance 是非 null 的,并将其返回。多线程能够安全并发地执行除第一次调用外的所有调用。尽管如此,由于该方法是 synchronized 的,需要为该方法的每一次调用付出同步的代价,即使只有第一次调用需要同步。
为使此方法更为有效,一个被称为双重检查锁定的习语就应运而生了。这个想法是为了避免对除第一次调用外的所有调用都实行同步的昂贵代价。同步的代价在不同的 JVM 间是不同的。在早期,代价相当高。随着更高级的 JVM 的出现,同步的代价降低了,但出入 synchronized 方法或块仍然有性能损失。不考虑 JVM 技术的进步,程序员们绝不想不必要地浪费处理时间。
因为只有清单 2 中的 //2 行需要同步,我们可以只将其包装到一个同步块中,如清单 3 所示:
清单 3. getInstance() 方法
1. public static Singleton getInstance()
2. {
3. if (instance == null )
4. {
5. synchronized (Singleton. class ) {
6. instance = new Singleton();
7. }
8. }
9. return instance;
10. }
清单 3 中的代码展示了用多线程加以说明的和清单 1 相同的问题。当 instance 为 null 时,两个线程可以并发地进入 if 语句内部。然后,一个线程进入 synchronized 块来初始化instance ,而另一个线程则被阻断。当第一个线程退出 synchronized 块时,等待着的线程进入并创建另一个 Singleton 对象。注意:当第二个线程进入 synchronized 块时,它并没有检查instance 是否非 null 。
双重检查锁定
为处理清单 3 中的问题,我们需要对instance 进行第二次检查。这就是“双重检查锁定”名称的由来。将双重检查锁定习语应用到清单 3 的结果就是清单 4 。
清单 4. 双重检查锁定示例
1. public static Singleton getInstance()
2. {
3. if (instance == null )
4. {
5. synchronized (Singleton. class ) { //1
6. if (instance == null) //2
7. instance = new Singleton(); //3
8. }
9. }
10. return instance;
11. }
双重检查锁定背后的理论是:在 //2 处的第二次检查使(如清单 3 中那样)创建两个不同的Singleton 对象成为不可能。假设有下列事件序列:
线程 1 进入getInstance() 方法。
由于 instance 为 null ,线程 1 在 //1 处进入synchronized 块。
线程 1 被线程 2 预占。
线程 2 进入getInstance() 方法。
由于 instance 仍旧为 null ,线程 2 试图获取 //1 处的锁。然而,由于线程1 持有该锁,线程 2 在 //1 处阻塞。
线程 2 被线程 1 预占。
线程 1 执行,由于在 //2 处实例仍旧为 null ,线程 1 还创建一个Singleton 对象并将其引用赋值给 instance 。
线程 1 退出synchronized 块并从 getInstance() 方法返回实例。
线程 1 被线程 2 预占。
线程 2 获取 //1 处的锁并检查 instance 是否为 null 。
由于 instance 是非 null 的,并没有创建第二个 Singleton 对象,由线程 1 创建的对象被返回。
双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。
双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。
无序写入
为解释该问题,需要重新考察上述清单 4 中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量instance 来引用此对象。这行代码的问题是:在 Singleton 构造函数体执行之前,变量instance 可能成为非 null 的。
什么?这一说法可能让您始料未及,但事实确实如此。在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏的。假设清单 4 中代码执行以下事件序列:
线程 1 进入getInstance() 方法。
由于 instance 为 null ,线程 1 在 //1 处进入synchronized 块。
线程 1 前进到 //3 处,但在构造函数执行之前 ,使实例成为非 null 。
线程 1 被线程 2 预占。
线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将 instance引用返回给一个构造完整但部分初始化了的 Singleton 对象。
线程 2 被线程 1 预占。
线程 1 通过运行Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。
此事件序列发生在线程 2 返回一个尚未执行构造函数的对象的时候。
为展示此事件的发生情况,假设为代码行 instance =new Singleton(); 执行了下列伪代码:
1. instance =new Singleton();
2. mem = allocate(); //Allocate memory for Singleton object.
3. instance = mem; //Note that instance is now non-null, but
4. //has not been initialized.
5. ctorSingleton(instance); //Invoke constructor for Singleton passing
6. //instance.
这段伪代码不仅是可能的,而且是一些 JIT 编译器上真实发生的。执行的顺序是颠倒的,但鉴于当前的内存模型,这也是允许发生的。JIT 编译器的这一行为使双重检查锁定的问题只不过是一次学术实践而已。
为说明这一情况,假设有清单 5 中的代码。它包含一个剥离版的getInstance() 方法。我已经删除了“双重检查性”以简化我们对生成的汇编代码(清单 6)的回顾。我们只关心JIT 编译器如何编译 instance=new Singleton(); 代码。此外,我提供了一个简单的构造函数来明确说明汇编代码中该构造函数的运行情况。
清单 5. 用于演示无序写入的单例类
1. class Singleton
2. {
3. private static Singleton instance;
4. private boolean inUse;
5. private int val;
6.
7. private Singleton()
8. {
9. inUse = true ;
10. val = 5 ;
11. }
12. public static Singleton getInstance()
13. {
14. if (instance == null )
15. instance = new Singleton();
16. return instance;
17. }
18. }
19.
清单 6 包含由 Sun JDK 1.2.1JIT 编译器为清单 5 中的getInstance() 方法体生成的汇编代码。
清单 6. 由清单 5 中的代码生成的汇编代码
1. ;asm code generated for getInstance
2. 054D20B0 mov eax,[049388C8] ;load instance ref
3. 054D20B5 test eax,eax ;test for null
4. 054D20B7 jne 054D20D7
5. 054D20B9 mov eax,14C0988h
6. 054D20BE call 503EF8F0 ;allocate memory
7. 054D20C3 mov [049388C8],eax ;store pointer in
8. ;instance ref. instance
9. ;non-null and ctor
10. ;has not run
11. 054D20C8 mov ecx,dword ptr [eax]
12. 054D20CA mov dword ptr [ecx],1 ;inline ctor - inUse= true ;
13. 054D20D0 mov dword ptr [ecx+4 ], 5 ;inline ctor - val= 5 ;
14. 054D20D7 mov ebx,dword ptr ds:[49388C8h]
15. 054D20DD jmp 054D20B0
注: 为引用下列说明中的汇编代码行,我将引用指令地址的最后两个值,因为它们都以 054D20 开头。例如,B5 代表 test eax,eax 。
汇编代码是通过运行一个在无限循环中调用 getInstance() 方法的测试程序来生成的。程序运行时,请运行 Microsoft Visual C++ 调试器并将其附到表示测试程序的 Java 进程中。然后,中断执行并找到表示该无限循环的汇编代码。
B0 和 B5 处的前两行汇编代码将 instance 引用从内存位置 049388C8 加载至 eax 中,并进行 null 检查。这跟清单 5 中的 getInstance() 方法的第一行代码相对应。第一次调用此方法时,instance 为 null ,代码执行到 B9 。BE 处的代码为Singleton 对象从堆中分配内存,并将一个指向该块内存的指针存储到 eax 中。下一行代码,C3 ,获取 eax 中的指针并将其存储回内存位置为 049388C8 的实例引用。结果是,instance现在为非 null 并引用一个有效的 Singleton 对象。然而,此对象的构造函数尚未运行,这恰是破坏双重检查锁定的情况。然后,在 C8 行处,instance 指针被解除引用并存储到 ecx 。CA 和 D0 行表示内联的构造函数,该构造函数将值 true 和 5 存储到 Singleton 对象。如果此代码在执行C3 行后且在完成该构造函数前被另一个线程中断,则双重检查锁定就会失败。
不是所有的 JIT 编译器都生成如上代码。一些生成了代码,从而只在构造函数执行后使 instance 成为非 null 。针对 Java 技术的 IBM SDK 1.3 版和 Sun JDK1.3 都生成这样的代码。然而,这并不意味着应该在这些实例中使用双重检查锁定。该习语失败还有一些其他原因。此外,您并不总能知道代码会在哪些 JVM 上运行,而 JIT 编译器总是会发生变化,从而生成破坏此习语的代码。
双重检查锁定:获取两个
考虑到当前的双重检查锁定不起作用,我加入了另一个版本的代码,如清单 7 所示,从而防止您刚才看到的无序写入问题。
清单 7. 解决无序写入问题的尝试
1. public static Singleton getInstance()
2. {
3. if (instance == null )
4. {
5. synchronized (Singleton. class ) { //1
6. Singleton inst = instance; //2
7. if (inst == null )
8. {
9. synchronized (Singleton. class ) { //3
10. inst = new Singleton(); //4
11. }
12. instance = inst; //5
13. }
14. }
15. }
16. return instance;
17. }
看着清单 7 中的代码,您应该意识到事情变得有点荒谬。请记住,创建双重检查锁定是为了避免对简单的三行 getInstance() 方法实现同步。清单 7 中的代码变得难于控制。另外,该代码没有解决问题。仔细检查可获悉原因。
此代码试图避免无序写入问题。它试图通过引入局部变量 inst 和第二个synchronized 块来解决这一问题。该理论实现如下:
线程 1 进入getInstance() 方法。
由于 instance 为 null ,线程 1 在 //1 处进入第一个synchronized 块。
局部变量 inst 获取 instance 的值,该值在 //2 处为 null 。
由于 inst 为 null ,线程 1 在 //3 处进入第二个synchronized 块。
线程 1 然后开始执行 //4 处的代码,同时使 inst 为非 null ,但在Singleton 的构造函数执行前。(这就是我们刚才看到的无序写入问题。)
线程 1 被线程 2 预占。
线程 2 进入getInstance() 方法。
由于 instance 为 null ,线程 2 试图在 //1 处进入第一个synchronized 块。由于线程 1 目前持有此锁,线程 2 被阻断。
线程 1 然后完成 //4 处的执行。
线程 1 然后将一个构造完整的Singleton 对象在 //5 处赋值给变量instance ,并退出这两个 synchronized 块。
线程 1 返回 instance 。
然后执行线程 2 并在 //2 处将 instance 赋值给 inst 。
线程 2 发现 instance 为非 null ,将其返回。
这里的关键行是 //5。此行应该确保 instance 只为 null 或引用一个构造完整的 Singleton 对象。该问题发生在理论和实际彼此背道而驰的情况下。
由于当前内存模型的定义,清单 7 中的代码无效。Java 语言规范(Java Language Specification,JLS)要求不能将 synchronized 块中的代码移出来。但是,并没有说不能将 synchronized 块外面的代码移入 synchronized 块中。
JIT 编译器会在这里看到一个优化的机会。此优化会删除 //4 和 //5 处的代码,组合并且生成清单 8 中所示的代码。
清单 8. 从清单 7 中优化来的代码。
1. public static Singleton getInstance()
2. {
3. if (instance == null )
4. {
5. synchronized (Singleton. class ) { //1
6. Singleton inst = instance; //2
7. if (inst == null )
8. {
9. synchronized (Singleton. class ) { //3
10. //inst = new Singleton(); //4
11. instance = new Singleton();
12. }
13. //instance = inst; //5
14. }
15. }
16. }
17. return instance;
18. }
如果进行此项优化,您将同样遇到我们之前讨论过的无序写入问题。
用 volatile 声明每一个变量怎么样?
另一个想法是针对变量 inst 以及 instance 使用关键字 volatile 。根据 JLS(参见参考资料 ),声明成 volatile 的变量被认为是顺序一致的,即,不是重新排序的。但是试图使用 volatile 来修正双重检查锁定的问题,会产生以下两个问题:
这里的问题不是有关顺序一致性的,而是代码被移动了,不是重新排序。
即使考虑了顺序一致性,大多数的 JVM 也没有正确地实现 volatile 。
第二点值得展开讨论。假设有清单 9 中的代码:
清单 9. 使用了 volatile 的顺序一致性
1. class test
2. {
3. private volatile boolean stop = false ;
4. private volatile int num = 0 ;
5.
6. public void foo()
7. {
8. num = 100 ; //This can happen second
9. stop = true ; //This can happen first
10. //...
11. }
12.
13. public void bar()
14. {
15. if (stop)
16. num += num; //num can == 0!
17. }
18. //...
19. }
根据 JLS,由于 stop 和 num 被声明为 volatile ,它们应该顺序一致。这意味着如果 stop 曾经是 true ,num 一定曾被设置成 100 。尽管如此,因为许多 JVM 没有实现volatile 的顺序一致性功能,您就不能依赖此行为。因此,如果线程 1 调用 foo 并且线程 2 并发地调用 bar ,则线程 1 可能在 num 被设置成为 100 之前将 stop 设置成 true 。这将导致线程见到 stop 是 true ,而 num 仍被设置成 0 。使用 volatile和 64 位变量的原子数还有另外一些问题,但这已超出了本文的讨论范围。有关此主题的更多信息,请参阅 参考资料 。
解决方案
底线就是:无论以何种形式,都不应使用双重检查锁定,因为您不能保证它在任何 JVM 实现上都能顺利运行。JSR-133是有关内存模型寻址问题的,尽管如此,新的内存模型也不会支持双重检查锁定。因此,您有两种选择:
接受如清单 2 中所示的getInstance() 方法的同步。
放弃同步,而使用一个 static 字段。
选择项 2 如清单 10 中所示
清单 10. 使用 static 字段的单例实现
1. class Singleton
2. {
3. private Vector v;
4. private boolean inUse;
5. private static Singleton instance = new Singleton();
6.
7. private Singleton()
8. {
9. v = new Vector();
10. inUse = true ;
11. //...
12. }
13.
14. public static Singleton getInstance()
15. {
16. return instance;
17. }
18. }
清单 10 的代码没有使用同步,并且确保调用 static getInstance() 方法时才创建 Singleton 。如果您的目标是消除同步,则这将是一个很好的选择。
String 不是不变的
鉴于无序写入和引用在构造函数执行前变成非 null 的问题,您可能会考虑String 类。假设有下列代码:
1. private String str;
2. //...
3. str = new String( "hello" );
4.
String 类应该是不变的。尽管如此,鉴于我们之前讨论的无序写入问题,那会在这里导致问题吗?答案是肯定的。考虑两个线程访问 String str 。一个线程能看见 str引用一个 String 对象,在该对象中构造函数尚未运行。事实上,清单 11 包含展示这种情况发生的代码。注意,这个代码仅在我测试用的旧版 JVM 上会失败。IBM 1.3 和 Sun 1.3JVM 都会如期生成不变的 String 。
清单 11. 可变 String 的例子
1. class StringCreator extends Thread
2. {
3. MutableString ms;
4. public StringCreator(MutableString muts)
5. {
6. ms = muts;
7. }
8. public void run()
9. {
10. while ( true )
11. ms.str = new String( "hello"); //1
12. }
13. }
14. class StringReader extends Thread
15. {
16. MutableString ms;
17. public StringReader(MutableString muts)
18. {
19. ms = muts;
20. }
21. public void run()
22. {
23. while ( true )
24. {
25. if (!(ms.str.equals( "hello"))) //2
26. {
27. System.out.println("String is not immutable!" );
28. break ;
29. }
30. }
31. }
32. }
33. class MutableString
34. {
35. public String str; //3
36. public static void main(String args[])
37. {
38. MutableString ms = new MutableString(); //4
39. new StringCreator(ms).start(); //5
40. new StringReader(ms).start(); //6
41. }
42. }
此代码在 //4 处创建一个MutableString 类,它包含了一个 String 引用,此引用由 //3 处的两个线程共享。在行 //5 和 //6 处,在两个分开的线程上创建了两个对象 StringCreator 和 StringReader 。传入一个MutableString 对象的引用。StringCreator 类进入到一个无限循环中并且使用值“hello”在 //1 处创建 String 对象。StringReader 也进入到一个无限循环中,并且在 //2 处检查当前的 String 对象的值是不是“hello”。如果不行,StringReader 线程打印出一条消息并停止。如果 String 类是不变的,则从此程序应当看不到任何输出。如果发生了无序写入问题,则使 StringReader 看到 str 引用的惟一方法绝不是值为“hello”的 String 对象。
在旧版的 JVM 如 Sun JDK 1.2.1上运行此代码会导致无序写入问题。并因此导致一个非不变的 String 。
结束语
为避免单例中代价高昂的同步,程序员非常聪明地发明了双重检查锁定习语。不幸的是,鉴于当前的内存模型的原因,该习语尚未得到广泛使用,就明显成为了一种不安全的编程结构。重定义脆弱的内存模型这一领域的工作正在进行中。尽管如此,即使是在新提议的内存模型中,双重检查锁定也是无效的。对此问题最佳的解决方案是接受同步或者使用一个 static field 。
单例模式与多线程
概要
单例模式是最简单的设计模式之一,但是对于Java的开发者来说,它却有很多缺陷。在本月的专栏中,David Geary探讨了单例模式以及在面对多线程(multithreading)、类装载器(classloaders)和序列化(serialization)时如何处理这些缺陷。
单例模式适合于一个类只有一个实例的情况,比如窗口管理器,打印缓冲池和文件系统,它们都是原型的例子。典型的情况是,那些对象的类型被遍及一个软件系统的不同对象访问,因此需要一个全局的访问指针,这便是众所周知的单例模式的应用。当然这只有在你确信你不再需要任何多于一个的实例的情况下。
单例模式的用意在于前一段中所关心的。通过单例模式你可以:
确保一个类只有一个实例被建立
提供了一个对对象的全局访问指针
在不影响单例类的客户端的情况下允许将来有多个实例
尽管单例设计模式如在下面的图中的所显示的一样是最简单的设计模式,但对于粗心的Java开发者来说却呈现出许多缺陷。这篇文章讨论了单例模式并揭示了那些缺陷。
注意:你可以从Resources下载这篇文章的源代码。
单例模式
在《设计模式》一书中,作者这样来叙述单例模式的:确保一个类只有一个实例并提供一个对它的全局访问指针。
下图说明了单例模式的类图。
(图1)
单例模式的类图
正如你在上图中所看到的,这不是单例模式的完整部分。此图中单例类保持了一个对唯一的单例实例的静态引用,并且会从静态getInstance()方法中返回对那个实例的引用。
例1显示了一个经典的单例模式的实现。
例1.经典的单例模式
public classClassicSingleton {
private static ClassicSingleton instance = null;
protected ClassicSingleton() {
// Exists only to defeat instantiation.
}
public static ClassicSingleton getInstance() {
if(instance == null) {
instance = newClassicSingleton();
}
return instance;
}
}
在例1中的单例模式的实现很容易理解。ClassicSingleton类保持了一个对单独的单例实例的静态引用,并且从静态方法getInstance()中返回那个引用。
关于ClassicSingleton类,有几个让我们感兴趣的地方。首先,ClassicSingleton使用了一个众所周知的懒汉式实例化去创建那个单例类的引用;结果,这个单例类的实例直到getInstance()方法被第一次调用时才被创建。这种技巧可以确保单例类的实例只有在需要时才被建立出来。其次,注意ClassicSingleton实现了一个protected的构造方法,这样客户端不能直接实例化一个ClassicSingleton类的实例。然而,你会惊奇的发现下面的代码完全合法:
public classSingletonInstantiator {
public SingletonInstantiator() {
ClassicSingleton instance = ClassicSingleton.getInstance();
ClassicSingleton anotherInstance =
new ClassicSingleton();
...
}
}
前面这个代码片段为何能在没有继承ClassicSingleton并且ClassicSingleton类的构造方法是protected的情况下创建其实例?答案是protected的构造方法可以被其子类以及在同一个包中的其它类调用。因为ClassicSingleton和SingletonInstantiator位于相同的包(缺省的包),所以SingletonInstantiator方法能创建ClasicSingleton的实例。
这种情况下有两种解决方案:一是你可以使ClassicSingleton的构造方法变化私有的(private)这样只有ClassicSingleton的方法能调用它;然而这也意味着ClassicSingleton不能有子类。有时这是一种很合意的解决方法,如果确实如此,那声明你的单例类为final是一个好主意,这样意图明确,并且让编译器去使用一些性能优化选项。另一种解决方法是把你的单例类放到一个外在的包中,以便在其它包中的类(包括缺省的包)无法实例化一个单例类。
关于ClassicSingleton的第三点感兴趣的地方是,如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
第四点,如果ClasicSingleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
最后也许是最重要的一点,就是例1中的ClassicSingleton类不是线程安全的。如果两个线程,我们称它们为线程1和线程2,在同一时间调用ClassicSingleton.getInstance()方法,如果线程1先进入if块,然后线程2进行控制,那么就会有ClassicSingleton的两个的实例被创建。
正如你从前面的讨论中所看到的,尽管单例模式是最简单的设计模式之一,在Java中实现它也是决非想象的那么简单。这篇文章接下来会揭示Java规范对单例模式进行的考虑,但是首先让我们近水楼台的看看你如何才能测试你的单例类。
测试单例模式
接下来,我使用与log4j相对应的JUnit来测试单例类,它会贯穿在这篇文章余下的部分。如果你对JUnit或log4j不很熟悉,请参考相关资源。
例2是一个用JUnit测试例1的单例模式的案例:
例2.一个单例模式的案例
import org.apache.log4j.Logger;
import junit.framework.Assert;
import junit.framework.TestCase;
public classSingletonTest extends TestCase{
private ClassicSingleton sone = null, stwo = null;
private static Logger logger = Logger.getRootLogger();
public SingletonTest(String name){
super(name);
}
public void setUp() {
logger.info("getting singleton...");
sone = ClassicSingleton.getInstance();
logger.info("...got singleton: " + sone);
logger.info("getting singleton...");
stwo = ClassicSingleton.getInstance();
logger.info("...got singleton: " + stwo);
}
public void testUnique() {
logger.info("checking singletons for equality");
Assert.assertEquals(true,sone == stwo);
}
}
例2两次调用ClassicSingleton.getInstance(),并且把返回的引用存储在成员变量中。方法testUnique()会检查这些引用看它们是否相同。例3是这个测试案例的输出:
例3.是这个测试案例的输出
Buildfile: build.xml
init:
[echo] Build 20030414 (14-04-2003 03:08)
compile:
run-test-text:
[java] .INFO main: [b]getting singleton...[/b]
[java] INFO main: [b]created singleton:[/b] Singleton@e86f41
[java] INFO main: ...got singleton: Singleton@e86f41
[java] INFO main: [b]getting singleton...[/b]
[java] INFO main: ...got singleton: Singleton@e86f41
[java] INFO main: checking singletons for equality
[java] Time: 0.032
[java] OK (1 test)
正如前面的清单所示,例2的简单测试顺利通过----通过ClassicSingleton.getInstance()获得的两个单例类的引用确实相同;然而,你要知道这些引用是在单线程中得到的。下面的部分着重于用多线程测试单例类。
多线程因素的考虑
在例1中的ClassicSingleton.getInstance()方法由于下面的代码而不是线程安全的:
1: if(instance== null) {
2: instance = newSingleton();
3: }
如果一个线程在第二行的赋值语句发生之前切换,那么成员变量instance仍然是null,然后另一个线程可能接下来进入到if块中。在这种情况下,两个不同的单例类实例就被创建。不幸的是这种假定很少发生,这样这种假定也很难在测试期间出现(译注:在这可能是作者对很少出现这种情况而导致无法测试从而使人们放松警惕而感到叹惜)。为了演示这个线程轮换,我得重新实现例1中的那个类。例4就是修订后的单例类:
例4.人为安排的方式
import org.apache.log4j.Logger;
public classSingleton {
private static Singleton singleton = null;
private static Logger logger = Logger.getRootLogger();
private static booleanfirstThread = true;
protected Singleton() {
//Exists only to defeat instantiation.
}
public static Singleton getInstance() {
if(singleton == null) {
simulateRandomActivity();
singleton = newSingleton();
}
logger.info("created singleton: " + singleton);
return singleton;
}
private static voidsimulateRandomActivity() {
try {
if(firstThread) {
firstThread = false;
logger.info("sleeping...");
// This nap should give the second thread enough time
// to get by the first thread.
Thread.currentThread().sleep(50);
}
}
catch(InterruptedException ex) {
logger.warn("Sleep interrupted");
}
}
}
除了在这个清单中的单例类强制使用了一个多线程错误处理,例4类似于例1中的单例类。在getInstance()方法第一次被调用时,调用这个方法的线程会休眠50毫秒以便另外的线程也有时间调用getInstance()并创建一个新的单例类实例。当休眠的线程觉醒时,它也会创建一个新的单例类实例,这样我们就有两个单例类实例。尽管例4是人为如此的,但它却模拟了第一个线程调用了getInstance()并在没有完成时被切换的真实情形。
例5测试了例4的单例类:
例5.失败的测试
import org.apache.log4j.Logger;
import junit.framework.Assert;
import junit.framework.TestCase;
public classSingletonTest extends TestCase{
private static Logger logger = Logger.getRootLogger();
private static Singleton singleton = null;
public SingletonTest(String name){
super(name);
}
public void setUp() {
singleton = null;
}
public void testUnique() throwsInterruptedException {
// Both threads call Singleton.getInstance().
Thread threadOne = newThread(newSingletonTestRunnable()),
threadTwo = new Thread(new SingletonTestRunnable());
threadOne.start();
threadTwo.start();
threadOne.join();
threadTwo.join();
}
private static classSingletonTestRunnable implementsRunnable {
public void run() {
// Get a reference to the singleton.
Singleton s = Singleton.getInstance();
// Protect singleton member variable from
// multithreaded access.
synchronized(SingletonTest.class) {
if(singleton == null) // If local reference isnull...
singleton = s; // ...set it to the singleton
}
// Local reference must be equal to the one and
// only instance of Singleton; otherwise, we have two
// Singleton instances.
Assert.assertEquals(true, s== singleton);
}
}
}
例5的测试案例创建两个线程,然后各自启动,等待完成。这个案例保持了一个对单例类的静态引用,每个线程都会调用Singleton.getInstance()。如果这个静态成员变量没有被设置,那么第一个线程就会将它设为通过调用getInstance()而得到的引用,然后这个静态变量会与一个局部变量比较是否相等。
在这个测试案例运行时会发生一系列的事情:第一个线程调用getInstance(),进入if块,然后休眠;接着,第二个线程也调用getInstance()并且创建了一个单例类的实例。第二个线程会设置这个静态成员变量为它所创建的引用。第二个线程检查这个静态成员变量与一个局部备份的相等性。然后测试通过。当第一个线程觉醒时,它也会创建一个单例类的实例,并且它不会设置那个静态成员变量(因为第二个线程已经设置过了),所以那个静态变量与那个局部变量脱离同步,相等性测试即告失败。例6列出了例5的输出:
例6.例5的输出
Buildfile: build.xml
init:
[echo] Build 20030414 (14-04-2003 03:06)
compile:
run-test-text:
INFO Thread-1: sleeping...
INFO Thread-2: created singleton:Singleton@7e5cbd
INFO Thread-1: created singleton:Singleton@704ebb
junit.framework.AssertionFailedError:expected: but was:
atjunit.framework.Assert.fail(Assert.java:47)
atjunit.framework.Assert.failNotEquals(Assert.java:282)
atjunit.framework.Assert.assertEquals(Assert.java:64)
atjunit.framework.Assert.assertEquals(Assert.java:149)
atjunit.framework.Assert.assertEquals(Assert.java:155)
atSingletonTest$SingletonTestRunnable.run(Unknown Source)
atjava.lang.Thread.run(Thread.java:554)
[java] .
[java] Time: 0.577
[java] OK (1 test)
到现在为止我们已经知道例4不是线程安全的,那就让我们看看如何修正它。
同步
要使例4的单例类为线程安全的很容易----只要像下面一个同步化getInstance()方法:
public synchronizedstatic Singleton getInstance(){
if(singleton == null) {
simulateRandomActivity();
singleton = newSingleton();
}
logger.info("created singleton: " + singleton);
return singleton;
}
在同步化getInstance()方法后,我们就可以得到例5的测试案例返回的下面的结果:
Buildfile: build.xml
init:
[echo] Build 20030414 (14-04-2003 03:15)
compile:
[javac] Compiling 2 source files
run-test-text:
INFO Thread-1: sleeping...
INFO Thread-1: created singleton:Singleton@ef577d
INFO Thread-2: created singleton:Singleton@ef577d
[java] .
[java] Time: 0.513
[java] OK (1 test)
这此,这个测试案例工作正常,并且多线程的烦恼也被解决;然而,机敏的读者可能会认识到getInstance()方法只需要在第一次被调用时同步。因为同步的性能开销很昂贵(同步方法比非同步方法能降低到100次左右),或许我们可以引入一种性能改进方法,它只同步单例类的getInstance()方法中的赋值语句。
一种性能改进的方法
寻找一种性能改进方法时,你可能会选择像下面这样重写getInstance()方法:
public staticSingleton getInstance() {
if(singleton == null) {
synchronized(Singleton.class) {
singleton = newSingleton();
}
}
return singleton;
}
这个代码片段只同步了关键的代码,而不是同步整个方法。然而这段代码却不是线程安全的。考虑一下下面的假定:线程1进入同步块,并且在它给singleton成员变量赋值之前线程1被切换。接着另一个线程进入if块。第二个线程将等待直到第一个线程完成,并且仍然会得到两个不同的单例类实例。有修复这个问题的方法吗?请读下去。
双重加锁检查
初看上去,双重加锁检查似乎是一种使懒汉式实例化为线程安全的技术。下面的代码片段展示了这种技术:
public staticSingleton getInstance() {
if(singleton == null) {
synchronized(Singleton.class) {
if(singleton == null) {
singleton = newSingleton();
}
}
}
return singleton;
}
如果两个线程同时访问getInstance()方法会发生什么?想像一下线程1进行同步块马上又被切换。接着,第二个线程进入if 块。当线程1退出同步块时,线程2会重新检查看是否singleton实例仍然为null。因为线程1设置了singleton成员变量,所以线程2的第二次检查会失败,第二个单例类实例也就不会被创建。似乎就是如此。
不幸的是,双重加锁检查不会保证正常工作,因为编译器会在Singleton的构造方法被调用之前随意给singleton赋一个值。如果在singleton引用被赋值之后而被初始化之前线程1被切换,线程2就会被返回一个对未初始化的单例类实例的引用。
一个改进的线程安全的单例模式实现
例7列出了一个简单、快速而又是线程安全的单例模式实现:
例7.一个简单的单例类
public classSingleton {
public final static SingletonINSTANCE = new Singleton();
private Singleton() {
// Exists only to defeat instantiation.
}
}
这段代码是线程安全的是因为静态成员变量一定会在类被第一次访问时被创建。你得到了一个自动使用了懒汉式实例化的线程安全的实现;你应该这样使用它:
Singleton singleton =Singleton.INSTANCE;
singleton.dothis();
singleton.dothat();
...
当然万事并不完美,前面的Singleton只是一个折衷的方案;如果你使用那个实现,你就无法改变它以便后来你可能想要允许多个单例类的实例。用一种更折哀的单例模式实现(通过一个getInstance()方法获得实例)你可以改变这个方法以便返回一个唯一的实例或者是数百个实例中的一个.你不能用一个公开且是静态的(public static)成员变量这样做.
你可以安全的使用例7的单例模式实现或者是例1的带一个同步的getInstance()方法的实现.然而,我们必须要研究另一个问题:你必须在编译期指定这个单例类,这样就不是很灵活.一个单例类的注册表会让我们在运行期指定一个单例类.
使用注册表
使用一个单例类注册表可以:
在运行期指定单例类
防止产生多个单例类子类的实例
在例8的单例类中,保持了一个通过类名进行注册的单例类注册表:
例8 带注册表的单例类
import java.util.HashMap;
import org.apache.log4j.Logger;
public classSingleton {
private static HashMap map = newHashMap();
private static Logger logger = Logger.getRootLogger();
protected Singleton() {
// Exists only to thwart instantiation
}
public static synchronizedSingleton getInstance(String classname) {
if(classname == null) throw newIllegalArgumentException("Illegal classname");
Singleton singleton = (Singleton)map.get(classname);
if(singleton != null) {
logger.info("got singleton from map:" + singleton);
return singleton;
}
if(classname.equals("SingeltonSubclass_One"))
singleton = newSingletonSubclass_One();
else if(classname.equals("SingeltonSubclass_Two"))
singleton = newSingletonSubclass_Two();
map.put(classname, singleton);
logger.info("created singleton: " + singleton);
return singleton;
}
//Assume functionality follows that's attractive to inherit
}
这段代码的基类首先创建出子类的实例,然后把它们存储在一个Map中。但是基类却得付出很高的代价因为你必须为每一个子类替换它的getInstance()方法。幸运的是我们可以使用反射处理这个问题。
使用反射
在例9的带注册表的单例类中,使用反射来实例化一个特殊的类的对象。与例8相对的是通过这种实现,Singleton.getInstance()方法不需要在每个被实现的子类中重写了。
例9 使用反射实例化单例类
import java.util.HashMap;
import org.apache.log4j.Logger;
public classSingleton {
private static HashMap map = newHashMap();
private static Logger logger = Logger.getRootLogger();
protected Singleton() {
// Exists only to thwart instantiation
}
public static synchronizedSingleton getInstance(String classname) {
Singleton singleton = (Singleton)map.get(classname);
if(singleton != null) {
logger.info("got singleton from map: " + singleton);
return singleton;
}
try {
singleton = (Singleton)Class.forName(classname).newInstance();
}
catch(ClassNotFoundException cnf){
logger.fatal("Couldn't find class " + classname);
}
catch(InstantiationException ie) {
logger.fatal("Couldn't instantiate an object of type " +classname);
}
catch(IllegalAccessException ia) {
logger.fatal("Couldn't access class " + classname);
}
map.put(classname, singleton);
logger.info("created singleton: " + singleton);
return singleton;
}
}
关于单例类的注册表应该说明的是:它们应该被封装在它们自己的类中以便最大限度的进行复用。
封装注册表
例10列出了一个单例注册表类。
例10 一个SingletonRegistry类
import java.util.HashMap;
import org.apache.log4j.Logger;
public classSingletonRegistry {
public static SingletonRegistry REGISTRY = new SingletonRegistry();
private static HashMap map = newHashMap();
private static Logger logger = Logger.getRootLogger();
protected SingletonRegistry() {
// Exists to defeat instantiation
}
public static synchronizedObject getInstance(String classname) {
Object singleton = map.get(classname);
if(singleton != null) {
return singleton;
}
try {
singleton = Class.forName(classname).newInstance();
logger.info("created singleton: " + singleton);
}
catch(ClassNotFoundException cnf){
logger.fatal("Couldn't find class " + classname);
}
catch(InstantiationException ie) {
logger.fatal("Couldn't instantiate an object of type " +
classname);
}
catch(IllegalAccessException ia) {
logger.fatal("Couldn't access class " + classname);
}
map.put(classname, singleton);
return singleton;
}
}
注意我是把SingletonRegistry类作为一个单例模式实现的。我也通用化了这个注册表以便它能存储和取回任何类型的对象。例11显示了的Singleton类使用了这个注册表。
例11 使用了一个封装的注册表的Singleton类
import java.util.HashMap;
import org.apache.log4j.Logger;
public classSingleton {
protected Singleton() {
// Exists only to thwart instantiation.
}
public static Singleton getInstance() {
return(Singleton)SingletonRegistry.REGISTRY.getInstance(classname);
}
}
上面的Singleton类使用那个注册表的唯一实例通过类名取得单例对象。
现在我们已经知道如何实现线程安全的单例类和如何使用一个注册表去在运行期指定单例类名,接着让我们考查一下如何安排类载入器和处理序列化。
Classloaders
在许多情况下,使用多个类载入器是很普通的--包括servlet容器--所以不管你在实现你的单例类时是多么小心你都最终可以得到多个单例类的实例。如果你想要确保你的单例类只被同一个的类载入器装入,那你就必须自己指定这个类载入器;例如:
private staticClass getClass(String classname)
throws ClassNotFoundException {
ClassLoader classLoader =Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = Singleton.class.getClassLoader();
return(classLoader.loadClass(classname));
}
}
这个方法会尝试把当前的线程与那个类载入器相关联;如果classloader为null,这个方法会使用与装入单例类基类的那个类载入器。这个方法可以用Class.forName()代替。
序列化
如果你序列化一个单例类,然后两次重构它,那么你就会得到那个单例类的两个实例,除非你实现readResolve()方法,像下面这样:
例12 一个可序列化的单例类
import org.apache.log4j.Logger;
public classSingleton implementsjava.io.Serializable {
public static Singleton INSTANCE = newSingleton();
protected Singleton() {
// Exists only to thwart instantiation.
}
private Object readResolve() {
return INSTANCE;
}
}
上面的单例类实现从readResolve()方法中返回一个唯一的实例;这样无论Singleton类何时被重构,它都只会返回那个相同的单例类实例。
例13测试了例12的单例类:
例13 测试一个可序列化的单例类
import java.io.*;
import org.apache.log4j.Logger;
import junit.framework.Assert;
import junit.framework.TestCase;
public classSingletonTest extends TestCase{
private Singleton sone = null, stwo = null;
private static Logger logger = Logger.getRootLogger();
public SingletonTest(String name){
super(name);
}
public void setUp() {
sone = Singleton.INSTANCE;
stwo = Singleton.INSTANCE;
}
public void testSerialize() {
logger.info("testing singleton serialization...");
[b] writeSingleton();
Singleton s1 = readSingleton();
Singleton s2 = readSingleton();
Assert.assertEquals(true, s1== s2);[/b] }
private void writeSingleton() {
try {
FileOutputStream fos = newFileOutputStream("serializedSingleton");
ObjectOutputStream oos = newObjectOutputStream(fos);
Singleton s = Singleton.INSTANCE;
oos.writeObject(Singleton.INSTANCE);
oos.flush();
}
catch(NotSerializableException se){
logger.fatal("Not Serializable Exception: " +se.getMessage());
}
catch(IOException iox) {
logger.fatal("IO Exception: " + iox.getMessage());
}
}
private Singleton readSingleton(){
Singleton s = null;
try {
FileInputStream fis = newFileInputStream("serializedSingleton");
ObjectInputStream ois = newObjectInputStream(fis);
s = (Singleton)ois.readObject();
}
catch(ClassNotFoundException cnf){
logger.fatal("Class Not Found Exception: " +cnf.getMessage());
}
catch(NotSerializableException se){
logger.fatal("Not Serializable Exception: " +se.getMessage());
}
catch(IOException iox) {
logger.fatal("IO Exception: " + iox.getMessage());
}
return s;
}
public void testUnique() {
logger.info("testing singleton uniqueness...");
Singleton another = newSingleton();
logger.info("checking singletons for equality");
Assert.assertEquals(true,sone == stwo);
}
}
前面这个测试案例序列化例12中的单例类,并且两次重构它。然后这个测试案例检查看是否被重构的单例类实例是同一个对象。下面是测试案例的输出:
Buildfile: build.xml
init:
[echo] Build 20030422 (22-04-200311:32)
compile:
run-test-text:
[java] .INFO main: testing singleton serialization...
[java] .INFO main: testing singleton uniqueness...
[java] INFO main: checking singletons for equality
[java] Time: 0.1
[java] OK (2 tests)
单例模式结束语
单例模式简单却容易让人迷惑,特别是对于Java的开发者来说。在这篇文章中,作者演示了Java开发者在顾及多线程、类载入器和序列化情况如何实现单例模式。作者也展示了你怎样才能实现一个单例类的注册表,以便能够在运行期指定单例类。
单例模式的优缺点
1、时间和空间
比较上面两种写法:懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。
2、线程安全
(1)从线程安全性上讲,不加同步的懒汉式是线程不安全的,比如,有两个线程,一个是线程A,一个是线程B,它们同时调用getInstance方法,那就可能导致并发问题。如下示例:
1. public static Singleton getInstance(){
2. if(instance == null){
3.
4.
5.
6. instance = new Singleton();
7. }
8. return instance;
9. }
程序继续运行,两个线程都向前走了一步,如下:
1. public static Singleton getInstance(){
2. if(instance == null){
3.
4.
5.
6.
7.
8.
9.
10.
11.
12. instance = new Singleton();
13. }
14. return instance;
15. }
可能有些朋友会觉得文字描述还是不够直观,再来画个图说明一下,如图5.4所示。
通过图5.4的分解描述,明显地看出,当A、B线程并发的情况下,会创建出两个实例来,也就是单例的控制在并发情况下失效了。
(2)饿汉式是线程安全的,因为虚拟机保证只会装载一次,在装载类的时候是不会发生并发的。
(3)如何实现懒汉式的线程安全呢?
当然懒汉式也是可以实现线程安全的,只要加上synchronized即可,如下:
1. public static synchronized Singleton getInstance(){}
但是这样一来,会降低整个访问的速度,而且每次都要判断。那么有没有更好的方式来实现呢?
(4)双重检查加锁
可以使用"双重检查加锁"的方式来实现,就可以既实现线程安全,又能够使性能不受到很大的影响。那么什么是"双重检查加锁"机制呢?
所谓双重检查加锁机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
看看代码可能会更加清楚些。示例代码如下:
1. public class Singleton {
2. /**
3. * 对保存实例的变量添加volatile的修饰
4. */
5. private volatile static Singleton instance = null;
6. private Singleton(){
7. }
8. public static Singleton getInstance(){
9. //先检查实例是否存在,如果不存在才进入下面的同步块
10. if(instance == null){
11. //同步块,线程安全地创建实例
12. synchronized(Singleton.class){
13. //再次检查实例是否存在,如果不存在才真正地创建实例
14. if(instance == null){
15. instance = new Singleton();
16. }
17. }
18. }
19. return instance;
20. }
21. }
这种实现方式可以实现既线程安全地创建实例,而又不会对性能造成太大的影响。它只是在第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。
并发/并行编程中多线程编译占有重要的地位,编写一份线程安全的程序即使对于一名经验丰富的开发人员也是一种挑战。本文将通过详解线程安全的上的相关概念,确定出一系列的原则,帮忙多线程程序的开发人员能够容易的检查判断自己所编译的程序是否是线程安全的。相信这部分内容对所有有计划或已经编写过多线程程序的程序员会有一定的帮助。
同时根据部分易于产生线程安全问题的情况,针对C++编写多线程程序给出了一些实用的技术及相关代码供大家参考。这部分内容的阅读需要读者对C++有一定的了解;当然就其给出的思想也部分适用于其它面象对象的编程语言。
可重入性(reentrant)是针对函数的,它有两个方面的内涵:
1, 可并行/并发①[vollin1] 同时进入:指可重入函数被某任务调用时,其它任务可同时进行调用而不产生错误的结果;或称在相同的输入情况下可重入函数的执行所产生的效果,并不因其并发的调用而产生不同,也称并发安全。
2, 中断后可重新进入:指可重入函数可被任意的中断,当中断执行完毕,返回断点可以继续正确的运行下去;或称在相同的输入情况下可重入函数的执行所产生的结果,并不因为在函数执行期间有中断的调用而产生不同,也称中断安全。
线程安全(MT-safe)不仅仅针对函数,它主要是指数据或程序的安全这一结果,所以有不同的层次上讲的线程安全:如线程安全的函数,线程安全的库②[vollin2] 。本文还会引入线程安全的类这一概念。通常意义上一个线程安全的函数是指有可重入函数第一个内涵的函数即并发安全的函数。但需要注意的是即使是一个从函数级别上并不安全的函数,如果使其不安全的因素在特定应用中并不存在时,这个函数对于该应用来讲同样也是线程安全的。例如对于全局变量的访问,一般而言未命名同步方式访问肯定是非线程安全的,但如果所有可能同时发生的访问均是只读访问则从结果上讲也是线程安全的。
信号的本质软中断,中断在本质上是异步的,所谓异步信号安全同线程安全一样,也是占在结果上考虑的,指在信号的中断处理过程中的安全。通常意义上一个异步信号安全的函数是指可以在异步信息处理函数中调用而不会出现异常的函数。同样需要注意到即使一个从函数级别上并非异步信息安全的函数,如果在信息处理函数中调用,也并不一定会产生不安全的结果。
对于一个多线程程序的安全来讲,通常包括了线程安全和异步信号安全这两个部分。从函数级别考虑,仅从概念上就可以发现可重入函数一定是线程安全函数,也是异步信号安全函数;多线程安全函数却要弱得多,并非一定要是可重入函数,它只要求并发无误即可;虽然异步信号函数与可重入函数的描述方式有所不同,但两者从实现层面上讲是完全一致的,或称可重入函数与异步信号安全函数这两个概念是等价的。
如何编写或判断一个函数是否是可重入的呢?以下具体例出的一些经验将帮助您的编写和判断。
(一) 纯代码(purecode)函数一定是可重入函数
如果你编写的一个函数中所使用的所有数据均是局部变量(可以为寄存器变量,因为中断时寄存器中的数据也会入栈的)或形参,没有使用任何外部的全局变量和内部的静态变量,也没有使用系统函数,这样的函数就可以称为纯代码。
例:
|
特别提示:一般仅仅只有仅用于计算的函数才属于此类型。
使用局部静态变量的函数都是不可重入的,但可以通过改变参数的方式进行简单的调整使其可重入,以下是一个示例:
|
特别提示:在函数内使用静态变量,但并不返回静态变量相关的引用,指针等的函数可以通过对静态变量的加解锁机制使其线程安全;但如果有将静态变量的相关信息返回就一定时非线程安全的。
调用了自己编写的函数自不必待言,本节主要说明如何判断一个系统函数是否一个可重入的函数,以使得容易判断自己编写的调用了系统函数的函数是否可重入:
1, 系统函数的函数名中带有“_r”后缀的函数都是可重入的,其对应的无后缀的函数是不可重入的。这类函数多是字符串相关,时间相关的函数,路径地址相关函数;其它以非入参指针为返回值的函数也应当重点怀疑可跳到第4点进行判断;
特别提示:这类非“_r”函数均在函数体中引用了全局变量或静态变量,所以也都是非线程安全的函数。
2, 内存分配与释放函数是不可重入的,因为内存的分配释放函数维护了一个链表,这使得在正常处理函数中调用分配函数时,被中断,且在中断中再调用内存分配释放函数则会出现异常;
特别提示:这类函数是线程安全的。
3, IO相关函数是不可重入的,如printf,及其它与文件描述符相关的操作函数(文件IO,网络IO,管道等本地同步IO);
特别提示:不涉及文件句柄的IO函数是线程安全的,涉及文件描述符的函数如果不是在对同一句柄操作则是线程安全的,仅当在需要多线程对同一文件描述符做操作时才需要添加同步机制,当然更好的办法应该是避免多线程有同时操作同一文件描述符的操作;特别的pread与pwrite即使在对同一个文件描述符操作时也是线程安全。
4, 最后一招:查系统man 手册,或IEEE Std1003.1,例:关于usleep,IEEE Std1003.1中有如下描述
Theusleep() function need not bereentrant. A function that is not required to be reentrant is not required tobe thread-safe.
这表明这个函数即非可重入的,也非线程安全的。
同步机制对于可重入是没有帮助的,或者说在中断面前同步机制是无效的,所以有同步机制的函数肯定是不可重入的,也当然不能够在信号处理函数中调用,但这却是最常用也最有效的消除线程不安全因素的手段。
特别提示:使用自旋锁进行同步是一种比较特殊的情况,自旋锁的实现在各个平台上都有所不同,而且也需要做特殊的处理才能够用于信号处理函数中,所以从严格意义上讲仍然不能算是可重入的函数。
三、 Easy线程安全
前文提到做为一个多线程程序,它的安全性需要从异步信号安全和线程安全两方面考虑,下面提供了一些建议及一些技巧,以使得保证多线程安全更加轻松如意。
信号处理函数对于多进程(特指每进程仅有一个线程的多进程)编程而言,是一个非常强有力的工具,因为对具体的每个进程而言不具有并发性所以需要有一些异步的手段来解决实际的编程问题,并且由于仅有一个线程,所以可以控制信号在希望出现的地方出现,这样能够避开在信号处理函数中仅能调用可重入函数的限制。但对于多线程编程而言信号处理的必要性就大大减弱,完全可以被多线程自身的并发性解决;另一方面由于多线程的并发造成无法保证所有的线程都在同一个段代码中等待信号的出现,这是与多线程编程的思想是相饽的,这也造成了如果要编写安全的信号处理函数,所受到的限制非常多,难于写出强大的信号处理函数。
一种简单易用,且绝对“安全”的信号处理方案如下:
在这个信号处理方案中,信号处理函数被弱化到仅仅做为信号收集的程度,将通过异步处理信号的方式来避免信号处理函数中可能带来的不安全因素。通过代码阅读大家可以发现前文在安全上加引号的原因:该方案在两个或以上信号在很短时间之内同时发生时,有可能产生覆盖,但是可以绝对的保证程序的安全,如果程序的主要处理不依赖信号,仅用信号来处理一些关键性的非频繁事件(如配置重载,程序退出等),则此方案可以绝对安全有效的完成。当然这也是基于前面的假设,我们要尽可能的弱化信号在程序处理中的做用。
实际上可以对以上方案做出少量的修改使得更加安全,但在信号处理的弱化这一大前提下,这样的改进也并非必要。
如果一定要编写信号处理函数,那么需要特别注意在这里只能使用可重入的函数,否则极易出现段异常,慎之慎之。
前文对函数的线程安全做了较多的讨论,如果对于纯函数式的语言如C来讲基本足够了,但对于面向对象的语言如C++,我们要使得程序安全,那么编写线程安全的类就变得重要了。
线程安全的类是怎样的一个概念呢?
1,从STL的线程安全开始
并没有相关的文章专门讨论过类的线程安全,但似乎我们又经常在用,比如我们经常听到说stl容器是线程安全的,但又有文章称真正线程安全的stl容器并不存在,它们的安全需要程序员自己保证。怎么回事呢?我们要可以查到关于stl容器线程安全相关的声明:
n 多个读取者是安全的。多线程可能同时读取一个容器的内容,这将正确地执行。当然,在读取时不能有任何写入者操作这个容器。
n 对不同容器的多个写入者是安全的。多线程可以同时写不同的容器。
这样的声明清晰而不足够,本文引入线程安全的类这样的概念不但将有利用大家理解上面关于stl线程安全与不安全的争论,而且将利于大家编写出线程安全的类以及在此基础之上的线程安全的程序。
2,线程安全类的定义
一个线程安全的类是指满足以下条件的类:
Ø 静态成员函数是线程安全的,即在多个线程调用该类的同一静态成员函数,能够正确的执行;
Ø 对于同一实例的const成员函数调用,可以同时安全的进行,当然在此过程中该实例不能有非const成员函数的调用或成员变量的写入操作;
Ø 非静态成员函数对于不同的实例是线程安全的,即可以在多个线程中以不同的实例调用同一动态成员函数,能够正确的执行。
只要一个类满足以上的三个条件,我们就可以在不同的线程中同时安全的使用该类的不同实例;如果需要多线程访问该类的同一实例时也只需要同使用一般全局数据变量一样,即:如果只有同时的读取则可以直接使用,如果有可能有写入操作那么就应该使用同步的方式来保证其数据的安全。
怎样理解这三个条件呢?类从设计的角度来讲就是一组相关的数据及其上的操作的集合,即类同时具有数据及函数两种特质,所以要使其安全也须要考虑这两个方面的因素。
a) 类的静态成员函数在使用方式上实际上同全局函数相差无几,所以我们对它的要求就如果一般的全局函数一样;所以一个线程安全的需要有线程安全的静态函数;
b) 类的具体实例数据(非静态数据)的访问上实际与一般的数据变量的访问相差无几,所以要求也一样:在只有只读访问时是完全的,在有写操作参与时所有操作均需要同步;而const成员函数在使用上是只当作对实例的只读访问的,所以需要这样的保证,实际上只需要多注意一下mutable这种数据成员变量,做出控制就可以达到这一点;
c) 类的非静态成员函数不仅可能涉及到具体实例的数据访问(这应当由类的使用者去保证安全),而且它又同时是一个函数,要线程安全当然需要保证在对全局变量或静态变量的使用符合线程安全的规范,这样才能够保证不同实例在多线程实用同一非静态函数时不会受到相互的影响。
d) 这三类成同函数是有交集的,不过很明显三者的要求,是依次减弱的,即对于静态const成员函数需要满足静态成员函数的要求;非静态const成员函数需要满足const成员函数的要求。
3,到STL结束
回来STL的线程安全与不安全的争论,我们可以这样理解了:STL容器类是线程安全的类,但对于同一实例的多线程访问,还是需要由使用者去保证安全的。这就如一个其它任何共享数据要在多线程间共享一样,不外如是。
当然也应该注意到一个线程安全的类,与一个安全的结果仍然是不同的。很多时候我们自己定义的类,其实并不满足线程安全的条件,但只要注意其使用方式也是可以保证线程安全的。但如果是编写一个提供给其它程序员使用的类,则一定要注意线程安全类的规则,如果有不符的地方一定要做出特别的说明避免使用者进入陷阱,否则可是会遭人腹绯的哦。
多线程编程的一个极大的好处是多个线程共享存储空间,使得共享数据较多进程方便了不知几许倍。但数据的共享也带来了同步的问题,这无疑是多线程安全中最为重要的一环,也是多线程编程的重点之一,更是易于出现错误的地方,所以需要通过提高其抽象层次降低其出现错误的可能。
多线程同步的机制/方式多种多样,事件,信号量,条件变量,原子操作,锁,关键代码段等;仅拿锁来讲,就有各种分类的方式,各种锁如互拆锁,读写锁,自旋锁,递归锁等,以及他们的组合如递归自旋锁,自旋读写锁等;这些具体方式的讨论主要与同步效率有关,本文主要讨论线程安全所以对同步的具体方式和锁的类型不作多的讨论,仅以POSIX下最常用的默认线程互斥锁为例给出一些技巧以使得数据同步更加方便且不易出现错误。
1,共享数组的组织
面向对象编程的一个极大的好处是可以提高编程设计的层次,这主要体现在能够将一些相关的数据及操作组织在一起形成一个单独的逻辑体――类。毫无疑问我们应当将总是在一起访问的共享数据组织在一起。另外我们发现每组共享数据的使用总是与一个锁同时使用③[vollin3] 。根据类的设计原则,这个锁对象无疑也应当与数据组织在一起。这将使得共享数据的访问更加方便和容易,然而如果我们只有一项数据要为这项数据专门设计一个类,是否反而会带来麻烦?
其实自从有了模板类以后,这样的烦恼是有解决办法的,以下的代码给出了一个解决方案。
具体的实现很简单,因为涉及后文讲的技巧,此处没有列出,具体的实现可参见附录给出的代码。其中关于锁成员采用的是mutable 类型,这使得非数据的修改都可以是const的,在使用上更像是对常量的访问。JXMutex类是一个简单的互斥锁类。
可以看到有了这样的一个模板类,就可以很方便的将一个数据类型与一个互斥锁组织在了一起,从该模板类提供了一些简单安全设置及获取数据的操作。有点需要注意的是_Tp& Data(void)这个函数,及取出的数据引用都需要在锁的保护下操作;根据这样的思路,我们也可以针对具体的一些常用共享数据类型做一些扩展,甚至可以使得它们可以像一般的数据一样简单的使用,如下面这个安全数字类:
有了这样的一个安全数字类,关于数字的相关处理全都封装成线程安全的方法,要写一个全局计算器,只需要:
同样我们还可以定义出使作stl的安全序列容器,当然只能包括一些常用的操作,如常对共享的stl序列容器做的操作push_back,swap等,所有的方法都包装是不太现实的。但对于我们向共享序列中加入新的项,或全面处理时都会更加简单高效,而不易产生错误。
将锁与数据组成为一个新的类这种方式与为每组共享数据定义一个全局的锁这种方式来讲,不但少了为各种全局锁取名的麻烦,锁与数据对应的麻烦,减少了出错的可能;更令程序的数据组织结构显得清晰易读,程序的易读性在很多时候甚至比效率更加的重要。
2,异常安全的锁使用
锁的加解锁需要严格配对,否则将造成死锁。也许这并不是一个很严格的要求,似乎程序员只要仔细一些就可以满足这个要求;然而实际并不尽如此。试想以下几种场景:
a) 在锁保护的代码段中便需要return:难道只能使用C++开发人员难以接受的goto语句?又或者组织一个夸张的多层次嵌套的条件从句?这样当然也能够完成任务,但出现疏漏的可能性大量增加,代码的阅读也将令人窒息;
b) 在锁保护的代码段中调用了一些可能产生异常的函数:难道每次加锁后都需要加一个try,catch语句?然而这些异常并不应该在本函数层次中进行处理啊,这将令写代码与阅读代码都成为一种折磨。当然更多的情况是没有try,异常还是产生了,并由上一级代码处理,而死锁也就这样产生了。
怎样才能写出异常安全的锁的使用?怎样既保证加的锁得到释放而又不用花费太多的心力?很自然的,我们想起了类的构造和析构,如果不是使用new,他们就是严格配对的,并且对于析构的控制并不需要手工的进行,在跨出了定义对象的大括号时④[vollin4] ,就自动的析构了。下面给出了一个辅助加锁类的实现。
这个实现如果我们需要对一个包含Lock(),Unlock()包成员函数的锁类或JSafe类实现加解锁,只需要在需要在一个大括号范围内定义一个JLockHelper对象即可,当大括号退出则自动解锁,如果某段代码中可能用到多个锁,也最好使用一组嵌套的大括号,将每个锁的控制范围括起来,这将使得每个锁的控制范围都更加的清晰易读。
如果使用Lock宏,则更加简单,因为每个辅助加锁变量的名字都相同,所以如果不用嵌套的大括号分开,还会出现编译器的错误提示,提示程序员需要加上大括号以标记不同锁的作用范围。
我们可以在任意的位置return出当前函数,当前函数中产生任何的异常也不用担心,辅助加锁类的析构函数会自动帮且我们解除锁定。
具体的使用方式如下:
这样的使用方式是否令你感觉到对于多线程的同步来讲,真的是非常简单且决计不易出错。附录1还给出了另一组几乎类似的实现方案,可以使得辅助对象的使用Easy到令你心动以至于想立即试用一下。
3,其它的同步技巧
关于此点,其实已经超出来线程安全的讨论范围,不过与前两点的使用有关,所以随便提及:
a) 关于JSafe类的成员函数Data(),必须在锁控制下操作,并且其取出的数据也必须在锁控制下操作,这实际上似乎是一种退化,这个函数也不过是为了该类的完整才提供出来。可以说在绝大多数的情况下不应该使用这个函数。首先对于一个简单的数据对象,如原生数据对象,这个函数是没有使用必要的可以用Value()函数来取代;其次对于一个复杂的数据对象,可以从JSafe中继承出来,为其特殊操作,做一个安全的包装,以使得其操作能够更加安全的进行;
b) stl容器对象,在多线程的数据交互中有很重要的地位,如何安全并且高效的使用?这里需要特别注意一个叫做swap的函数,特别对于非连续存储的list,map之类它的代价非常之低,以至于你可以在需要处理时先用一个空对象swap出来,有必要再swap或添加到同步数据中去,这不但能够避免安全风险,更能够减少锁定的时间,提高程序效率。
对于前面提到的一些技巧,给出了一个具体的实例,包括安全对象及辅助加锁类的实现(见附录1),以及一个类似于文件过滤器的多线程小程序(见附录2)。
大家可以在附录中找到具体的代码,代码可以在linux下g++ *.cpp –lpthread直接编译通过。大家可以从实例中看到,对于安全对象的使用会带来多大的便利,与使用原始的同步方式相比,不仅对于代码可阅读性产生无可比拟的提升,更使得多线程的编写成为了一种乐趣。
虽然本文提供了一些使得我们编写安全的多线程程序更加容易的方法和技巧,但是我仍然还是要说使一个多线程程序安全是一门艺术,也是一个挑战。它所涉及的内容远远超过本文所述,但它也并非那么遥不可及,它是我们触手可及的,只要我们多用心,多总结。
1.《使用可重入函数进行更安全的信号处理》
http://www.ibm.com/developerworks/cn/linux/l-reent.html
2.《IEEE Std 1003.1》 参见1中的参考资料
3.《多线程编程指南》http://docs.sun.com/app/docs/doc/819-7051/mtintro-75924
/*
*=====================================================================================
*
* Filename: safe.h
*
* Description: MT safe
*
* Version: 1.0
* Created: 04/30/2009 01:00:58 PM CST
* Revision: none
* Compiler: gcc
*
* Author: vollinwan,
* Company: Tencent Tech Ltd
*
*=====================================================================================
*/
#include <sys/types.h>
#include <pthread.h>
#ifndef __VOLLIN_J_SAFE_H__
#define __VOLLIN_J_SAFE_H__
#define LOCK(Mutex) JLockHelper __lock__(Mutex)
class JLockHelper;
struct JAbsXMutex
{
virtual voidLock() const= 0;
virtual voidUnlock() const= 0;
};
/*****************************************************
* JXMutex X锁(互斥锁)类
*****************************************************/
class JXMutex:public JAbsXMutex
{
public:
JXMutex()
{
pthread_mutex_init (&m_Mutex,NULL);
}
virtual~JXMutex()
{
pthread_mutex_destroy(&m_Mutex);
}
private:
JXMutex(constJXMutex&); //禁止锁拷贝
constJXMutex& operator =(const JXMutex &); //禁止锁赋值
public:
void Lock()const
{
pthread_mutex_lock (&m_Mutex); //锁定
}
void Unlock()const
{
pthread_mutex_unlock (&m_Mutex); //解锁
}
public:
mutablepthread_mutex_t m_Mutex; //互斥锁
};
/**************************************************************************************
* JSafe 基于各类锁的安全类模板
**************************************************************************************/
template<class _Tp,class _Mutex = JXMutex>
class JSafe
{
public:
typedef _Tpvalue_type;
typedef_Mutex lock_type;
JSafe(void){}
~JSafe(void){}
explicitJSafe(const _Tp& other)
{
m_Data =other;
}
JSafe(constJSafe& other)
{
LOCK(other);
m_Data =other.m_Data;
}
operatorconst _Tp() const //定义强制类型转换,注意已锁定时不能使用;虽然使用起来方便,但实际上都可以由获取值的Value函数代替
{
LOCK(m_Mutex);
returnm_Data;
}
JSafe&operator =(const JSafe& other)
{
LOCK(other);
{
LOCK(m_Mutex);
m_Data = other.m_Data;
}
return*this;
}
voidValue(const _Tp& other)//设置值
{
LOCK(m_Mutex);
m_Data =other;
}
const _TpValue(void) const //获取值
{
LOCK(m_Mutex);
returnm_Data;
}
voidLock(void) const //加锁
{
m_Mutex.Lock();
}
voidUnlock(void) const //解锁
{
m_Mutex.Unlock();
}
_Tp& Data(void) //加锁后方可安全使用此函数得到资源的引用,如未加锁直接调用可能造成相应的安全问题
{
returnm_Data;
}
protected:
_Tp m_Data; //数据成员
mutable_Mutex m_Mutex; //锁成员
friend classJLockHelper;
};
/**************************************************************************************
*JLockHelper 使用于有Lock,及Unlock成员函数的安全类或锁类的辅助加锁对象,
* 可以做到异常安全的解锁
* 说明: 在创建类时加锁,析构时解锁;一种简单的但不是必须的使用方法是使用{}
* 将要加锁的代码括起来,在{}中第一行定义该辅助类,则}时将自动解锁
****************************************************************************************/
class JLockHelper
{
public:
JLockHelper(const JAbsXMutex& xMutex):m_xMutex(xMutex)
{
m_xMutex.Lock();
}
template<class _Tp,class _Mutex>
JLockHelper(const JSafe<_Tp,_Mutex>& S):m_xMutex(S.m_Mutex)
{
m_xMutex.Lock();
}
~JLockHelper()
{
m_xMutex.Unlock();
}
private:
constJAbsXMutex& m_xMutex;
};
template<class _Tp>
class JSafeNum : public JSafe<_Tp>
{
public:
typedefJSafe<_Tp> safe_type;
explicitJSafeNum(const _Tp& _Value = 0):JSafe<_Tp>(_Value){}
JSafeNum&operator ++()
{
LOCK(safe_type::m_Mutex);
safe_type::m_Data++;
return*this;
}
JSafeNumoperator ++(int)
{
LOCK(safe_type::m_Mutex);
JSafeNumtmp(safe_type::m_Data++);
returntmp;
}
JSafeNum&operator +=(const _Tp& other)
{
LOCK(safe_type::m_Mutex);
safe_type::m_Data += other;
return*this;
}
JSafeNum&operator --(void)
{
LOCK(safe_type::m_Mutex);
safe_type::m_Data--;
return*this;
}
JSafeNum operator --(int)
{
LOCK(safe_type::m_Mutex);
JSafeNumtmp(safe_type::m_Data--);
returntmp;
}
JSafeNum&operator -=(const _Tp& other)
{
LOCK(safe_type::m_Mutex);
safe_type::m_Data -= other;
return*this;
}
JSafeNum&operator =(const _Tp& other)
{
LOCK(safe_type::m_Mutex);
safe_type::m_Data = other;
return*this;
}
};
typedef JSafe<bool> JSafeBool;
typedef JSafeNum<int> JSafeInt;
typedef JSafeNum<long> JSafeLong;
typedef JSafeNum<u_int> JSafeUint;
//安全容器,适用于stl容器类
template<class _Con>
class JSafeCon :public JSafe<_Con>
{
public:
typedefJSafe<_Con> _Base;
typedefJSafeCon<_Con> _Self;
typedef _Concon_type;
typedeftypename con_type::iterator iterator;
explicitJSafeCon(void):_Base(){}
bool empty()
{
LOCK(_Base::m_Mutex);
return_Base::m_Data.empty();
}
size_t size()
{
LOCK(_Base::m_Mutex);
return_Base::m_Data.size();
}
voidswap(con_type& Seq)
{
LOCK(_Base::m_Mutex);
_Base::m_Data.swap(Seq);
}
//以下两个函数及其使用均需在同步的保护下
iteratorbegin()
{
return_Base::m_Data.begin();
}
iteratorend()
{
return_Base::m_Data.end();
}
};
//安全序列(适用于stl的序列容器类如vector,list,...)
template<class _Sequence>
class JSafeSeq : public JSafeCon<_Sequence>
{
public:
typedefJSafeCon<_Sequence> _Base;
typedefJSafeSeq<_Sequence> _Self;
typedef_Sequence seq_type;
typedeftypename seq_type::iterator iterator;
typedeftypename seq_type::value_type value_type;
explicit JSafeSeq(void):_Base(){}
voidpush_back(const value_type& Val)
{
LOCK(_Base::m_Mutex);
_Base::m_Data.push_back(Val);
}
};
//适用于stl前向插入序列如deque等
template<class _FSeq>
class JSafeFSeq:public JSafeSeq<_FSeq>
{
public:
typedefJSafeSeq<_FSeq> _Base;
typedefJSafeFSeq<_FSeq> _Self;
typedeftypename _Base::seq_type seq_type;
typedeftypename seq_type::iterator iterator;
typedeftypename seq_type::value_type value_type;
explicitJSafeFSeq(void):_Base(){}
boolpop_front(value_type& Val)//取出第一个元素
{
LOCK(_Base::m_Mutex);
if(_Base::m_Data.empty())
{
return false;
}
else
{
Val =*(_Base::m_Data.begin());
_Base::m_Data.pop_front();
return true;
}
}
};
//安全map类,适用于stl::map
template<class _Map>
class JSafeMap :public JSafeCon<_Map>
{
public:
typedef JSafeCon<_Map>_Base;
typedefJSafeSeq<_Map> _Self;
typedef _Mapmap_type;
typedeftypename map_type::iterator iterator;
typedeftypename map_type::const_iterator const_iterator;
typedeftypename map_type::value_type value_type;
typedeftypename value_type::first_type key_type;
typedeftypename value_type::second_type data_type;
explicitJSafeMap(void):_Base(){}
voidinsert(const value_type& Val)
{
LOCK(_Base::m_Mutex);
_Base::m_Data.insert(Val);
}
boolfind(const key_type& Key) const
{
LOCK(_Base::m_Mutex);
return_Base::m_Data.find(Key) != _Base::m_Data.end();
}
boolfind(const key_type& Key,data_type& Val) const
{
LOCK(_Base::m_Mutex);
const_iteratorit = _Base::m_Data.find(Key);
if (_Base::m_Data.end() != it )
{
Val =it->second;
return true;
}
returnfalse;
}
voiderase(const key_type& Key)
{
LOCK(_Base::m_Mutex);
_Base::m_Data.erase(Key);
}
data_typeget(const key_type& Key,const data_type& Default) const
{
LOCK(_Base::m_Mutex);
const_iterator it = _Base::m_Data.find(Key);
if (_Base::m_Data.end() == it )
{
return Default;
}
else
{
return it->second();
}
}
boolfind_erase(const key_type& Key,data_type& Val)
{
LOCK(_Base::m_Mutex);
iteratorit = _Base::m_Data.find(Key);
if ( _Base::m_Data.end()!= it )
{
Val =it->second;
_Base::m_Data.erase(it);
return true;
}
returnfalse;
}
voidset(const key_type& Key,const data_type& Val)
{
LOCK(_Base::m_Mutex);
_Base::m_Data[Key] = Val;
}
};
#endif //head file
/*
*=====================================================================================
*
* Filename: mtmatch.cpp
*
* Description: 多线程的匹配工具,类似“grepvollin vollin.txt”
*
* Version: 1.0
* Created: 04/30/2009 01:11:07 PM CST
* Revision: none
* Compiler: g++
*
* Author: vollinwan
* Company: Tencent Tech Ltd
*
*=====================================================================================
*/
#include "safe.h"
#include <deque>
#include <list>
#include <string>
#include <iostream>
#include <fstream>
using namespace std;
JSafeFSeq<deque<string*> > g_HandleDeque;//待处理队列
JSafeSeq<list<string*> > g_ResList; //结果列表
JSafeBool g_bPutOver(false); //输入结束标志
JSafeInt g_nCurTCnt(0); //当前运行的工作线程数
string g_sFilter; //过滤字符串
const u_int g_uTcnt = 3; //线程数
inline u_int usleep_r(u_int usec)
{
//usleep的线程安全版本
structtimespec rqtp;
structtimespec rem;
memset(&rem,0,sizeof(rem));
rqtp.tv_sec =usec/1000000;
rqtp.tv_nsec= usec%1000000*1000;
nanosleep(&rqtp, &rem);
returnrem.tv_sec * 1000000 + rem.tv_nsec / 1000;
}
//工作线程的主函数
void* Work(void* pParam)
{
++g_nCurTCnt;
string*pIn=NULL;
while (1)
{
while(g_HandleDeque.pop_front(pIn))
{
if(pIn->find(g_sFilter) != string::npos)
{
g_ResList.push_back(pIn);
}
else
{
delete pIn;
}
}
if(g_bPutOver)
{
break;
}
else
{
usleep_r(1);
}
}
--g_nCurTCnt;
return NULL;
}
int main(int argc,char** argv)
{
string sUsage= string(argv[0]) + " filter filepath";
if (argc !=3)
{
cout<<sUsage<<endl;
_exit(1);
}
g_sFilter =argv[1];
ifstreamf(argv[2]);
if ( !f )
{
cout<<argv[2]<<" can't open!"<<endl;
_exit(1);
}
pthread_t tI[g_uTcnt];
for (inti=0;i<g_uTcnt;i++)
{
if (0 !=pthread_create(&tI[i],NULL,Work,NULL) )
{
cout<<"congratulations!"<<endl;
_exit(1);
}
}
string *pLine= new string;
while(getline(f,*pLine))
{
g_HandleDeque.push_back(pLine);
pLine =new string;
}
delete pLine;
g_bPutOver=true;
while (1)
{
list<string*> l;
if(!g_ResList.empty())
{
g_ResList.swap(l);
for(list<string*>::iterator it = l.begin();it != l.end();)
{
cout<<**it<<endl;
delete *it;
it = l.erase(it);
}
}
else if (g_HandleDeque.empty() && g_nCurTCnt == 0)
{
break;
}
else
{
usleep_r(1);
}
}
return 0;
}
Java 并发核心编程
内容涉及:
1、关于java并发
2、概念
3、保护共享数据
4、并发集合类
5线程
6、线程协作及其他
1、关于java并发
自从java创建以来就已经支持并发的理念,如线程和锁。这篇指南主要是为帮助java多线程开发人员理解并发的核心概念以及如何应用这些理念。本文的主题是关于具有java语言风格的Thread、synchronized、volatile,以及J2SE5中新增的概念,如锁(Lock)、原子性(Atomics)、并发集合类、线程协作摘要、Executors。开发者通过这些基础的接口可以构建高并发、线程安全的java应用程序。
2、概念
本部分描述的java并发概念在这篇DZone Refard会被通篇使用。
从JVM并发看CPU内存指令重排序(Memory Reordering):http://kenwublog.com/illustrate-memory-reordering-in-cpu
java内存模型详解: http://kenwublog.com/explain-java-memory-model-in-detail
概念 | 描述 |
Java Memory Model Java内存模型 | 在JavaSE5(JSR133)中定义的Java Memory Model(JMM)是为了确保当编写并发代码的时候能够提供Java程序员一个可用的JVM实现。术语JMM的作用类似与一个观察同步读写字段的monitor。它按照“happens-before order(先行发生排序)”的顺序—可以解释为什么一个线程可以获得其他线程的结果,这组成了一个属性同步的程序,使字段具有不变性,以及其他属性。 |
monitor Monitor | Java语言中,每个对象都拥有一个访问代码关键部分并防止其他对象访问这段代码的“monitor”(每个对象都拥有一个对代码关键部分提供访问互斥功能的“monitor”)。这段关键部分是使用synchronized对方法或者代码标注实现的。同一时间在同一个monitor中,只允许一个线程运行代码的任意关键部分。当一个线程试图获取代码的关键部分时,如果这段代码的monitor被其他线程拥有,那么这个线程会无限期的等待这个monitor直到它被其他线程释放。除了访问互斥之外,monitor还可以通过wait和notify来实现协作。 |
原子字段赋值 Atomic field assignment | 除了doubles和langs之外的类型,给一个这些类型的字段赋值是一个原子操作。在JVM中,doubles和langs的更新是被实现为2个独立的操作,因此理论上可能会有其他的线程得到一个部分更新的结果。为了保护共享的doubles和langs,可以使用volatile标记这个字段或者在synchronized修饰的代码块中操作字段。 |
竞争状态 Race condition | 竞争发生在当不少于一个线程对一个共享的资源进行一系列的操作,如果这些线程的操作的顺序不同,会导致多种可能的结果。 |
数据竞争 Data race | 数据竞争主要发生在多个线程访问一个共享的、non-final、non-volatile、没有合适的synchronization限制的字段。Java内存模型不会对这种非同步的数据访问提供任何的保证。在不同的架构和机器中数据竞争会导致不可预测的行为。 |
安全发布 Safe publications | 在一个对象创建完成之前就发布它的引用时非常危险的。避免这种使用这种引用的一种方法就是在创建期间注册一个回调接口。另外一种不安全的情况就是在构造子中启动一个线程。在这2种情况中,非完全创建的对象对于其他线程来说都是可见的。 |
不可变字段 Final Fields | 不可变字段在对象创建之后必须明确设定一个值,否则编译器就会报出一个错误。一旦设定值后,不可变字段的值就不可以再次改变。将一个对象的引用设定为不可变字段并不能阻止这个对象的改变。例如,ArrayList类型的不可变字段不能改变为其他ArrayList实例的引用,但是可以在这个list实例中添加或者删除对象。 在创建结尾,对象会遇到”final field freeze”:如果对象被安全的发布后,即使在没有synchronization关键字修饰的情况下,也能保证所有的线程获取final字段在构建过程中设定的值。final field freezer不仅对final字段有用,而且作用于final对象中的可访问属性。 |
不可变对象 Immutable objects | 在语法上final 字段能够创建不需要synchronization修饰的、能够被共享读取的线程安全的不可变对象。实现Immutable Object需要保证如下条件: ·对象被安全的发布(在创建过程中this 引用是无法避免的) ·所有字段被声明为final ·在创建之后,在对象字段能够被访问的范围中是不允许修改这个字段的。 ·class被声明为final(为了防止subclass违反这些规则) |
3、保护共享数据
编写线程安全的java程序,当修改共享数据的时候要求开发人员使用合适的锁来保护数据。锁能够建立符合Java Memory Model要求的访问顺序,而且确保其他线程知道数据的变化。
注意:
在Java Memory Model中,如果没有被synchronization修饰,改变数据不需要什么特别的语法表示。JVM能够自由地重置指令顺序的特性和对可见性的限制方式很容易让开发人员感到奇怪。
3.1、Synchronized
每个对象实例都拥有一个每次只能让一个线程锁住的monitor。synchronized能够用在一个方法或者代码块中来锁住这个monitor。用synchronized修饰一个对象,当修改这个对象的一个字段,synchronized保证其他线程余下的对这个对象的读操作能够获取修改后的值。需要注意的是修改同步块之外的数据或者synchronized没有修饰当前被修改的对象,那么不能保证其他线程读到这些最新的数据。synchronized关键字能够修饰一个对象实例中的函数或者代码块。在一个非静态方法中this关键字表示当前的实例对象。在一个synchronized修饰的静态的方法中,这个方法所在的类使用Class作为实例对象。
3.2、Lock
Java.util.concurrent.locks包中有个标准Lock接口。ReentrantLock 实现了Lock接口,它完全拥有synchronized的特性,同时还提供了新的功能:获取Lock的状态、非阻塞获取锁的方法tryLock()、可中断Lock。
下面是使用ReentrantLock的详细示例:
public class Counter{ private final Lock lock = new ReentrantLock(); private int value; public int increment() { lock.lock(); try { return ++value; }finally{ lock.unlock(); } } } |
3.3、ReadWriteLock
Java.util.concurrent.locks包中还有个ReadWriteLock接口(实现类是ReentrantWriteReadLock),它定义一对锁:读锁和写锁,特征是能够被并发的读取但每次只能有一个写操作。使用ReentrantReadWriteLock并发读取特性的详细示例:
public class ReadWrite { private final ReadWriteLock lock = new ReentrantReadWriteLock(); private int value; public void increment(){ lock.writeLock().lock(); try{ value++; }finally{ lock.writeLock().lock(); } } public int current(){ lock.readLock().lock(); try{ return value; }finally{ lock.readLock().unlock(); } } } |
3.4、volatile
volatile原理与技巧: http://kenwublog.com/the-theory-of-volatile
volatile修饰符用来标注一个字段,表明任何对这个字段的修改都必须能被其他随后访问的线程获取到,这个修饰符和同步无关。因此,volatile修饰的数据的可见性和synchronization类似,但是这个它只作用于对字段的读或写操作。在JavaSE5之前,因为JVM的架构和实现的原因,不同JVM的volatile效果是不同的而且也是不可信的。下面是Java内存模型明确地定义volatile的行为:
public class Processor implements Runnable { private volatile boolean stop; public void stopProcessing(){ stop = true; } public void run() { while (!stop) { //do processing } } } |
注意:使用volatile修饰一个数组并不能让这个数组的每个元素拥有volatile特性,这种声明只是让这个数组的reference具有volatile属性。数组被声明为AtomicIntegerArray类型,则能够拥有类似volatile的特性。
3.5、原子类
使用volatile的一个缺点是它能够保证数据的可见性,却不能在一个原子操作中对volatile修饰的字段同时进行校验和更新操作。java.util.concurrent.atomic包中有一系列支持在单个非锁定(lock)的变量上进行原子操作的类,类似于volatile。示例:
public class Counter{ private AtomicInteger value = new AtomicInteger(); private int value; public int increment() { return value.incrementAndGet(); } } |
incrementAndGet方法是原子类的复合操作的一个示例。booleans, integers, longs, object references, integers数组, longs数组, object references数组 都有相应的原子类。
3.6、ThreadLocal
通过ThreadLocal能数据保存在一个线程中,而且不需要lock同步。理论上ThreadLocal可以让一个变量在每个线程都有一个副本。ThreadLocal常用来屏蔽线程的私有变量,例如“并发事务”或者其他的资源。而且,它还被用来维护每个线程的计数器,统计,或者ID生成器。
public class TransactionManager { private static final ThreadLocal<Transaction> currentTransaction = new ThreadLocal<Transaction>() { @Override protected Transaction initialValue() { return new NullTransaction(); } }; public Transaction currentTransaction() { Transaction current = currentTransaction.get(); if(current.isNull()) { current = new TransactionImpl(); currentTransaction.put(current); } return current; } } |
4、Concurrent Collections(并发集合类)
保护共享数据的一个关键技术是在存储数据的类中封装同步机制。所有对数据的使用都要经过同步机制的确认使这个技术能够避免数据的不当访问。在java.util.concurrent包中有很多为并发使用情况下设计的数据结构。通常,使用这些数据结构比使用同步包装器装饰的非同步的集合的效率更高。
4.1、Concurrent lists and sets
在Table2 中列出了java.util.concurrent包中拥有的3个并发的List和Set实现类。
类 | 描述 |
CopyOnWriteArraySet | CopyOnWriteArraySet在语意上提供写时复制(copy-on-werite)的特性,对这个集合的每次修改都需要对当前数据结构新建一个副本,因此写操作发费很大。在迭代器创建的时候,会对当前数据数据结构创建一个快照用于迭代。 |
CopyOnWriteArrayList | CopyOnWriteArrayList和CopyOnWriteArraySet类似,也是基于copy-on-write语义实现了List接口 |
ConcurrentSkipListSet | ConcurrentSkipListSet(在JavaSE 6新增的)提供的功能类似于TreeSet,能够并发的访问有序的set。因为ConcurrentSkipListSet是基于“跳跃列表(skip list)”实现的,只要多个线程没有同时修改集合的同一个部分,那么在正常读、写集合的操作中不会出现竞争现象。 |
skip list: http://blog.csdn.net/yuanyufei/archive/2007/02/14/1509937.aspx
http://zh.wikipedia.org/zh-cn/%E8%B7%B3%E8%B7%83%E5%88%97%E8%A1%A8
4.2、Concurrent maps
Java.util.concurrent包中有个继承Map接口的ConcurrentMap的接口,ConcurrentMap提供了一些新的方法(表3)。所有的这些方法在一个原子操作中各自提供了一套操作步骤。如果将每套步骤在放在map之外单独实现,在非原子操作的多线程访问的情况下会导致资源竞争。
表3:ConcurrentMap的方法:
方法 | 描述 |
putIfAbsent(K key, V value) : V | 如果key在map中不存在,则把key-value键值对放入map中,否则不执行任何操作。返回值为原来的value,如果key不存在map中则返回null |
remove(Object key, Object value) : boolean | 如果map中有这个key及相应的value,那么移除这对数据,否则不执行任何操作 |
replace (K key, V value) : V | 如果map中有这个key,那么用新的value替换原来的value,否则不执行任何操作 |
replace (K key, V oldValue, V newValue) : boolean | 如果map中有这对key-oldValue数据,那么用newValue替换原来的oldValue,否则不执行任何操作 |
在表4中列出的是ConcurrentMap的2个实现类
方法 | 描述 |
ConcurrentHashMap | ConcurrentHashMap提供了2种级别的内部哈希方法。第一种级别是选择一个内部的Segment,第二种是在选定的Segment中将数据哈希到buckets中。第一种方法通过并行地在不同的Segment上进行读写操作来实现并发。(ConcurrentHashMap是引入了Segment,每个Segment又是一个hash表,ConcurrentHashMap相当于是两级Hash表,然后锁是在Segment一级进行的,提高了并发性。http://mooncui.javaeye.com/blog/380884 http://www.javaeye.com/topic/344876 |
ConcurrentSkipListMap | ConcurrentSkipListMap(JavaSE 6新增的类)功能类似TreeMap,是能够被并发访问的排序map。尽管能够被多线程正常的读写---只要这些线程没有同时修改map的同一个部分,ConcurrentSkipListMap的性能指标和TreeMap差不多。 |
4.3、Queues
Queues类似于沟通“生产者”和“消费者”的管道。组件从管道的一端放入,然后从另一端取出:“先进先出”(FIFO)的顺序。Queue接口在JavaSE5新添加到java.util中的,能够被用于单线程访问的场景中,主要适用于多个生产者、一个或多个消费者的情景,所有的读写操作都是基于同一个队列。
java.util.concurrent包中的BlockingQueue接口是Queue的子接口,而且还添加了新的特性处理如下场景:队列满(此时刚好有一个生产者要加入一个新的组件)、队列空(此时刚好有一个消费者读取或者删除一个组件)。BlockingQueue提供如下方案解决这些情况:一直阻塞等待直到其他线程修改队列的数据状态;阻塞一段时间之后返回,如果在这段时间内有其他线程修改队列数据,那么也会返回。
表5:Queue和BlockingQueue的方法:
方法 | 策略 | 插入 | 移除 | 核查 |
Queue | 抛出异常 | add | remove | element |
返回特定的值 | offer | poll | peek | |
Blocking Queue | 一直阻塞 | put | take | n/a |
超时阻塞 | offer | poll | n/a |
在JDK中提供了一些Queue的实现,在表6中是这些实现类的关系列表。
方法 | 描述 |
PriorityQueue | PriorityQueue是唯一一个非线程安全的队列实现类,用于单线程存放数据并且将数据排序。 |
CurrentLinkedQueue | 一个无界的、基于链接列表的、唯一一个线程安全的队列实现类,不支持BlockingQueue。 |
ArrayBlockingQueue | 一个有界的、基于数组的阻塞队列。 |
LinkedBlockingQueue | 一个有界的、基于链接列表的阻塞队列。有可能是最常用的队列实现。 |
PriorityBlockingQueue | 一个无界的、基于堆的阻塞队列。队列根据设置的Comparator(比较器)来确定组件读取、移除的顺序(不是队列默认的FIFO顺序) |
DelayQueue | 一个无界的、延迟元素(每个延迟元素都会有相应的延迟时间值)的阻塞队列实现。只有在延时期过了之后,元素才能被移除,而且最先被移除的是延时最先到期的元素。 |
SynchronousQueue | 一种0容量的队列实现,生产者添加元素之后必须等待消费者移除后才可以返回,反之依然。如果生产者和消费者2个线程同时访问,那么参数直接从生产者传递到消费者。经常用于线程之间的数据传输。 |
4.4、Deque
在JavaSE6中新增加了两端都可以添加和删除的队列-Deque (发音"deck",not "dick"). Deques不仅可以从一端添加元素,从另一端移除,而且两端都可以添加和删除元素。如同BlockingQueue,BlockingDeque接口也为阻塞等待和超时等待的特殊情况提供了解决方法。因为Deque继承Queue、BlockingDeque继承BlockingQueue,下表中的方法都是可以使用的:
接口 | 头或尾 | 策略 | 插入 | 移除 | 核查 |
Queue | Head | 抛出异常 | addFirst | removeFirst | getFirst |
返回特定的值 | offerFirst | pollFirst | peekFirst | ||
Tail | 抛出异常 | addLast | removeLast | getLast | |
返回特定的值 | offerLast | pollLast | peekLast | ||
BlockingQueue | Head | 一直阻塞 | putFirst | takeFirst | n/a |
超时阻塞 | offerFirst | pollFirst | n/a | ||
Tail | 一直阻塞 | putLast | takeLast | n/a | |
超时阻塞 | offerLast | pollLast | n/a |
Deque的一个特殊应用场景是只在一个端口进行添加、删除、检查操作--堆栈(first-in-last-out顺序)。Deque接口提供了stack相同的方法:push(), pop()和peek(),这方法和addFirst(), removeFirst(), peekFirst()一一对应,可以把Deque的任何一个实现类当做堆栈使用。表6中是JDK中Deque和BlockingDeque的实现。注意Deque继承Queue,BlockingDeque继承自BlockingQueue。
表8:Deques
5、线程
在Java中,java.lang.Thread类是用来代表一个应用或者JVM线程。代码是在某个线程类的上下文环境中执行的(使用Thread.currentThread()来获取当前运行的线程)。
5.1、线程通讯
线程之间最简单的通讯方式是一个线程直接调用另一个线程对象的方法。表9中列出的是线程之间可以直接交互的方法。
表9:线程协作方法
类 | 描述 |
LinkedList | 这个经常被用到的类在JavaSE6中有了新的改进-实现了Deque接口。在LinkedList中,可以使用标准的Deque方法来添加或者删除list两端的元素。LinkedList还可以被当做一个非同步的堆栈,用来替代同步的Stack类 |
ArrayDeque | 一个非同步的、支持无限队列长度(根据需要动态扩展队列的长度)的Deque实现类 |
LinkedBlockingDeque | LinkeBlockingDeque是Deque实现中唯一支持并发的、基于链接列表、队列长度可选的类。 |
线程方法 | 描述 |
start | 启动一个线程实例,并且执行它的run() 方法。 |
join | 一直阻塞直到其他线程退出 |
interrupt | 中断其他线程。线程如果在一个方法中被阻塞,会对interrupt操作做出回应,并在这个方法执行的线程中抛出InterruptedException异常;否则线程的中断状态被设定。 |
stop, suspend, resume, destroy | 这些方法都被废弃,不应该再使用了。因为线程处理过程中状态问题会导致危险的操作。相反,应该使用interrupt() 或者 volatile标示来告诉一个线程应该做什么。 |
5.2、"未捕获异常"处理器
线程能够指定一个UncaughtExceptionHandler来接收任何一个导致线程非正常突然终止的未捕获异常的通知。
5.3、死锁
当存在多个线程(最少2个)等待对方占有的资源,就会形成资源循环依赖和线程等待,产生死锁。最常见的导致死锁的资源是对象monitor,同时其他阻塞操作(例如wait/notify)也能导致死锁。
很多新的JVM能够检测Monitor死锁,并且可以将线程 dump中由信号(中断信号)、jstack或者其他线程dump工具生成的死锁原因显示打印出来。
除了死锁,线程之间还会出现饥饿(starvation)和活锁(livelock). Starvation是因为一个线程长时间占有一个锁导致其他的线程一直处于等待状态无法进行下一步操作。Livelock是因为线程发费大量的时间来协调资源的访问或者检测避免死锁导致没有一个线程真正的干活。
6、线程协作
6.1、wait/notify
wait/notify关键字适用于一个线程通知另一个线程所需的条件状态已就绪,最常用于线程在循环中休眠直到获取特定条件的场景. 例如,一个线程一直等待直到队列中有一个组件能够处理;当组件添加到队列时,另一个线程能够通知这个等待的线程。
wait和notify的经典用法是:
Thread t = new Thread(runnable); t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { public void uncaughtException(Thread t, Throwable e) { // TODO get Logger and log uncaught exception } }); t.start(); |
public class Latch { private final Object lock = new Object(); private volatile boolean flag = false; public void waitTillChange(){ synchronized (lock) { while(!flag){ try { lock.wait(); } catch (InterruptedException e) { } } } } public void change(){ synchronized (lock) { flag = true; lock.notifyAll(); } } } |
在代码中需要注意的重要地方是:
wait、notify、notifyAll必须在synchronized修饰的代码块中执行,否则会在运行的时候抛出IllegalMonitorStateException异常
在循环语句wait的时候一定要设定循环的条件--这样能够避免wait开始之前,线程所需的条件已经被其他线程提供了却依然开始此线程wait导致的时间消耗。同时,这种办法还能够保证你的代码不被虚假的信息唤醒。
总是要保证在调用notify和notifyAll之前,能够提供符合线程退出等待的条件。否则会出现即使线程接收到通知信息,却不能退出循环等待的情况。
6.2、Condition
在JavaSE5中新添加了java.util.concurrent.locks.Condition接口。Condition不仅在API中实现了wait/notify语义,而且提供了几个新的特性,例如:为每个Lock创建多重Condition、可中断的等待、访问统计信息等。Condition是通过Lock示例产生的,示例:
public class LatchCondition { private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); private volatile boolean flag = false; public void waitTillChange(){ lock.lock(); try{ while(!flag){ try { condition.await(); } catch (InterruptedException e) { } } }finally{ lock.unlock(); } } public void change(){ lock.lock(); try{ flag = true; condition.notifyAll(); }finally{ lock.unlock(); } } } |
6.3、Coordination classes
java.util.concurrent包中有几个类适用于常见的多线程通讯。这几个协作类适用范围几乎涵盖了使用wait/notify和Condition最常见的场景,而且更安全、更易于使用。
CyclicBarrier
在CyclicBarrier初始化的时候指定参与者的数量。参与者调用awart()方法进入阻塞状态直到参与者的个数达到指定数量,此时最后一个到达的线程执行预定的屏障任务,然后释放所有的线程。屏障可以被重复的重置状态。常用于协调分组的线程的启动和停止。
CountDownLatch
需要指定一个计数才能初始化CountDownLatch。线程调用await()方法进入等待状态知道计数变为0。其他的线程(或者同一个线程)调用countDown()来减少计数。如果计数变为0后是无法被重置的。常用于当确定数目的操作完成后,触发数量不定的线程。
Semaphore
Semaphore维护一个“许可”集,能够使用acquire()方法检测这个“许可”集,在“许可”可用之前Semaphore会阻塞每个acquire访问。线程能够调用release()来返回一个许可。当Semaphore只有一个“许可”的时候,可当做一个互斥锁来使用。
Exchanger
线程在Exchanger的exchange()方法上进行交互、原子操作的方式交换数据。功能类似于数据可以双向传递的SynchronousQueue加强版。
7、任务执行
很多java并发程序需要一个线程池来执行队列中的任务。在java.util.concurrent包中为这种类型的任务管理提供了一种可靠的基本方法。
7.1、ExecutorService
Executor和易扩展的ExecutorService接口规定了用于执行任务的组件的标准。这些接口的使用者可以通过一个标准的接口使用各种具有不同行为的实现类。
最通用的Executor接口只能访问这种类型的可执行(Runnable)任务 :
void execute(Runnable command)
Executor子接口ExecutorService新加了方法,能够执行:Runnable任务、Callable任务以及任务集合。
Future<?> submit(Runnable task)
Future<T> submit(Callable<T> task)
Future<T> submit(Runnable task, T result)
List<Future<T>> invokeAll (Collection<? extends Callable<T>> tasks)
List<Future<T>> invokeAll (Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
T invokeAny(Collection<? extends Callable<T>> tasks)
T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
7.2、Callable and Future
Callable类似于Runnable,而且能够返回值、抛出异常:
V call() throws Exception;
在一个任务执行框架中提交一个Callable 任务,然后返回一个Future结果是很常见的。Future表示在将来的某个时刻能够获取到结果。Future提供能够获取结果或者阻塞直到结果可用的方法。任务运行之前或正在运行的时候,可以通过Future中的方法取消。
如果只是需要一个Runnable特性的Future(例如在Executor执行),可用使用FutureTask。FutureTask实现了Future和Runnable接口,可用提交一个Runnable类型任务,然后在调用部分使用这个Future类型的任务。
7.3、实现ExecutorService
ExecutorService最主要的实现类是ThreadPoolExecutor。这个实现类提供了大量的可配置特性:
线程池--设定常用线程数量(启动前可选参数)和最大可用线程数量。
线程工厂--通过自定义的线程工厂生成线程,例如生成自定义线程名的线程。
工作队列--指定队列的实现类,实现类必须是阻塞的、可以是无界的或有界的。
被拒绝的任务--当队列已经满了或者是执行者不可用,需要为这些情况指定解决策略。
生命周期中的钩子--重写扩展在任务运行之前或之后的生命周期中的关键点
关闭--停止已接受的任务,等待正在运行的任务完成后,关闭ThreadPoolExecutor。
ScheduledThreadPoolExecutor是ThreadPoolExecutor的一个子类,能够按照定时的方式完成任务(而不是FIFO方式)。在java.util.Timer不是足够完善的情况下,ScheduleThreadPoolExecutor具有强大的可适用性。
Executors类有很多静态方法(表10)用于创建适用于各种常见情况的预先包装的ExecutorService和ScheduleExecutorService实例
表10
方法 | 描述 |
newSingleThreadExecutor | 创建只有一个线程的ExecutorService |
newFixedThreadPool | 返回拥有固定数量线程的ExecutorService |
newCachedThreadPool | 返回一个线程数量可变的ExecutorService |
newSingleThreadScheduledExecutor | 返回只有一个线程的ScheduledExecutorService |
newScheduledThreadPool | 创建拥有一组核心线程的ScheduledExecutorService |
下面的例子是创建一个固定线程池,然后提交一个长期运行的任务:
在这个示例中提交任务到executor之后,代码没有阻塞而是立即返回。在代码的最后一行调用get()方法会阻塞直到有结果返回。
ExecutorService几乎涵盖了所有应该创建线程对象或线程池的情景。在代码中需要直接创建一个线程的时候,可以考虑通过Executor工厂创建的ExecutorService能否实现相同的目标;这样做经常更简单、更灵活。
7.4、CompletionService
除了常见的线程池和输入队列模式,还有一种常见的情况:为后面的处理,每个任务生成的结果必须积累下来。CompletionService接口允许提交Callable和Runnable任务,而且还可以从任务队列中获取这些结果:(绿色部分和英文版不一样,已和作者确认,英文版将take()和poll()方法混淆了)
Future<V> take () -- 如果结果存在则获取,否则直接返回
Future<V> poll () -- 阻塞直到结果可用
Future<V> poll (long timeout, TimeUnit unit) -- 阻塞直到timeout时间结束
ExecutorCompletionService是CompletionService的标准实现类。在ExecutorCompletionService的构成函数中需要一个Executor,ExecutorCompletionService提供输入队列和线程池。
8、Hot Tip
热门信息:当设置线程池大小的时候,最好是基于当前应用所运行的机器拥有的逻辑处理器的数量。在java中,可用使用Runtime.getRuntime().availableProcessors()获取这个值。在JVM的生命周期中,可用处理器的数目是可变的。
并发集合类
DougLea的 util.concurrent 包除了包含许多其他有用的并发构造块之外,还包含了一些主要集合类型 List 和 Map 的高性能的、线程安全的实现。在本月的 Java理论与实践中,BrianGoetz向您展示了用ConcurrentHashMap 替换 Hashtable 或synchronizedMap ,将有多少并发程序获益。您可以在本文的 论坛中与作者以及其他读者共享您的想法(您也可以点击文章顶部或者底部的 讨论进入论坛)。
在Java类库中出现的第一个关联的集合类是 Hashtable ,它是JDK 1.0的一部分。Hashtable 提供了一种易于使用的、线程安全的、关联的map功能,这当然也是方便的。然而,线程安全性是凭代价换来的―― Hashtable 的所有方法都是同步的。此时,无竞争的同步会导致可观的性能代价。 Hashtable 的后继者 HashMap是作为JDK1.2中的集合框架的一部分出现的,它通过提供一个不同步的基类和一个同步的包装器 Collections.synchronizedMap ,解决了线程安全性问题。通过将基本的功能从线程安全性中分离开来, Collections.synchronizedMap 允许需要同步的用户可以拥有同步,而不需要同步的用户则不必为同步付出代价。
Hashtable 和 synchronizedMap 所采取的获得同步的简单方法(同步 Hashtable 中或者同步的 Map 包装器对象中的每个方法)有两个主要的不足。首先,这种方法对于可伸缩性是一种障碍,因为一次只能有一个线程可以访问hash表。同时,这样仍不足以提供真正的线程安全性,许多公用的混合操作仍然需要额外的同步。虽然诸如 get() 和 put() 之类的简单操作可以在不需要额外同步的情况下安全地完成,但还是有一些公用的操作序列,例如迭代或者put-if-absent(空则放入),需要外部的同步,以避免数据争用。
有条件的线程安全性
同步的集合包装器 synchronizedMap 和 synchronizedList ,有时也被称作 有条件地线程安全――所有单个的操作都是线程安全的,但是多个操作组成的操作序列却可能导致数据争用,因为在操作序列中控制流取决于前面操作的结果。 清单1中第一片段展示了公用的put-if-absent语句块――如果一个条目不在 Map 中,那么添加这个条目。不幸的是,在 containsKey() 方法返回到 put() 方法被调用这段时间内,可能会有另一个线程也插入一个带有相同键的值。如果您想确保只有一次插入,您需要用一个对 Map m 进行同步的同步块将这一对语句包装起来。
清单1中其他的例子与迭代有关。在第一个例子中, List.size() 的结果在循环的执行期间可能会变得无效,因为另一个线程可以从这个列表中删除条目。如果时机不得当,在刚好进入循环的最后一次迭代之后有一个条目被另一个线程删除了,则 List.get() 将返回 null ,而 doSomething() 则很可能会抛出一个NullPointerException 异常。那么,采取什么措施才能避免这种情况呢?如果当您正在迭代一个 List 时另一个线程也可能正在访问这个 List ,那么在进行迭代时您必须使用一个 synchronized 块将这个 List 包装起来,在 List 1 上同步,从而锁住整个 List 。这样做虽然解决了数据争用问题,但是在并发性方面付出了更多的代价,因为在迭代期间锁住整个 List 会阻塞其他线程,使它们在很长一段时间内不能访问这个列表。
集合框架引入了迭代器,用于遍历一个列表或者其他集合,从而优化了对一个集合中的元素进行迭代的过程。然而,在 java.util 集合类中实现的迭代器极易崩溃,也就是说,如果在一个线程正在通过一个 Iterator 遍历集合时,另一个线程也来修改这个集合,那么接下来的 Iterator.hasNext() 或Iterator.next() 调用将抛出 ConcurrentModificationException 异常。就拿刚才这个例子来讲,如果想要防止出现 ConcurrentModificationException 异常,那么当您正在进行迭代时,您必须使用一个在 List l 上同步的synchronized 块将该 List 包装起来,从而锁住整个List 。(或者,您也可以调用 List.toArray() ,在不同步的情况下对数组进行迭代,但是如果列表比较大的话这样做代价很高)。
清单 1. 同步的map中的公用竞争条件
Map m = Collections.synchronizedMap(new HashMap()); List l = Collections.synchronizedList(new ArrayList()); // put-if-absent idiom -- contains a race condition // may require external synchronization if (!map.containsKey(key)) map.put(key, value); // ad-hoc iteration -- contains race conditions // may require external synchronization for (int i=0; i<list.size(); i++) { doSomething(list.get(i)); } // normal iteration -- can throw ConcurrentModificationException // may require external synchronization for (Iterator i=list.iterator(); i.hasNext(); ) { doSomething(i.next()); } |
synchronizedList 和 synchronizedMap 提供的有条件的线程安全性也带来了一个隐患 ―― 开发者会假设,因为这些集合都是同步的,所以它们都是线程安全的,这样一来他们对于正确地同步混合操作这件事就会疏忽。其结果是尽管表面上这些程序在负载较轻的时候能够正常工作,但是一旦负载较重,它们就会开始抛出 NullPointerException 或 ConcurrentModificationException 。
可伸缩性问题
可伸缩性指的是一个应用程序在工作负载和可用处理资源增加时其吞吐量的表现情况。一个可伸缩的程序能够通过使用更多的处理器、内存或者I/O带宽来相应地处理更大的工作负载。锁住某个共享的资源以获得独占式的访问这种做法会形成可伸缩性瓶颈――它使其他线程不能访问那个资源,即使有空闲的处理器可以调用那些线程也无济于事。为了取得可伸缩性,我们必须消除或者减少我们对独占式资源锁的依赖。
同步的集合包装器以及早期的 Hashtable 和 Vector 类带来的更大的问题是,它们在单个的锁上进行同步。这意味着一次只有一个线程可以访问集合,如果有一个线程正在读一个 Map ,那么所有其他想要读或者写这个 Map 的线程就必须等待。最常见的 Map 操作, get() 和 put() ,可能比表面上要进行更多的处理――当遍历一个hash表的bucket以期找到某一特定的key时, get() 必须对大量的候选bucket调用 Object.equals() 。如果key类所使用的 hashCode() 函数不能将value均匀地分布在整个hash表范围内,或者存在大量的hash冲突,那么某些bucket链就会比其他的链长很多,而遍历一个长的hash链以及对该hash链上一定百分比的元素调用 equals() 是一件很慢的事情。在上述条件下,调用 get() 和 put() 的代价高的问题不仅仅是指访问过程的缓慢,而且,当有线程正在遍历那个hash链时,所有其他线程都被锁在外面,不能访问这个 Map 。
(哈希表根据一个叫做hash的数字关键字(key)将对象存储在bucket中。hash value是从对象中的值计算得来的一个数字。每个不同的hash value都会创建一个新的bucket。要查找一个对象,您只需要计算这个对象的hash value并搜索相应的bucket就行了。通过快速地找到相应的bucket,就可以减少您需要搜索的对象数量了。译者注)
get() 执行起来可能会占用大量的时间,而在某些情况下,前面已经作了讨论的有条件的线程安全性问题会让这个问题变得还要糟糕得多。 清单1中演示的争用条件常常使得对单个集合的锁在单个操作执行完毕之后还必须继续保持一段较长的时间。如果您要在整个迭代期间都保持对集合的锁,那么其他的线程就会在锁外停留很长的一段时间,等待解锁。
实例:一个简单的cache
Map 在服务器应用中最常见的应用之一就是实现一个 cache。 服务器应用可能需要缓存文件内容、生成的页面、数据库查询的结果、与经过解析的XML文件相关的DOM树,以及许多其他类型的数据。cache的主要用途是重用前一次处理得出的结果以减少服务时间和增加吞吐量。cache工作负载的一个典型的特征就是检索大大多于更新,因此(理想情况下)cache能够提供非常好的 get() 性能。不过,使用会妨碍性能的cache还不如完全不用cache。
如果使用 synchronizedMap 来实现一个cache,那么您就在您的应用程序中引入了一个潜在的可伸缩性瓶颈。因为一次只有一个线程可以访问 Map ,这些线程包括那些要从Map 中取出一个值的线程以及那些要将一个新的 (key, value) 对插入到该map中的线程。
提高 HashMap 的并发性同时还提供线程安全性的一种方法是废除对整个表使用一个锁的方式,而采用对hash表的每个bucket都使用一个锁的方式(或者,更常见的是,使用一个锁池,每个锁负责保护几个bucket)。这意味着多个线程可以同时地访问一个 Map 的不同部分,而不必争用单个的集合范围的锁。这种方法能够直接提高插入、检索以及移除操作的可伸缩性。不幸的是,这种并发性是以一定的代价换来的――这使得对整个集合进行操作的一些方法(例如 size() 或 isEmpty() )的实现更加困难,因为这些方法要求一次获得许多的锁,并且还存在返回不正确的结果的风险。然而,对于某些情况,例如实现cache,这样做是一个很好的折衷――因为检索和插入操作比较频繁,而 size() 和 isEmpty() 操作则少得多。
ConcurrentHashMap
util.concurrent 包中的 ConcurrentHashMap 类(也将出现在JDK1.5中的 java.util.concurrent 包中)是对 Map 的线程安全的实现,比起 synchronizedMap 来,它提供了好得多的并发性。多个读操作几乎总可以并发地执行,同时进行的读和写操作通常也能并发地执行,而同时进行的写操作仍然可以不时地并发进行(相关的类也提供了类似的多个读线程的并发性,但是,只允许有一个活动的写线程) 。ConcurrentHashMap 被设计用来优化检索操作;实际上,成功的 get() 操作完成之后通常根本不会有锁着的资源。要在不使用锁的情况下取得线程安全性需要一定的技巧性,并且需要对Java内存模型(JavaMemory Model)的细节有深入的理解。 ConcurrentHashMap 实现,加上 util.concurrent 包的其他部分,已经被研究正确性和线程安全性的并发专家所正视。在下个月的文章中,我们将看看 ConcurrentHashMap 的实现的细节。
ConcurrentHashMap 通过稍微地松弛它对调用者的承诺而获得了更高的并发性。检索操作将可以返回由最近完成的插入操作所插入的值,也可以返回在步调上是并发的插入操作所添加的值(但是决不会返回一个没有意义的结果)。由 ConcurrentHashMap.iterator() 返回的 Iterators 将每次最多返回一个元素,并且决不会抛出 ConcurrentModificationException 异常,但是可能会也可能不会反映在该迭代器被构建之后发生的插入操作或者移除操作。在对集合进行迭代时,不需要表范围的锁就能提供线程安全性。在任何不依赖于锁整个表来防止更新的应用程序中,可以使用 ConcurrentHashMap 来替代 synchronizedMap 或 Hashtable 。
上述改进使得 ConcurrentHashMap 能够提供比 Hashtable 高得多的可伸缩性,而且,对于很多类型的公用案例(比如共享的cache)来说,还不用损失其效率。
表 1对 Hashtable 和 ConcurrentHashMap 的可伸缩性进行了粗略的比较。在每次运行过程中,n 个线程并发地执行一个死循环,在这个死循环中这些线程从一个 Hashtable 或者ConcurrentHashMap 中检索随机的key value,发现在执行 put()操作时有80%的检索失败率,在执行操作时有1%的检索成功率。测试所在的平台是一个双处理器的Xeon系统,操作系统是Linux。数据显示了10,000,000次迭代以毫秒计的运行时间,这个数据是在将对 ConcurrentHashMap的操作标准化为一个线程的情况下进行统计的。您可以看到,当线程增加到多个时, ConcurrentHashMap 的性能仍然保持上升趋势,而 Hashtable 的性能则随着争用锁的情况的出现而立即降了下来。
比起通常情况下的服务器应用,这次测试中线程的数量看上去有点少。然而,因为每个线程都在不停地对表进行操作,所以这与实际环境下使用这个表的更多数量的线程的争用情况基本等同。
表1.Hashtable 与ConcurrentHashMap在可伸缩性方面的比较
线程数 | ConcurrentHashMap | Hashtable |
1 | 1.00 | 1.03 |
2 | 2.59 | 32.40 |
4 | 5.58 | 78.23 |
8 | 13.21 | 163.48 |
16 | 27.58 | 341.21 |
32 | 57.27 | 778.41 |
CopyOnWriteArrayList
在那些遍历操作大大地多于插入或移除操作的并发应用程序中,一般用 CopyOnWriteArrayList 类替代 ArrayList 。如果是用于存放一个侦听器(listener)列表,例如在AWT或Swing应用程序中,或者在常见的JavaBean中,那么这种情况很常见(相关的 CopyOnWriteArraySet 使用一个 CopyOnWriteArrayList 来实现 Set 接口)。
如果您正在使用一个普通的 ArrayList 来存放一个侦听器列表,那么只要该列表是可变的,而且可能要被多个线程访问,您就必须要么在对其进行迭代操作期间,要么在迭代前进行的克隆操作期间,锁定整个列表,这两种做法的开销都很大。
当对列表执行会引起列表发生变化的操作时, CopyOnWriteArrayList 并不是为列表创建一个全新的副本,它的迭代器肯定能够返回在迭代器被创建时列表的状态,而不会抛出 ConcurrentModificationException 。在对列表进行迭代之前不必克隆列表或者在迭代期间锁定列表,因为迭代器所看到的列表的副本是不变的。换句话说, CopyOnWriteArrayList 含有对一个不可变数组的一个可变的引用,因此,只要保留好那个引用,您就可以获得不可变的线程安全性的好处,而且不用锁定列表。
结束语
同步的集合类 Hashtable 和 Vector ,以及同步的包装器类 Collections.synchronizedMap 和 Collections.synchronizedList ,为 Map 和 List 提供了基本的有条件的线程安全的实现。然而,某些因素使得它们并不适用于具有高度并发性的应用程序中――它们的集合范围的单锁特性对于可伸缩性来说是一个障碍,而且,很多时候还必须在一段较长的时间内锁定一个集合,以防止出现 ConcurrentModificationException s异常。 ConcurrentHashMap 和CopyOnWriteArrayList 实现提供了更高的并发性,同时还保住了线程安全性,只不过在对其调用者的承诺上打了点折扣。 ConcurrentHashMap 和 CopyOnWriteArrayList 并不是在您使用HashMap 或 ArrayList 的任何地方都一定有用,但是它们是设计用来优化某些特定的公用解决方案的。许多并发应用程序将从对它们的使用中获得好处。
java同步详解
Java语言的关键字synchronized,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
五、以上规则对其它对象锁同样适用.
举例说明:
一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
package ths;
public classThread1 implements Runnable {
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " synchronized loop" + i);
}
}
}
public static void main(String[] args) {
Thread1 t1 = new Thread1();
Thread ta = new Thread(t1, "400电话");
Thread tb = new Thread(t1, "800电话");
ta.start();
tb.start();
}
}
结果:
400电话 synchronized loop 0
400电话 synchronized loop 1
400电话 synchronized loop 2
400电话 synchronized loop 3
400电话 synchronized loop 4
800电话 synchronized loop 0
800电话 synchronized loop 1
800电话 synchronized loop 2
800电话 synchronized loop 3
800电话 synchronized loop 4
二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
package ths;
public classThread2 {
public void m4t1() {
synchronized(this) {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " +i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
}
public void m4t2() {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " +i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
public static void main(String[] args) {
final Thread2 myt2 = new Thread2();
Thread t1 = new Thread( new Runnable() { public void run() { myt2.m4t1(); } }, "t1" );
Thread t2 = new Thread( new Runnable() { public void run(){ myt2.m4t2(); } }, "t2" );
t1.start();
t2.start();
}
}
结果:
t1 : 4
t2 : 4
t1 : 3
t2 : 3
t1 : 2
t2 : 2
t1 : 1
t2 : 1
t1 : 0
t2 : 0
三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
//修改Thread2.m4t2()方法:
public void m4t2() {
synchronized(this) {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " +i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
}
结果:
t1 : 4
t1 : 3
t1 : 2
t1 : 1
t1 : 0
t2 : 4
t2 : 3
t2 : 2
t2 : 1
t2 : 0
四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
//修改Thread2.m4t2()方法如下:
public synchronizedvoid m4t2() {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " +i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
结果:
t1 : 4
t1 : 3
t1 : 2
t1 : 1
t1 : 0
t2 : 4
t2 : 3
t2 : 2
t2 : 1
t2 : 0
五、以上规则对其它对象锁同样适用:
package ths;
public classThread3 {
class Inner {
private void m4t1() {
int i = 5;
while(i-- > 0) {
System.out.println(Thread.currentThread().getName() + " :Inner.m4t1()=" + i);
try {
Thread.sleep(500);
} catch(InterruptedException ie) {
}
}
}
private void m4t2() {
int i = 5;
while(i-- > 0) {
System.out.println(Thread.currentThread().getName() + " :Inner.m4t2()=" + i);
try {
Thread.sleep(500);
} catch(InterruptedException ie) {
}
}
}
}
private void m4t1(Inner inner) {
synchronized(inner) { //使用对象锁
inner.m4t1();
}
private void m4t2(Inner inner) {
inner.m4t2();
}
public static void main(String[] args) {
final Thread3 myt3 = new Thread3();
final Inner inner = myt3.new Inner();
Thread t1 = new Thread( new Runnable() {public void run() { myt3.m4t1(inner);}}, "t1");
Thread t2 = new Thread( new Runnable() {public void run() { myt3.m4t2(inner);}}, "t2");
t1.start();
t2.start();
}
}
结果:
尽管线程t1获得了对Inner的对象锁,但由于线程t2访问的是同一个Inner中的非同步部分。所以两个线程互不干扰。
t1 :Inner.m4t1()=4
t2 : Inner.m4t2()=4
t1 : Inner.m4t1()=3
t2 : Inner.m4t2()=3
t1 : Inner.m4t1()=2
t2 : Inner.m4t2()=2
t1 : Inner.m4t1()=1
t2 : Inner.m4t2()=1
t1 : Inner.m4t1()=0
t2 : Inner.m4t2()=0
现在在Inner.m4t2()前面加上synchronized:
privatesynchronized void m4t2() {
int i = 5;
while(i-- > 0) {
System.out.println(Thread.currentThread().getName() + " :Inner.m4t2()=" + i);
try {
Thread.sleep(500);
} catch(InterruptedException ie) {
}
}
}
结果:
尽管线程t1与t2访问了同一个Inner对象中两个毫不相关的部分,但因为t1先获得了对Inner的对象锁,所以t2对Inner.m4t2()的访问也被阻塞,因为m4t2()是Inner中的一个同步方法。
t1 :Inner.m4t1()=4
t1 : Inner.m4t1()=3
t1 : Inner.m4t1()=2
t1 : Inner.m4t1()=1
t1 : Inner.m4t1()=0
t2 : Inner.m4t2()=4
t2 : Inner.m4t2()=3
t2 : Inner.m4t2()=2
t2 : Inner.m4t2()=1
t2 : Inner.m4t2()=0
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 (如前所述,可以是类实例或类)的锁方能执行,具体机
制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。 对synchronized(this)的一些理解一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线
程必须等待当前线程执行完这个代码块以后才能执行该代码块。二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized
(this)同步代码块。 三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)
同步代码块的访问将被阻塞。
四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个
object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
五、以上规则对其它对象锁同样适用
打个比方:一个object就像一个大房子,大门永远打开。房子里有 很多房间(也就是方法)。
这些房间有上锁的(synchronized方法), 和不上锁之分(普通方法)。房门口放着一把钥匙(key),这把钥匙可以打开所有上锁的房间。
另外我把所有想调用该对象方法的线程比喻成想进入这房子某个 房间的人。所有的东西就这么多了,下面我们看看这些东西之间如何作用的。
在此我们先来明确一下我们的前提条件。该对象至少有一个synchronized方法,否则这个key还有啥意义。当然也就不会有我们的这个主题了。
一个人想进入某间上了锁的房间,他来到房子门口,看见钥匙在那儿(说明暂时还没有其他人要使用上锁的 房间)。于是他走上去拿到了钥匙
,并且按照自己 的计划使用那些房间。注意一点,他每次使用完一次上锁的房间后会马上把钥匙还回去。即使他要连续使用两间上锁的房间,
中间他也要把钥匙还回去,再取回来。
因此,普通情况下钥匙的使用原则是:“随用随借,用完即还。”
这时其他人可以不受限制的使用那些不上锁的房间,一个人用一间可以,两个人用一间也可以,没限制。但是如果当某个人想要进入上锁的房
间,他就要跑到大门口去看看了。有钥匙当然拿了就走,没有的话,就只能等了。
要是很多人在等这把钥匙,等钥匙还回来以后,谁会优先得到钥匙?Not guaranteed。象前面例子里那个想连续使用两个上锁房间的家伙,他
中间还钥匙的时候如果还有其他人在等钥匙,那么没有任何保证这家伙能再次拿到。 (JAVA规范在很多地方都明确说明不保证,象
Thread.sleep()休息后多久会返回运行,相同优先权的线程那个首先被执行,当要访问对象的锁被 释放后处于等待池的多个线程哪个会优先得
到,等等。我想最终的决定权是在JVM,之所以不保证,就是因为JVM在做出上述决定的时候,绝不是简简单单根据 一个条件来做出判断,而是
根据很多条。而由于判断条件太多,如果说出来可能会影响JAVA的推广,也可能是因为知识产权保护的原因吧。SUN给了个不保证 就混过去了
。无可厚非。但我相信这些不确定,并非完全不确定。因为计算机这东西本身就是按指令运行的。即使看起来很随机的现象,其实都是有规律
可寻。学过 计算机的都知道,计算机里随机数的学名是伪随机数,是人运用一定的方法写出来的,看上去随机罢了。另外,或许是因为要想弄
的确定太费事,也没多大意义,所 以不确定就不确定了吧。)
再来看看同步代码块。和同步方法有小小的不同。
1.从尺寸上讲,同步代码块比同步方法小。你可以把同步代码块看成是没上锁房间里的一块用带锁的屏风隔开的空间。
2.同步代码块还可以人为的指定获得某个其它对象的key。就像是指定用哪一把钥匙才能开这个屏风的锁,你可以用本房的钥匙;你也可以指定
用另一个房子的钥匙才能开,这样的话,你要跑到另一栋房子那儿把那个钥匙拿来,并用那个房子的钥匙来打开这个房子的带锁的屏风。
记住你获得的那另一栋房子的钥匙,并不影响其他人进入那栋房子没有锁的房间。
为什么要使用同步代码块呢?我想应该是这样的:首先对程序来讲同步的部分很影响运行效率,而一个方法通常是先创建一些局部变
量,再对这些变量做一些 操作,如运算,显示等等;而同步所覆盖的代码越多,对效率的影响就越严重。因此我们通常尽量缩小其影响范围。
如何做?同步代码块。我们只把一个方法中该同 步的地方同步,比如运算。
另外,同步代码块可以指定钥匙这一特点有个额外的好处,是可以在一定时期内霸占某个对象的key。还记得前面说过普通情况下钥
匙的使用原则吗。现在不是普通情况了。你所取得的那把钥匙不是永远不还,而是在退出同步代码块时才还。
还用前面那个想连续用两个上锁房间的家伙打比方。怎样才能在用完一间以后,继续使用另一间呢。用同步代码块吧。先创建另外
一个线程,做一个同步代码 块,把那个代码块的锁指向这个房子的钥匙。然后启动那个线程。只要你能在进入那个代码块时抓到这房子的钥匙
,你就可以一直保留到退出那个代码块。也就是说 你甚至可以对本房内所有上锁的房间遍历,甚至再sleep(10*60*1000),而房门口却还有
1000个线程在等这把钥匙呢。很过瘾吧。
在此对sleep()方法和钥匙的关联性讲一下。一个线程在拿到key后,且没有完成同步的内容时,如果被强制sleep()了,那key还一
直在 它那儿。直到它再次运行,做完所有同步内容,才会归还key。记住,那家伙只是干活干累了,去休息一下,他并没干完他要干的事。为
了避免别人进入那个房间 把里面搞的一团糟,即使在睡觉的时候他也要把那唯一的钥匙戴在身上。
最后,也许有人会问,为什么要一把钥匙通开,而不是一个钥匙一个门呢?我想这纯粹是因为复杂性问题。一个钥匙一个门当然更
安全,但是会牵扯好多问题。钥匙 的产生,保管,获得,归还等等。其复杂性有可能随同步方法的增加呈几何级数增加,严重影响效率。这也
算是一个权衡的问题吧。为了增加一点点安全性,导致效 率大大降低,是多么不可取啊。
synchronized的一个简单例子
public classTextThread {
public static voidmain(String[] args) {
TxtThread tt = new TxtThread();
new Thread(tt).start();
new Thread(tt).start();
new Thread(tt).start();
new Thread(tt).start();
}
}
class TxtThreadimplements Runnable {
int num = 100;
String str = new String();
public void run() {
synchronized (str) {
while (num > 0) {
try {
Thread.sleep(1);
} catch (Exception e) {
e.getMessage();
}
System.out.println(Thread.currentThread().getName()
+ "this is " + num--);
}
}
}
}
上面的例子中为了制造一个时间差,也就是出错的机会,使用了Thread.sleep(10)
Java对多线程的支持与同步机制深受大家的喜爱,似乎看起来使用了synchronized关键字就可以轻松地解决多线程共享数据同步问题。到底如
何?――还得对synchronized关键字的作用进行深入了解才可定论。
总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,
synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
在进一步阐述之前,我们需要明确几点:
A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其
他线程的对象访问。
B.每个对象只有一个锁(lock)与之相关联。
C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
接着来讨论synchronized用到不同地方对代码产生的影响:
假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2就都可以调用它们。
1.把synchronized当作函数修饰符时,示例代码如下:
Public synchronizedvoid methodAAA()
{
//….
}
这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中
执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了
synchronized关键字的方法。
上边的示例代码等同于如下代码:
public voidmethodAAA()
{
synchronized(this)// (1)
{
//…..
}
}
(1)处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――那个
拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造
成数据混乱:(
2.同步块,示例代码如下:
public voidmethod3(SomeObject so)
{
synchronized(so)
{
//…..
}
}
这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明
确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:
class Fooimplements Runnable
{
private byte[] lock= new byte[0]; // 特殊的instance变量
Public voidmethodA()
{
synchronized(lock){ //… }
}
//…..
}
注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock
= new Object()则需要7行操作码。
3.将synchronized作用于static 函数,示例代码如下:
Class Foo
{
public synchronizedstatic void methodAAA() // 同步的static 函数
{
//….
}
public voidmethodBBB()
{
synchronized(Foo.class) // class literal(类名称字面常量)
}
}
代码中的methodBBB()方法是把classliteral作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这
个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。
记得在《Effective Java》一书中看到过将Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的
目的。P1指的是由Foo类产生的对象。
可以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj
在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。
小结如下:
搞清楚synchronized锁定的是哪个对象,就能帮助我们设计更安全的多线程程序。
还有一些技巧可以让我们对共享资源的同步访问更加安全:
1.定义private 的instance变量+它的 get方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象在外界可以
绕过同步方法的控制而直接取得它,并改动它。这也是JavaBean的标准实现方式之一。
2.如果instance变量是一个对象,如数组或ArrayList什么的,那上述方法仍然不安全,因为当外界对象通过get方法拿到这个instance对象
的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。 这个时候就需要将get方法也加上synchronized同步,并
且,只返回这个private对象的clone()――这样,调用端得到的就是对象副本的引用了