转自
http://earthrelic.blog.sohu.com/157151118.html
熟悉 Java 的多线程的一般都知道会有数据不一致的情况发生,比如两个线程在操作同一个类变量时,而保护数据不至于错乱的办法就是让方法同步或者代码块同步。同步时非原子操作就得同步,比如一个简单的 1.2+1 运算也该同步,以保证一个代码块或方法成为一个原子操作。
简单点说就是给在多线程环境中可能会造成数据破坏的方法,做法有两种,以及一些疑问:
1. 不论是静态的或非静态的方法都加上 synchronized 关键字,那静态的方法和非静态的方法前加上 synchronized 关键字有区别吗?
2. 或者在可疑的代码块两旁用 synchronized(this) 或 synchronized(someObject) 包裹起来,而选用 this 还是某一个对象--someObject,又有什么不同呢?
3. 对方法加了 synchronized 关键字或用 synchronized(xxx) 包裹了代码,就一定能避免多线程环境下的数据破坏吗?
4. 对方法加 synchronized 关键字与用 synchronized(xxx) 同步代码块两种规避方法又有什么分别和联系呢?
为了理解上面的问题,我们还得从 Java 对线程同步的原理上说起。我们知道 Java 直接在语言级上支持多线程的。在多线程环境中我们要小心的数据是:
1) 保存在堆中的实例变量
2) 保存在方法区中的类变量。
现实点说呢就是某个方法会触及到的同一个变量,如类变量或单态实例的实例变量。避免冲突的最容易想到的办法就是同一时刻只让一个线程去执行某段代码块或方法,于是我们就要给一段代码块或整个方法体标记出来,被保护的代码块或方法体在 Java 里叫做监视区域(Monitor Region),类似的东西在 C++ 中叫做临界区(Critical Section)。
比如说一段代码:
01.public void operate() {
02. flag ++;
03. try {
04. //休眠一个随机时间,让不同线程能在此交替执行
05. Thread.sleep(new Random().nextInt(10));
06. } catch (InterruptedException e) {
07. e.printStackTrace();
08. }
09. flag --;
10. System.out.println("Current flag: " + flag);
11.}
用 synchronized 标记起来的话,可以写成:
01.public void operate() {
02. synchronized(this){//只需要把可能造成麻烦的代码标记起来
03. flag ++;
04. try {
05. //休眠一个随机时间,让不同线程能在此交替执行
06. Thread.sleep(new Random().nextInt(5));
07. } catch (InterruptedException e) {
08. e.printStackTrace();
09. }
10. flag --;
11.
12. System.out.println("Current flag: " + flag);
13. }
14.
15. //some code out of the monitor region
16. System.out.println("线程安全的代码放外面就行啦");
17.
18.}
那如果我们悲观,或许是偷点懒,直接给方法加个 synchronized 关键字就行,就是这样:
01.public synchronized void operate() {
02. flag ++;
03. try {
04. //休眠一个随机时间,让不同线程能在此交替执行
05. Thread.sleep(new Random().nextInt(10));
06. } catch (InterruptedException e) {
07. e.printStackTrace();
08. }
09. flag --;
10. System.out.println("Current flag: " + flag);
11.}
给方法加个关键字 synchronized 其实就是相当于把方法中的所有代码行框到了 synchronized(xxx) 块中。同步肯定会影响到效率,这也是大家知道的,因为它会造成方法调用的等待。方法中有些代码可能是线程安全的,所以可不用包裹在 synchronized(xxx) 中。
那么只要给方法加上关键字 synchronized,或者 synchronized(this) 括起一段代码一定就是线程安全的吗?现在来看个例子,比如类 TestMultiThread:
01.package com.unmi;
02.
03.import java.util.Random;
04.
05./**
06. * 多线程测试程序
07. *
08. * @author Unmi
09. */
10.public class TestMultiThread {
11.
12. // 一个标志值
13. private static int flag = 1;
14.
15. /**
16. * @param args
17. */
18. public static void main(String[] args) {
19. new Thread("Thread-01") {
20. public void run() {
21. new TestMultiThread().operate();
22. }
23. }.start(); // 启动第一个线程
24.
25. new Thread("Thread-02") {
26. public void run() {
27. new TestMultiThread().operate();
28. }
29. }.start(); // 启动第二个线程
30. }
31.
32. /**
33. * 对 flag 进行一个自增,然后自减的操作,正常情况下 flag 还应是 1
34. */
35. public void operate() {
36. flag++;
37. try {
38. // 增加随机性,让不同线程能在此交替执行
39. Thread.sleep(new Random().nextInt(5));
40. } catch (InterruptedException e) {
41. e.printStackTrace();
42. }
43. flag--;
44.
45. System.out.println("Thread: " + Thread.currentThread().getName()
46. + " /Current flag: " + flag);
47. }
48.}
有一个静态变量 flag = 1,还有一个实例方法 operate() 方法,对 flag 进行 flag ++,然后 flag -- 操作,最后输出当前的 flag 值,理想情况下,输出的 flag 应该仍然是 1。可实际上是两个线程执行行的输出很大的机会得到:
Thread: Thread-01 /Current flag: 2
Thread: Thread-02 /Current flag: 1
好,我们也知道那是因为线程在对 flag 操作不同步引起的,对照代码来理解就是:
当线程 Thread-01 执行到 flag ++ 后,此时 flag 等于 2,有个 sleep,能使得 Thread-01 稍事休息
此时线程 Thread-02 进入方法 operate,并相执行 flag ++,即当前的 2 ++,flag 为 3 了,碰到 sleep 也停顿一下
Thread-01 又再执行剩下的 flag --,在当前的 flag 为 3 基础上进行 flag --,最后输出 Thread: Thread-01 /Current flag: 2
Thread-02 接着执行 flag --,当前 flag 为 2,flag -- 后输出就是 Thread: Thread-02 /Current flag: 1
注:在 flag++ 与 flag -- 之前加个随机的 sleep 是为了模拟有些环境,比如某个线程执行快,另一个线程执行慢的可能性,多执行几遍,你也能看到另外几种输出:
Thread: Thread-02 /Current flag: 2
Thread: Thread-01 /Current flag: 1
和
Thread: Thread-02 /Current flag: 1
Thread: Thread-01 /Current flag: 1
和
Thread: Thread-01 /Current flag: 1
Thread: Thread-02 /Current flag: 1
出现不同状况的可能性都好理解。为确保 flag 的完整性,于是加上 synchronized(this) 把代码 flag ++ 和 flag -- 代码块同步了,最后的 operate() 方法的代码如下:
01.public void operate() {
02. synchronized(this){//只需要把可能制造麻烦的代码标记起来
03. flag ++;
04. try {
05. //增加随机性,让不同线程能在此交替执行
06. Thread.sleep(new Random().nextInt(5));
07. } catch (InterruptedException e) {
08. e.printStackTrace();
09. }
10. flag --;
11.
12. System.out.println("Thread: "+ Thread.currentThread().getName() +
13. " /Current flag: " + flag);
14. }
15.
16. //some code out of the monitor region
17. System.out.print("");
18.}
再次执行上面的测试代码,仍然会看到如下的输出:
Thread: Thread-01 /Current flag: 2
Thread: Thread-02 /Current flag: 1
而不是我们所期盼的两次输出 flag 值都应为 1 的结果。难道 synchronized 也灵验了,非也,玄机就在 synchronized() 中的那个对象的选取上,我们用 this 在这里不可行。
现在来解析跟在 synchronized 后面的那个对象参数。在 JVM 中,每个对象和类(其实是类本身的实例) 在逻辑上都是和一个监视器相关联的,监视器指的就是被同步的方法或代码块。这句话不好理解,主谓调换一下再加上另外几条规则:
1) Java 程序中每一个监视区域都和一个对象引用相关联,譬如 synchronized(this) 中的 this 对象。
2) 线程在进入监视区域前必须对相关联的对象进行加锁,退出监视区域后释放该锁。
3) 不同线程在进入同一监视区域不能对关联对象加锁多次。意即 A 线程在进入 M 监视区域时,获得了关联对象 O 的锁,在未释放该锁之前,另一线程 B 无法获得 M 监视区域的对象锁,此时就要等待 A 线程释放锁。但是 A 线程可能对 O 加锁多次(递归调用就可能出现这种情况)。
4) 线程只能获得了监视区域相关联的对象锁,才能执行监视区域内的代码,否则等待。JVM 维护了一个监视区域相关联的对象锁的计数,比如 A 线程对监视区域 M 相关联的 O 对象加锁了 N 次,计数则为一,要等锁全部释放了,计数即为零,此时另一线程 B 才能获得该对象锁。
好了,明白了线程,监视区域,相关联对象,对象锁的关系之后,我们就可以理解上面的程序为何加了 synchronized(this) 后还是未能如我们所愿呢?
监视区域与 this 对象相关联的
线程 Thread-01 进入监视区域时,对此时的 this 对象加锁,也就是获得了 this 对象锁。因为代码中有意加了个 sleep 语句,所以还不会立即释放该锁
这时候线程 Thread-02 要求进入同一监视区域,也试图获得此时的 this 对象锁,并执行其中的代码
从执行的结果,或者可进行断点调试,你会发现,尽管 Thread-01 获得了 this 对象锁后,还未释放该锁时,另一线程 Thread-02 也可轻而易举的获得 this 对象锁,并同时执行监视区域中的代码。
前面不是说过,某一线程对监视区域相关联对象加锁上后,另一线程将不能同时对该对象加锁,必须等待其他线程释放该对象锁才行吗?这句话千真万确,原因就在于此 this 非彼 this,也就是 this 指代的对象一直在变。Thread-01 进入监视区域是对 this 代表的 new TestMultiThread() 对象,即使你没有释放该锁,Thread-02 在进入同一监视区域时当然还能对 this 代表的另一 new TestMultiThread() 对象加锁的。
所以说这里机械的框上 synchronized(this) 其实起不到任何效果,正确的做法,可以写成
synchronized(TestMultiThread.class){...}; //TestMultiThread 类实例在同一个 JVM 中指的就是同一个对象(不同 ClassLoader 时不考虑)
或者预先在 TestMultiThread 中声明一个静态变量,如 private static Object object = new Ojbect();,然后 synchronized 部分写成
synchronized(object){...}
然后再执行前面的测试代码,保管每回执行后,输出的两次 flag 的值都为 1。
又有人会有疑问了,难道就不能用 synchronized(this) 这样的写法了吗?这种写法也没少见啊,不能说人家总是错的吧。在有些时候,能确保每一次 this 会指向到与前面相同的对象时都不会有问题的,如单态类的 this。
到这里,前面的第二个疑问也同时得到解决了,答案是不一定,看关联对象是否同一个,有时候应分析实际的运行环境。