深入理解synchronized

深入理解synchronized

synchronized介绍

synchronized是java的关键字,用于修饰方法、代码块,能够保证同一时刻最多只有一个线程执行这段代码,通过上述描述可以清晰告诉我们synchronized的作用,也就是同步操作,同步是围绕称为内部锁监视器锁的内部实体构建的。内部锁在同步的两个方面都发挥作用:强制对对象状态进行独占访问,并建立对可见性至关重要的先发生关系。

基本用法

代码段

在方法内部使用synchronized来进行代码块的同步操作。

1
2
3
4
5
6
7
public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}

方法

直接在方法上使用synchronized进行修饰即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SynchronizedCounter {
private int c = 0;

public synchronized void increment() {
c++;
}

public synchronized void decrement() {
c--;
}

public synchronized int value() {
return c;
}
}

当线程调用synchronized方法时,它会自动获取该方法对象的内部锁,并在方法返回时释放它。即使返回是由未捕获的异常引起的,也会发生锁定释放,可能想知道在调用静态同步方法时会发生什么,因为静态方法与类关联,而不是与对象关联。在这种情况下,线程获取Class与类关联的对象的内部锁。因此,对类的静态字段的访问由与该类的任何实例的锁不同的锁控制。

内部锁或监视器锁

同步是围绕称为内部锁监视器锁的内部实体构建的。内部锁在同步的两个方面都发挥作用:强制对对象状态进行独占访问,并建立对可见性至关重要的先发生关系。每个对象都有一个与之关联的内在锁,按照惯例,需要对对象字段进行独占和一致访问的线程必须在访问对象之前获取对象的内部锁,然后在完成它们时释放内部锁。据说一个线程在获得锁定和释放锁定之间拥有内在锁定。只要一个线程拥有一个内部锁,没有其他线程可以获得相同的锁。另一个线程在尝试获取锁时将阻塞。当线程释放内部锁时,在该操作与同一锁的任何后续获取之间建立先发生关系。

  1. 当使用synchronized修饰非静态方法是,内置锁就是对象本身(this)

  2. 当使用synchronized修饰静态方法是,内置锁就是该方法的所在的类对象的内置锁

通过一张图来描述一下synchronized的运行过程:

Synchronized运行

  1. 当线程1获取到拥有一个内部锁,没有其他线程可以获得相同的锁。

  2. 线程2只能等到线程1释放内部锁,线程2处于Blocked阻塞状态,被阻塞的线程将等待。

  3. 当线程1方法执行完释放内部锁之后,线程2获取内部锁,执行相应的方法。

内部锁可重入

重入性指的是同一线程中,线程不需要再次获取同一把锁,内部锁是基于每个线程而不是基于每个方法的调用获取的。一旦线程获得了锁,它就可以在内部调用其他方法而无需重新获取锁。只有在使用entry方法调用完成线程时才会释放Lock。

下面结合例子进行分析锁的重入性:

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
public class ReentrantDemo {
public static void main (String[] args) throws InterruptedException {
ReentrantDemo demo = new ReentrantDemo();
Thread thread1 = new Thread(() -> {
System.out.println("线程1调用前 "+ LocalDateTime.now());
demo.syncMethod1("执行线程1的方法");
System.out.println("线程1调用后 "+LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("线程2调用前 "+LocalDateTime.now());
demo.syncMethod2("执行线程2的方法");
System.out.println("线程2调用后 "+LocalDateTime.now());
});

thread1.start();
thread2.start();
}
private synchronized void syncMethod1 (String msg) {
System.out.println("进入同步方法1syncMethod1 "+msg+" "+LocalDateTime.now());
syncMethod2("重入同步方法syncMethod2");
}
private synchronized void syncMethod2 (String msg) {
System.out.println("进入到同步方法2syncMethod2 "+msg+" "+LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

运行结果:

1
2
3
4
5
6
线程2调用前 2019-02-22T15:33:43.963
进入同步方法1syncMethod1 执行线程1的方法 2019-02-22T15:33:43.964
进入到同步方法2syncMethod2 重入同步方法syncMethod2 2019-02-22T15:33:43.964
线程1调用后 2019-02-22T15:33:46.969
进入到同步方法2syncMethod2 执行线程2的方法 2019-02-22T15:33:46.969
线程2调用后 2019-02-22T15:33:49.973

代码解析:

  1. syncMethod1syncMethod2都用synchronized进行修饰。
  2. syncMethod1调用了syncMethod2方法,这时候会产生重入的问题
  3. 线程1当调用syncMethod1时,获取当前对象的内部锁
  4. 线程1调用syncMethod2时,发现当前线程拥有当前对象的内部锁,直接重入syncMethod2
  5. 每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一

核心原理

我们当使用synchronized修饰代码块或者方法时内部实现了什么操作?我们通过下面例子来进行揭露真面目

实例代码:

1
2
3
4
5
6
7
8
9
10
public class Test {
private synchronized void syncMethod() {
System.out.println("testing");
}

public static void main(String[] args) {
Test test = new Test();
test.syncMethod();
}
}

通过javap -v Test.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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
  public com.example.demo.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/demo/Test;
public synchronized void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String testing
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/example/demo/Test;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #5 // class com/example/demo/Test
3: dup
4: invokespecial #6 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #7 // Method syncMethod:()V
12: return
LineNumberTable:
line 10: 0
line 11: 8
line 12: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 args [Ljava/lang/String;
8 5 1 test Lcom/example/demo/Test;
MethodParameters:
Name Flags
args
}
  1. syncMethod的synchronized修饰在方法上
  2. syncMethod的flag中存在ACC_SYNCHRONIZED进行修饰,标识是否为synchronized

实例二:

通过synchronized方法修饰代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author battleheart
*/
public class Test {

public static void syncMethod() {
System.out.println("testing");
}

public static void main(String[] args) throws Exception {

synchronized (Test.class) {
syncMethod();
}
}
}

通过javap -v Test.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
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
{
public com.example.demo.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/demo/Test;

public static void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String testing
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8

public static void main(java.lang.String[]) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/example/demo/Test
2: dup
3: astore_1
4: monitorenter
5: invokestatic #6 // Method syncMethod:()V
8: aload_1
9: monitorexit
10: goto 18
13: astore_2
14: aload_1
15: monitorexit
16: aload_2
17: athrow
18: return
Exception table:
from to target type
5 10 13 any
13 16 13 any
LineNumberTable:
line 11: 0
line 12: 5
line 13: 8
line 14: 18
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 13
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
Exceptions:
throws java.lang.Exception
MethodParameters:
Name Flags
args
}

  1. 执行同步代码块首先执行monitorenter指令,退出时候执行monitorexit指令
  2. 同步时必须要获取对象的监视器monitor,获取monitor后才能执行下面逻辑,否则只能等待。
  3. 经过monitorenter指令和monitorexit指令修饰的部分代码是互斥的,仅有一个线程持有内部锁。
文章目录
  1. 1. 深入理解synchronized
    1. 1.1. synchronized介绍
    2. 1.2. 基本用法
      1. 1.2.1. 代码段
      2. 1.2.2. 方法
    3. 1.3. 内部锁或监视器锁
    4. 1.4. 内部锁可重入
    5. 1.5. 核心原理