Fork me on GitHub

Synchronized关键字

1. Synchronized 锁的是什么?

我们都知道 Synchronized 是锁,而且是重量级锁,它保证原子性和可见性,但是不保证有序性。那么它到底锁的是什么?

答案很简单:锁的是对象,对象分为两种,一种是实例对象,一种是类的 Class 对象。有的人习惯把锁实例对象的称为对象锁,锁 Class 对象的称为类锁。其实类锁也是锁的对象,一种特殊的对象,类的 Class 对象。

  • 对象锁
    • synchronized(this|object)
    • 修饰非静态方法
  • 类锁
    • synchronized(类.class)
    • 修饰静态方法

1.1 synchronized (this)

下面我们测试对象锁的 synchronized (this) 。

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
public class SynchronizedThisDemo implements Runnable {

@Override
public void run() {
synchronized (this) {
System.out.println(Thread.currentThread().getName()
+ " enter ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " exist ");
}
}

public static void main(String[] args) throws InterruptedException {
testSynchronizedThis();
}

/**
* 测试 synchronized (this)
*/
private static void testSynchronizedThis() throws InterruptedException {
SynchronizedThisDemo synchronizedThisDemo = new SynchronizedThisDemo();
Thread ta = new Thread(new SynchronizedThisDemo(), "A");
Thread tb = new Thread(new SynchronizedThisDemo(), "B");
ta.start();
tb.start();
ta.join();
tb.join();
Thread tc = new Thread(synchronizedThisDemo, "C");
Thread td = new Thread(synchronizedThisDemo, "D");
tc.start();
td.start();
}
}

执行结果如下:

A enter
B enter
A exist
B exist
C enter
C exist
D enter
D exist

如结果所示, A、B 线程交替执行。因为 this 锁的是当前实例对象,而 A、B 线程的 SynchronizedThisDemo 对象不同,所以 A 在执行的时候并不能阻塞 B 的执行。

因为线程 C、D 使用的是同一个 SynchronizedThisDemo 对象,所以 D 线程被 C 线程阻塞了。

1.2 synchronized (object)

下面我们测试对象锁的 synchronized (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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class SynchronizedObjectDemo implements Runnable{

/**
* 对象锁
*/
private static final String LOCK = "lock";

@Override
public void run() {
synchronized (LOCK) {
System.out.println(Thread.currentThread().getName()
+ " enter ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " exist ");
}
}


public static void main(String[] args) throws InterruptedException {
testSynchronizedObject();
}

/**
* 测试 synchronized (object)
*/
private static void testSynchronizedObject() throws InterruptedException {
SynchronizedObjectDemo synchronizedObjectDemo = new SynchronizedObjectDemo();
Thread ta = new Thread(new SynchronizedObjectDemo(), "A");
Thread tb = new Thread(new SynchronizedObjectDemo(), "B");
ta.start();
tb.start();
ta.join();
tb.join();
Thread tc = new Thread(synchronizedObjectDemo, "C");
Thread td = new Thread(synchronizedObjectDemo, "D");
tc.start();
td.start();
}

}

执行结果:

A enter
A exist
B enter
B exist
C enter
C exist
D enter
D exist

因为从始至终加锁的对象只有一个——LOCK,所以当第一个线程 A 进来的时候就对 LOCK 进行加锁,后面的线程再进来就会被阻塞,直到 A 执行完毕释放锁。然后 B 加锁,释放锁,依次类推。

如果我们把 LOCK 对象放到 run 方法里面会怎样?如下面代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void run() {
Object LOCK = new Object();
synchronized (LOCK) {
System.out.println(Thread.currentThread().getName()
+ " enter ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " exist ");
}
}

执行结果如下:

A enter
B enter
A exist
B exist
C enter
D enter
C exist
D exist

很明显,因为每个线程进 run 方法时,加锁的对象都不一样(每次都新建了一个),所以不会造成阻塞。

1.3 synchronized (类.class)

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
public class SynchronizedClassDemo implements Runnable{

@Override
public void run() {
synchronized (SynchronizedClassDemo.class) {
System.out.println(Thread.currentThread().getName()
+ " enter ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " exist ");
}
}

public static void main(String[] args) throws InterruptedException{
testSynchronizedClass();
}

/**
* 测试 synchronized (class对象)
*/
private static void testSynchronizedClass() throws InterruptedException {
SynchronizedClassDemo synchronizedClassDemo = new SynchronizedClassDemo();
Thread ta = new Thread(new SynchronizedClassDemo(), "A");
Thread tb = new Thread(new SynchronizedClassDemo(), "B");
ta.start();
tb.start();
ta.join();
tb.join();
Thread tc = new Thread(synchronizedClassDemo, "C");
Thread td = new Thread(synchronizedClassDemo, "D");
tc.start();
td.start();
}
}

执行结果:

A enter
A exist
B enter
B exist
C enter
C exist
D enter
D exist

原因很简单,本质是类的 Class 对象有且仅有一个。类似于 synchronized (object) 的第一种情况。

普通方法的执行结果和 synchronized (this) 类似,如果是同一个对象,会串行;如果是多个对象,会交替执行。

静态方法和 synchronized (类.class) 类似,这里不做多余赘述。

有同学在使用过程中出现锁没生效的情况,多半是没搞清楚你加的锁到底锁的是什么。

2. 加锁原理

下面我们来看看,synchronized 加锁的原理是什么。

我们之前说过,synchronized 锁的是对象,无论是实例对象,还是类的 Class 对象。而每一个被加锁的对象都会关联一个 monitor 。monitor 有两个指令,一个是 monitorenter指令,另一个是 monitorexit指令。

monitorenter 指令是编译后插入到同步代码块的起始位置,monitorexit 指令是插入到结束位置或者异常处。

使用 Synchronized 的关键就是获取对象的 monitor,获取之后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到 monitor。执行同步代码块时先执行 monitorenter 指令,退出的时候 monitorexit 指令。

monitor 里面有一个计数器,从 0 开始计数。如果一个线程要获取 monitor 的锁,就看看计数器是不是 0,如果是,就可以获取锁了,然后对计数器加 1 。退出时对计数器减 1 。因为有计数器的存在,synchronized 也是可重入的。

下面我们来看看写的示例代码编译之后是不是有 monitorenter 和 monitorexit 。进入编译的目录,然后使用 javap -v 命令,如下所示:

javap -v SynchronizedClassDemo.class

结果如下,具体看加粗部分。

public void run();

descriptor: ()V

flags: ACC_PUBLIC

Code:

stack=3, locals=4, args_size=1

​ 0: ldc #2 // class com/common/concurrency/synchronize/SynchronizedClassDemo

​ 2: dup

​ 3: astore_1

4:monitorenter

​ 5: iconst_0

​ 6: istore_2

​ 7: iload_2

​ 8: iconst_5

​ 9: if_icmpge 52

​ 12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;

​ 15: new #4 // class java/lang/StringBuilder

​ 18: dup

​ 19: invokespecial #5 // Method java/lang/StringBuilder.”“:()V

​ 22: invokestatic #6 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;

​ 25: invokevirtual #7 // Method java/lang/Thread.getName:()Ljava/lang/String;

​ 28: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

​ 31: ldc #9 // String get synchronized

​ 33: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

​ 36: iload_2

​ 37: invokevirtual #10 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;

​ 40: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

​ 43: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

​ 46: iinc 2, 1

​ 49: goto 7

​ 52: aload_1

53: monitorexit

​ 54: goto 62

​ 57: astore_3

​ 58: aload_1

59: monitorexit

​ 60: aload_3

​ 61: athrow

​ 62: return

Synchronized 关键字再往深了研究,就到硬件级别了,有兴趣的同学可以自行深入研究,推荐 《Java并发编程的艺术》一书。目前来说,知道怎么使用,加锁原理就够了。

参考资料:

《Java并发编程的艺术》

本文标题:Synchronized关键字

原始链接:https://zhaoxiaofa.com/2019/02/17/Synchronized关键字/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。