JAVA多线程synchronized详解
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA多线程synchronized详解相关的知识,希望对你有一定的参考价值。
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
-
当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
-
然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
-
尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
-
第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
-
以上规则对其它对象锁同样适用.
举例说明(使用synchronized):
在编写一个类时,如果该类中的代码可能运行于多线程环境下,那么就要考虑同步的问题。在Java中内置了语言级的同步原语--synchronized,这也大大简化了Java中多线程同步的使用。我们首先编写一个非常简单的多线程的程序,是模拟银行中的多个线程同时对同一个储蓄账户进行存款、取款操作的。
在程序中我们使用了一个简化版本的Account类,代表了一个银行账户的信息。在主程序中我们首先生成了10000个线程,然后启动它们,每一个线程都对John的账户进行存100元,然后马上又取出100元。这样,对于John的账户来说,最终账户的余额应该是还是1000元才对。然而运行的结果却超出我们的想像,首先来看看我们的演示代码:
1 package com.zb.notify;
2
3 /**
4 * 内部类
5 * 模拟银行账户,测试多线程环境下的存钱、取钱
6 *
7 * 作者: zhoubang
8 * 日期:2015年6月2日 上午10:09:52
9 */
10 class Account {
11 /**账户金额*/
12 float amount;
13
14 public Account(float amount) {
15 this.amount = amount;
16 }
17
18 /**存钱*/
19 public void deposit(float amt) {
20 float tmp = amount;
21 tmp += amt;
22
23 try {
24 /**模拟其它处理所需要的时间,比如存完钱,后台系统更新数据库字段值等*/
25 Thread.sleep(100);
26 } catch (InterruptedException e) {
27 }
28
29 amount = tmp;
30 }
31
32 /**取钱*/
33 public void withdraw(float amt) {
34 float tmp = amount;
35 tmp -= amt;
36
37 try {
38 /**模拟其它处理所需要的时间,比如存完钱,后台系统更新数据库字段值等*/
39 Thread.sleep(100);
40 } catch (InterruptedException e) {
41 }
42
43 amount = tmp;
44 }
45
46 public float getBalance() {
47 return amount;
48 }
49 }
50
51 /**
52 * 账户存取金额 多线程测试
53 *
54 * 作者: zhoubang
55 * 日期:2015年6月2日 上午10:05:05
56 */
57 public class AccountTest {
58 /**模拟多个线程同时操作该账户*/
59 private static int NUM_OF_THREAD = 10000;
60
61 static Thread[] threads = new Thread[NUM_OF_THREAD];
62
63 public static void main(String[] args) {
64 /**为账户初始化1000元*/
65 final Account acc = new Account(1000.0f);
66
67 /**遍历线程,多个线程同时存取账户金额:每一个线程调用存钱的方法之后,立即调用取钱。*/
68 for (int i = 0; i < NUM_OF_THREAD; i++) {
69 threads[i] = new Thread(new Runnable() {
70 public void run() {
71 acc.deposit(100.0f);
72 acc.withdraw(100.0f);
73 }
74 });
75 threads[i].start();
76 }
77
78 /** 主线程等待所有线程运行结束 */
79 for (int i = 0; i < NUM_OF_THREAD; i++) {
80 try {
81 threads[i].join();/**等待所有线程运行结束*/
82 } catch (InterruptedException e) {
83 }
84 }
85
86 System.out.println("所有线程执行完毕,最终的账户余额为:" + acc.getBalance());
87 }
88
89 }
注意,上面在Account的deposit和withdraw方法中之所以要把对amount的运算使用一个临时变量首先存储,sleep一段时间,然后,再赋值给amount,是为了模拟真实运行时的情况。因为在真实系统中,账户信息肯定是存储在持久媒介中,比如RDBMS中,此处的睡眠的时间相当于比较耗时的数据库操作,最后把临时变量tmp的值赋值给amount相当于把amount的改动写入数据库中。
运行AccountTest,结果如下(每一次结果都会不同):
所有线程执行完毕,最终的账户余额为:1500.0
所有线程执行完毕,最终的账户余额为:1700.0
所有线程执行完毕,最终的账户余额为:1300.0
。。。
为什么会出现这样的问题?这就是多线程中的同步的问题。
在我们的程序中,Account中的amount会同时被多个线程所访问,这就是一个竞争资源,通常称作竞态条件。
对于这样的多个线程共享的资源我们必须进行同步,以避免一个线程的改动被另一个线程所覆盖。
在我们这个程序中,Account中的amount是一个竞态条件,所以所有对amount的修改访问都要进行同步,
我们将deposit()和withdraw()方法进行同步,修改为:
1 package com.zb.notify;
2
3 /**
4 * 内部类
5 * 模拟银行账户,测试多线程环境下的存钱、取钱
6 *
7 * 作者: zhoubang
8 * 日期:2015年6月2日 上午10:09:52
9 */
10 class Account {
11 /**账户金额*/
12 float amount;
13
14 public Account(float amount) {
15 this.amount = amount;
16 }
17
18 /**存钱*/
19 public synchronized void deposit(float amt) {
20 float tmp = amount;
21 tmp += amt;
22
23 try {
24 /**模拟其它处理所需要的时间,比如存完钱,后台系统更新数据库字段值等*/
25 Thread.sleep(100);
26 } catch (InterruptedException e) {
27 }
28
29 amount = tmp;
30 }
31
32 /**取钱*/
33 public synchronized void withdraw(float amt) {
34 float tmp = amount;
35 tmp -= amt;
36
37 try {
38 /**模拟其它处理所需要的时间,比如存完钱,后台系统更新数据库字段值等*/
39 Thread.sleep(100);
40 } catch (InterruptedException e) {
41 }
42
43 amount = tmp;
44 }
45
46 public float getBalance() {
47 return amount;
48 }
49 }
50
51 /**
52 * 账户存取金额 多线程测试
53 *
54 * 作者: zhoubang
55 * 日期:2015年6月2日 上午10:05:05
56 */
57 public class AccountTest {
58 /**模拟多个线程同时操作该账户*/
59 private static int NUM_OF_THREAD = 10000;
60
61 static Thread[] threads = new Thread[NUM_OF_THREAD];
62
63 public static void main(String[] args) {
64 /**为账户初始化1000元*/
65 final Account acc = new Account(1000.0f);
66
67 /**遍历线程,多个线程同时存取账户金额:每一个线程调用存钱的方法之后,立即调用取钱。*/
68 for (int i = 0; i < NUM_OF_THREAD; i++) {
69 threads[i] = new Thread(new Runnable() {
70 public void run() {
71 acc.deposit(100.0f);
72 acc.withdraw(100.0f);
73 }
74 });
75 threads[i].start();
76 }
77
78 /** 主线程等待所有线程运行结束 */
79 for (int i = 0; i < NUM_OF_THREAD; i++) {
80 try {
81 threads[i].join();/**等待所有线程运行结束*/
82 } catch (InterruptedException e) {
83 }
84 }
85
86 System.out.println("所有线程执行完毕,最终的账户余额为:" + acc.getBalance());
87 }
88
89 }
此时,再运行,我们就能够得到正确的结果了。
Account中的getBalance()也访问了amount,为什么不对getBalance()同步呢?
因为getBalance()并不会修改amount的值,所以,同时多个线程对它访问不会造成数据的混乱。
同步加锁的是对象,而不是代码。
因此,如果你的类中有一个同步方法,这个方法可以被两个不同的线程同时执行,只要每个线程自己创建一个的该类的实例即可。
参考下面的代码:
1 package com.zb.notify;
2
3 class Foo extends Thread {
4 private int val;
5
6 public Foo(int v) {
7 val = v;
8 }
9
10 public synchronized void printVal(int v) {
11 while (true)
12 System.out.println(v);
13 }
14
15 public void run() {
16 printVal(val);
17 }
18 }
19
20 class SyncTest {
21 public static void main(String args[]) {
22 Foo f1 = new Foo(1);
23 f1.start();
24 Foo f2 = new Foo(3);
25 f2.start();
26 }
27 }
运行SyncTest产生的输出是1和3交叉的。
如果printVal是断面,你看到的输出只能是1或者只能是3而不能是两者同时出现。
程序运行的结果证明两个线程都在并发的执行printVal方法,即使该方法是同步的并且由于是一个无限循环而没有终止。
类的同步:
要实现真正的断面,你必须同步一个全局对象或者对类进行同步。下面的代码给出了一个这样的范例。
1 package com.zb.notify;
2
3 class Foo extends Thread {
4 private int val;
5
6 public Foo(int v) {
7 val = v;
8 }
9
10 public void printVal(int v) {
11 synchronized(Foo.class){
12 while (true)
13 System.out.println(v);
14 }
15 }
16
17 public void run() {
18 printVal(val);
19 }
20 }
上面的类不再对个别的类实例同步而是对类进行同步。
对于类Foo而言,它只有唯一的类定义,两个线程在相同的锁上同步,因此只有一个线程可以执行printVal方法。
这个代码也可以通过对公共对象加锁。例如给Foo添加一个静态成员。两个方法都可以同步这个对象而达到线程安全。
下面给出一个参考实现,给出同步公共对象的两种通常方法:
1、
1 package com.zb.notify;
2
3 class Foo extends Thread {
4 private int val;
5 private static Object lock = new Object();
6
7 public Foo(int v) {
8 val = v;
9 }
10
11 public void printVal(int v) {
12 synchronized (lock) {
13 while (true)
14 System.out.println(v);
15 }
16 }
17
18 public void run() {
19 printVal(val);
20 }
21 }
上面的这个例子比原文给出的例子要好一些,因为原文中的加锁是针对类定义的,一个类只能有一个类定义,而同步的一般原理是应该尽量减小同步的粒度以到达更好的性能。这里给出的范例的同步粒度比原文的要小。
2、
1 package com.zb.notify;
2
3 class Foo extends Thread {
4 private String name;
5 private String val;
6
7 public Foo(String name, String v) {
8 this.name = name;
9 val = v;
10 }
11
12 public void printVal() {
13 synchronized (val) {
14 while (true)
15 System.out.println(name + val);
16 }
17 }
18
19 public void run() {
20 printVal();
21 }
22 }
1 public class SyncMethodTest {
2 public static void main(String args[]) {
3 Foo f1 = new Foo("Foo 1:", "printVal");
4 f1.start();
5 Foo f2 = new Foo("Foo 2:", "printVal");
6 f2.start();
7 }
8 }
上面这个代码需要进行一些额外的说明,因为JVM有一种优化机制,因为String类型的对象是不可变的,因此当你使用""的形式引用字符串时,如果JVM发现内存已经有一个这样的对象,那么它就使用那个对象而不再生成一个新的String对象,这样是为了减小内存的使用。
上面的main方法其实等同于:
1 public static void main(String args[]) {
2 String value="printVal";
3 Foo f1 = new Foo("Foo 1:",value);
4 f1.start();
5 Foo f2 = new Foo("Foo 2:",value);
6 f2.start();
7 }
下面开始举第二个例子:
一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。
另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
1 package com.zb.notify;
2
3 public class Thread1 implements Runnable {
4 public void run() {
5 synchronized (this) {
6 for (int i = 0; i < 5; i++) {
7 System.out.println(Thread.currentThread().getName() + " synchronized loop " + i);
8 }
9 }
10 }
11
12 public static void main(String[] args) {
13 Thread1 t1 = new Thread1();
14 Thread ta = new Thread(t1, "A");
15 Thread tb = new Thread(t1, "B");
16 ta.start();
17 tb.start();
18 }
19 }
结果:
A synchronized loop 0
A synchronized loop 1
A synchronized loop 2
A synchronized loop 3
A synchronized loop 4
B synchronized loop 0
B synchronized loop 1
B synchronized loop 2
B synchronized loop 3
B synchronized loop 4
二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
1 package com.zb.notify;
2
3 public class Thread2 {
4 public void m4t1() {
5 synchronized (this) {
6 int i = 5;
7 while (i-- > 0) {
8 System.out.println(Thread.currentThread().getName() + " : " + i);
9 try {
10 Thread.sleep(500);
11 } catch (InterruptedException ie) {
12 }
13 }
14 }
15 }
16
17 public void m4t2() {
18 int i = 5;
19 while (i-- > 0) {
20 System.out.println(Thread.currentThread().getName() + " : " + i);
21 try {
22 Thread.sleep(500);
23 } catch (InterruptedException ie) {
24 }
25 }
26 }
27
28 public static void main(String[] args) {
29 final Thread2 myt2 = new Thread2();
30 Thread t1 = new Thread(new Runnable() {
31 public void run() {
32 myt2.m4t1();
33 }
34 }, "t1");
35 Thread t2 = new Thread(new Runnable() {
36 public void run() {
37 myt2.m4t2();
38 }
39 }, "t2");
40 t1.start();
41 t2.start();
42 }
43 }
结果:
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)同步代码块的访问将被阻塞。
1 // 修改Thread2.m4t2()方法:
2 public void m4t2() {
3 synchronized (this) {
4 int i = 5;
5 while (i-- > 0) {
6 System.out
7 .println(Thread.currentThread().getName() + " : " + i);
8 try {
9 Thread.sleep(500);
10 } catch (InterruptedException ie) {
11 }
12 }
13 }
14 }
结果:
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对象所有同步代码部分的访问都被暂时阻塞。
1 // 修改Thread2.m4t2()方法如下:
2 public synchronized void m4t2() {
3 int i = 5;
4 while (i-- > 0) {
5 System.out.println(Thread.currentThread().getName() + " : " + i);
6 try {
7 Thread.sleep(500);
8 } catch (InterruptedException ie) {
9 }
10 }
11 }
结果:
t1 : 4
t1 : 3
t1 : 2
t1 : 1
t1 : 0
t2 : 4
t2 : 3
t2 : 2
t2 : 1
t2 : 0
五、以上规则对其它对象锁同样适用:
1 package com.zb.notify; 2 3 public class Thread3 { 4 class Inner { 5 private void m4t1() { 6 int i = 5; 7 while (i-- > 0) { 8 System.out.println(Thread.currentThread().getName() + " : Inner.m4t1()=" + i); 9 try { 10 Thread.sleep(500); 11 } catch (InterruptedException ie) { 12 } 13 } 14 } 15 16 private void m4t2() { 17 int i = 5; 18 while (i-- > 0) { 19 System.out.println(Thread.currentThread().getName() + " : Inner.m4t2()=" + i); 20 try { 21 Thread.sleep(500); 22 } catch (InterruptedException ie) { 23 } 24 } 25 } 26 } 27 28 private void m4t1(Inner inner) { 29 synchronized(inner) { //使用对象锁 30 inner.m4t1(); 31 } 32 } 33 private void m4t2(Inner inner) { 34 inner.m4t2(); 35 } 36 37 public static void main(String[] args) { 38 final Foo myt3 = new Foo(); 39 final Inner inner = myt3.new Inner(); 40 Thread t1 = new Thread(new Runnable() { 41 public void run() { 42 myt3.m4t1(inner); 43 } 44 }, "t1"); 45 Thread t2 = new Thread(new Runnable() { 46 public void run() { 47 myt3.m4t2(inner); 48 } 49 }, "t2"); 50 t1.start(); 51 t2.start(); 52 } 53 }以上是关于JAVA多线程synchronized详解的主要内容,如果未能解决你的问题,请参考以下文章