多线程的同步机制对资源进行加锁,使得在同一个时间,只有一个线程可以进行操作,同步用以解决多个线程同时访问时可能出现的问题。
同步机制可以使用synchronized关键字实现。
当synchronized关键字修饰一个方法的时候,该方法叫做同步方法。
当synchronized方法执行完或发生异常时,会自动释放锁。
下面通过一个例子来对synchronized关键字的用法进行解析。
1,是否使用synchronized关键字的不同
1. public class ThreadTest
2. {
3. public static void main(String[] args)
4. {
5. Example example = new Example();
6.
7. Thread t1 = new Thread1(example);
8. Thread t2 = new Thread1(example);
9.
10. t1.start();
11. t2.start();
12. }
13.
14. }
15.
16. class Example
17. {
18. public synchronized void execute()
19. {
20. for (int i = 0; i < 10; ++i)
21. {
22. try
23. {
24. Thread.sleep(500);
25. }
26. catch (InterruptedException e)
27. {
28. e.printStackTrace();
29. }
30. System.out.println("Hello: " + i);
31. }
32. }
33.
34. }
35.
36. class Thread1 extends Thread
37. {
38. private Example example;
39.
40. public Thread1(Example example)
41. {
42. this.example = example;
43. }
44.
45. @Override
46. public void run()
47. {
48. example.execute();
49. }
50.
51. }
是否在execute()方法前加上synchronized关键字,这个例子程序的执行结果会有很大的不同。
如果不加synchronized关键字,则两个线程同时执行execute()方法,输出是两组并发的。
如果加上synchronized关键字,则会先输出一组0到9,然后再输出下一组,说明两个线程是顺次执行的。
2.多个方法的多线程情况
将程序改动一下,Example类中再加入一个方法execute2()。
之后再写一个线程类Thread2,Thread2中的run()方法执行的是execute2()。Example类中的两个方法都是被synchronized关键字修饰的。
1. public class ThreadTest
2. {
3. public static void main(String[] args)
4. {
5. Example example = new Example();
6.
7. Thread t1 = new Thread1(example);
8. Thread t2 = new Thread2(example);
9.
10. t1.start();
11. t2.start();
12. }
13.
14. }
15.
16. class Example
17. {
18. public synchronized void execute()
19. {
20. for (int i = 0; i < 20; ++i)
21. {
22. try
23. {
24. Thread.sleep((long) Math.random() * 1000);
25. }
26. catch (InterruptedException e)
27. {
28. e.printStackTrace();
29. }
30. System.out.println("Hello: " + i);
31. }
32. }
33.
34. public synchronized void execute2()
35. {
36. for (int i = 0; i < 20; ++i)
37. {
38. try
39. {
40. Thread.sleep((long) Math.random() * 1000);
41. }
42. catch (InterruptedException e)
43. {
44. e.printStackTrace();
45. }
46. System.out.println("World: " + i);
47. }
48. }
49.
50. }
51.
52. class Thread1 extends Thread
53. {
54. private Example example;
55.
56. public Thread1(Example example)
57. {
58. this.example = example;
59. }
60.
61. @Override
62. public void run()
63. {
64. example.execute();
65. }
66.
67. }
68.
69. class Thread2 extends Thread
70. {
71. private Example example;
72.
73. public Thread2(Example example)
74. {
75. this.example = example;
76. }
77.
78. @Override
79. public void run()
80. {
81. example.execute2();
82. }
83.
84. }
如果去掉synchronized关键字,则两个方法并发执行,并没有相互影响。
但是如例子程序中所写,即便是两个方法:
执行结果永远是执行完一个线程的输出再执行另一个线程的。
说明:
如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的。
结论:
当synchronized关键字修饰一个方法的时候,该方法叫做同步方法。
Java中的每个对象都有一个锁(lock),或者叫做监视器(monitor),当一个线程访问某个对象的synchronized方法时,将该对象上锁,其他任何线程都无法再去访问该对象的synchronized方法了(这里是指所有的同步方法,而不仅仅是同一个方法),直到之前的那个线程执行方法完毕后(或者是抛出了异常),才将该对象的锁释放掉,其他线程才有可能再去访问该对象的synchronized方法。
注意这时候是给对象上锁,如果是不同的对象,则各个对象之间没有限制关系。
尝试在代码中构造第二个线程对象时传入一个新的Example对象,则两个线程的执行之间没有什么制约关系。
3.考虑静态的同步方法
当一个synchronized关键字修饰的方法同时又被static修饰,之前说过,非静态的同步方法会将对象上锁,但是静态方法不属于对象,而是属于类,它会将这个方法所在的类的Class对象上锁。
一个类不管生成多少个对象,它们所对应的是同一个Class对象。
1. public class ThreadTest
2. {
3. public static void main(String[] args)
4. {
5. Example example = new Example();
6.
7. Thread t1 = new Thread1(example);
8.
9. // 此处即便传入不同的对象,静态方法同步仍然不允许多个线程同时执行
10. example = new Example();
11.
12. Thread t2 = new Thread2(example);
13.
14. t1.start();
15. t2.start();
16. }
17.
18. }
19.
20. class Example
21. {
22. public synchronized static void execute()
23. {
24. for (int i = 0; i < 20; ++i)
25. {
26. try
27. {
28. Thread.sleep((long) Math.random() * 1000);
29. }
30. catch (InterruptedException e)
31. {
32. e.printStackTrace();
33. }
34. System.out.println("Hello: " + i);
35. }
36. }
37.
38. public synchronized static void execute2()
39. {
40. for (int i = 0; i < 20; ++i)
41. {
42. try
43. {
44. Thread.sleep((long) Math.random() * 1000);
45. }
46. catch (InterruptedException e)
47. {
48. e.printStackTrace();
49. }
50. System.out.println("World: " + i);
51. }
52. }
53.
54. }
55.
56. class Thread1 extends Thread
57. {
58. private Example example;
59.
60. public Thread1(Example example)
61.
62. {
63. this.example = example;
64. }
65.
66. @Override
67. public void run()
68. {
69. Example.execute();
70. }
71.
72. }
73.
74. class Thread2 extends Thread
75. {
76. private Example example;
77.
78. public Thread2(Example example)
79. {
80. this.example = example;
81. }
82.
83. @Override
84. public void run()
85. {
86. Example.execute2();
87. }
88.
89. }
所以如果是静态方法的情况(execute()和execute2()都加上static关键字),即便是向两个线程传入不同的Example对象,这两个线程仍然是互相制约的,必须先执行完一个,再执行下一个。
结论:
如果某个synchronized方法是static的,那么当线程访问该方法时,它锁的并不是synchronized方法所在的对象,而是synchronized方法所在的类所对应的Class对象。Java中,无论一个类有多少个对象,这些对象会对应唯一一个Class对象,因此当线程分别访问同一个类的两个对象的两个static,synchronized方法时,它们的执行顺序也是顺序的,也就是说一个线程先去执行方法,执行完毕后另一个线程才开始。
4. synchronized块
synchronized块写法:
synchronized(object)
{
}
表示线程在执行的时候会将object对象上锁。(注意这个对象可以是任意类的对象,也可以使用this关键字)。
这样就可以自行规定上锁对象。
1. public class ThreadTest
2. {
3. public static void main(String[] args)
4. {
5. Example example = new Example();
6.
7. Thread t1 = new Thread1(example);
8. Thread t2 = new Thread2(example);
9.
10. t1.start();
11. t2.start();
12. }
13.
14. }
15.
16. class Example
17. {
18. private Object object = new Object();
19.
20. public void execute()
21. {
22. synchronized (object)
23. {
24. for (int i = 0; i < 20; ++i)
25. {
26. try
27. {
28. Thread.sleep((long) Math.random() * 1000);
29. }
30. catch (InterruptedException e)
31. {
32. e.printStackTrace();
33. }
34. System.out.println("Hello: " + i);
35. }
36.
37. }
38.
39. }
40.
41. public void execute2()
42. {
43. synchronized (object)
44. {
45. for (int i = 0; i < 20; ++i)
46. {
47. try
48. {
49. Thread.sleep((long) Math.random() * 1000);
50. }
51. catch (InterruptedException e)
52. {
53. e.printStackTrace();
54. }
55. System.out.println("World: " + i);
56. }
57.
58. }
59.
60. }
61.
62. }
63.
64. class Thread1 extends Thread
65. {
66. private Example example;
67.
68. public Thread1(Example example)
69. {
70. this.example = example;
71. }
72.
73. @Override
74. public void run()
75. {
76. example.execute();
77. }
78.
79. }
80.
81. class Thread2 extends Thread
82. {
83. private Example example;
84.
85. public Thread2(Example example)
86. {
87. this.example = example;
88. }
89.
90. @Override
91. public void run()
92. {
93. example.execute2();
94. }
95.
96. }
例子程序4所达到的效果和例子程序2的效果一样,都是使得两个线程的执行顺序进行,而不是并发进行,当一个线程执行时,将object对象锁住,另一个线程就不能执行对应的块。
synchronized方法实际上等同于用一个synchronized块包住方法中的所有语句,然后在synchronized块的括号中传入this关键字。当然,如果是静态方法,需要锁定的则是class对象。
可能一个方法中只有几行代码会涉及到线程同步问题,所以synchronized块比synchronized方法更加细粒度地控制了多个线程的访问,只有synchronized块中的内容不能同时被多个线程所访问,方法中的其他语句仍然可以同时被多个线程所访问(包括synchronized块之前的和之后的)。
注意:被synchronized保护的数据应该是私有的。
结论:
synchronized方法是一种粗粒度的并发控制,某一时刻,只能有一个线程执行该synchronized方法;
synchronized块则是一种细粒度的并发控制,只会将块中的代码同步,位于方法内、synchronized块之外的其他代码是可以被多个线程同时访问到的。
JDK 5.0的并发包
使用synchronized关键字解决线程的同步问题会带来一些执行效率上的问题。
JDK1.4及之前是无法避免这些问题的。
登录 | 立即注册