前言
1.Synchronized
关键字由JVM虚拟机实现,平时用到的频率还是很高的,对于锁的作用域理解是很有必要的,如果锁的粒度太小,显然不值得牺牲CPU
切换线程的开销。粒度太大,效率同样会大打折扣。具体要选用合适的锁粒度就必须对方法锁、对象锁、类锁等概念有清晰的认识。
概念
1.方法内,修饰一段代码块,称为同步语句块,那么作用域的范围就是{}
内的内容,此时锁的对象就是调用方法的对象。object.method()
,锁的对象为object
。
2.修饰一个普通方法(区别于静态方法),此方法就为同步方法,作用域为整个方法。此时锁的对象为调用方法的对象。
3.修饰一个静态方法,回忆class
加载的机制,静态方法或者静态变量,由于静态方法属于这个类,内存中只有一份,此时锁的对象就是class对象
,class
在虚拟机中只会加载一次。
同步代码块
1.方法内部中一块代码被Synchronized
修饰:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55public class SynStatement implements Runnable {
private static int count = 0;
public SynStatement() {
}
public void run() {
synchronized (this) {
method();
}
}
private void method() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "count = " + count++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//调用的测试
public static void main(String[] args) {
//开启了两个线程,但是访问的是同一个对象synStatement
SynStatement synStatement = new SynStatement();
new Thread(synStatement).start();
new Thread(synStatement).start();
}
//打印的信息,可以看到等到线程Thread0与Thread1是互斥的,等到Thread0执行完之后Thread1才会执行。因为锁的对象是同一个synStatement
> Task :Sai.main()
Thread-0count = 0
Thread-0count = 1
Thread-0count = 2
Thread-0count = 3
Thread-0count = 4
Thread-0count = 5
Thread-0count = 6
Thread-0count = 7
Thread-0count = 8
Thread-0count = 9
Thread-1count = 10
Thread-1count = 11
Thread-1count = 12
Thread-1count = 13
Thread-1count = 14
Thread-1count = 15
Thread-1count = 16
Thread-1count = 17
Thread-1count = 18
Thread-1count = 19
2.修改SynStatement
中的方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class SynStatement implements Runnable {
private static final Object object = new Object();
// private static final byte[] bytes = new byte[0];
private static int count = 0;
public SynStatement() {
}
public void run() {
synchronized (object) {
method();
}
}
private void method() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "count = " + count++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.可以发现打印的结果是一样的,区别于synchronized(this)
的锁对象,这里的锁住的是object
,而不是调用此方法的对象了。修改测试方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public static void main(String[] args) {
//SynStatement synStatement = new SynStatement();
new Thread(new SynStatement()).start();
new Thread(new SynStatement()).start();
}
//打印信息
Thread-0count = 0
Thread-0count = 1
Thread-0count = 2
Thread-0count = 3
Thread-0count = 4
Thread-0count = 5
Thread-0count = 6
Thread-0count = 7
Thread-0count = 8
Thread-0count = 9
Thread-1count = 10
Thread-1count = 11
Thread-1count = 12
Thread-1count = 13
Thread-1count = 14
Thread-1count = 15
Thread-1count = 16
//有时候不太清楚具体要锁住哪个对象经常在源码中可以看到private static final Object object = new Object();而直接用object被synchronized修饰
//推荐还是使用Object object = new Object()的方式,大多数源码依据此方式,但是看资料显示byte[] bytes = new byte[0];这条语句虚拟机执行只需要三
//行指令,而Object object = new Object();需要七条。跟源码走还是比较靠谱一点的。
4.切换到synchronized(this)
,当实例子化两个SynStatement
看看会发生什么:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27public static void main(String[] args) {
//SynStatement synStatement = new SynStatement();
new Thread(new SynStatement()).start();
new Thread(new SynStatement()).start();
}
> Task :Sai.main()
Thread-0count = 0
Thread-1count = 1
Thread-0count = 2
Thread-1count = 3
Thread-0count = 4
Thread-1count = 5
Thread-1count = 6
Thread-0count = 7
Thread-1count = 8
Thread-0count = 8
Thread-1count = 9
Thread-0count = 9
Thread-1count = 10
Thread-0count = 10
Thread-0count = 11
Thread-1count = 11
Thread-1count = 12
Thread-0count = 13
Thread-1count = 14
Thread-0count = 15
5.可以发现两个线程Thread0、Thread1
互不干扰,交替执行,之前提到,修饰同步代码块时,锁的对象是调用者,这里实例化了两个SynStatement
,因此锁不同。另外可以发现,出现资源冲突问题,这也是必然的(count
只有一份)。
6.当线程访问一个对象的同步代码块时,普通方法的访问是否会阻塞?答案是否定,对于普通方法依然可以访问,就不测试了。
修饰普通实例方法
1.修改之前的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class SynStatement implements Runnable {
private static final Object object = new Object();
// private static final byte[] bytes = new byte[0];
private static int count = 0;
public SynStatement() {
}
public synchronized void run() {
method();
}
private void method() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "count = " + count++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//只是作用域比代码块更大(可能除了method之外,run中还有method2等),但是锁的对象还是调用的对象实例。
修饰静态方法
1.还是修改之前的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52public class SynStatement implements Runnable {
private static final Object object = new Object();
// private static final byte[] bytes = new byte[0];
private static int count = 0;
public SynStatement() {
}
public void run() {
method();
}
private synchronized static void method() {
for (int i = 0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "--count = " + count++);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//测试方法
public static void main(String[] args) {
SynStatement synStatement = new SynStatement();
new Thread(new SynStatement()).start();
new Thread(new SynStatement()).start();
new Thread(new SynStatement()).start();
new Thread(new SynStatement()).start();
}
//打印的信息
> Task :Sai.main()
Thread-0--count = 0
Thread-0--count = 1
Thread-0--count = 2
Thread-0--count = 3
Thread-3--count = 4
Thread-3--count = 5
Thread-3--count = 6
Thread-3--count = 7
Thread-2--count = 8
Thread-2--count = 9
Thread-2--count = 10
Thread-2--count = 11
Thread-1--count = 12
Thread-1--count = 13
Thread-1--count = 14
Thread-1--count = 15
2.前面已经提到,类中的静态方法是属于类的class
对象是属于静态方法的锁,而class
被虚拟机加载,只有一份,因此即使实例再多SynStatement
,只有首先获取锁的线程才可以执行,其他线程会阻塞直到之前的执行完毕。同时一个有趣的现象是线程排列居然是0-3-2-1
,现在修改测试代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69public class SynStatement implements Runnable {
//private static final Object object = new Object();
// private static final byte[] bytes = new byte[0];
private static int count = 0;
private final Thread thread;
public SynStatement(Thread thread) {
this.thread = thread;
}
public void run() {
if (null != thread) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
method();
}
private synchronized static void method() {
for (int i = 0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "--count = " + count++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//测试代码
public static void main(String[] args) {
Thread prev = Thread.currentThread();
System.out.println(prev.getName());
for (int i = 0; i < 5; i++) {
//每个线程持有前一个线程的引用,等到前一个线程执行在继续执行
Thread thread = new Thread(new SynStatement(prev));
thread.start();
prev = thread;
}
}
//打印信息
> Task :Sai.main()
main
Thread-0--count = 0
Thread-0--count = 1
Thread-0--count = 2
Thread-0--count = 3
Thread-1--count = 4
Thread-1--count = 5
Thread-1--count = 6
Thread-1--count = 7
Thread-2--count = 8
Thread-2--count = 9
Thread-2--count = 10
Thread-2--count = 11
Thread-3--count = 12
Thread-3--count = 13
Thread-3--count = 14
Thread-3--count = 15
Thread-4--count = 16
Thread-4--count = 17
Thread-4--count = 18
Thread-4--count = 19
//可以看到配合join可以让线程按照顺序执行
修饰一个类(class)
1.修改SynStatement
中的同步方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61public class SynStatement implements Runnable {
//private static final Object object = new Object();
// private static final byte[] bytes = new byte[0];
private static int count = 0;
private final Thread thread;
public SynStatement(Thread thread) {
this.thread = thread;
}
public void run() {
if (null != thread) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
method();
}
private void method() {
synchronized (SynStatement.class) {
for (int i = 0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "--count = " + count++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//测试方法
public static void main(String[] args) {
Thread prev = Thread.currentThread();
System.out.println(prev.getName());
for (int i = 0; i < 3; i++) {
//每个线程持有前一个线程的引用,等到前一个线程执行在继续执行
Thread thread = new Thread(new SynStatement(prev));
thread.start();
prev = thread;
}
}
//打印信息
> Task :Sai.main()
main
Thread-0--count = 0
Thread-0--count = 1
Thread-0--count = 2
Thread-0--count = 3
Thread-1--count = 4
Thread-1--count = 5
Thread-1--count = 6
Thread-1--count = 7
Thread-2--count = 8
Thread-2--count = 9
Thread-2--count = 10
2.可以发现修饰整个类SynStatement.class
,跟修饰类中的静态方法效果是一样的,获得锁是class
,因为class
只有一份,也就是说对所有类的实例对象同一把锁。那么假设,在SynStatement
中增加其他两个方法一个普通的同步方法,锁的粒度分别为调用对象
与SynStatement.class
,是否会存在竞争呢?修改SynStatement
的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79import java.util.concurrent.TimeUnit;
public class SynStatement implements Runnable {
private static int count = 0;
public SynStatement() {
}
public void run() {
method();
}
private void method() {
synchronized (SynStatement.class) {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "--count = " + count++);
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public synchronized void methodTwo() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("enter synchronized methodTwo");
}
public void methodThree() {
synchronized (SynStatement.class) {
try {
System.out.println("enter synchronized methodThree");
TimeUnit.SECONDS.sleep(3);
System.out.println("sleep 3 seconds, exit methodThree");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//测试方法
public static void main(String[] args) {
SynStatement synStatement = new SynStatement();
new Thread(synStatement).start();
synStatement.methodTwo();
synStatement.methodThree();
}
//打印信息
> Task :Sai.main()
Thread-0--count = 0
Thread-0--count = 1
Thread-0--count = 2
Thread-0--count = 3
Thread-0--count = 4
Thread-0--count = 5
Thread-0--count = 6
Thread-0--count = 7
Thread-0--count = 8
Thread-0--count = 9
enter synchronized methodTwo
Thread-0--count = 10
Thread-0--count = 11
Thread-0--count = 12
Thread-0--count = 13
Thread-0--count = 14
Thread-0--count = 15
....中间省略
enter synchronized methodThree
sleep 3 seconds, exit methodThree
3.分析,methodTwo
显然是普通的方法,而其锁的对象是调用者synStatement
,而method
与methodThree
锁的粒度同为SynStatement.class
是存在竞争的,谁先拿到锁谁就会先执行,也就是这个两个方法的打印信息存在先后的顺序关系的。但是普通同步方法methodTwo
则没有,依赖于调用者对象。
Tips
1.synchronized
修饰的普通方法(非静态),也即锁的粒度就是调用的对象。显然多个不同的实例调用是不受影响的。
2.synchronized
修饰的是静态方法,由于静态方法属于类(class),而虚拟机中class
仅有一份,也即锁住了所有的实例对象,者对于锁的粒度为.class
也是同样的效果。
3.对于不同的同步方法但是锁的粒度相同(锁对象相同),显然存在竞争关系的。如上述的methodTwo
与methodThree
之间。而锁对象不同时不存在竞争,使用不受影响。
4.同时同步损害了并发性,实际开发过程中考虑业务需求选择合适的锁粒度。