Java 的 synchronize 锁对象竞争

synchronized 的三种使用方式

  • synchronized 修饰普通方法时,是实例锁(对象锁),锁住的是当前实例对象
  • synchronized 修饰静态方法时,是类锁,锁住的是当前类的 Class 对象(反射)
  • synchronized 同步代码块,锁住的是 synchronized 后面紧接着的小括号内的对象

特别注意

  • 无论如何使用 synchronized 进行加锁,都是控制某个方法或代码块的访问,而不是控制整个类。
  • synchronized 修饰普通方法与同步代码块 synchronized(this){} 的效果是一样的,锁住的都是当前实例对象。
  • synchronized 修饰静态方法与同步代码块 synchronized(Object.class){} 的效果是一样的,锁住的都是当前类的 Class 对象。

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
public class Test {

public synchronized void func1() {
System.out.println("func1");
try {
Thread.sleep(10000);
System.out.println("sleep time over");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public synchronized void func2() {
System.out.println("func2");
}

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

Thread t1 = new Thread(() -> {
test.func1();
}, "t1");

Thread t2 = new Thread(() -> {
test.func2();
}, "t2");

t1.start();
t2.start();
}

}

程序执行结果:

1
2
3
func1
sleep time over
func2

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
public class Test {

public synchronized static void func1() {
System.out.println("func1");
try {
Thread.sleep(10000);
System.out.println("sleep time over");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public synchronized static void func2() {
System.out.println("func2");
}

public static void main(String[] args) {
Test test1 = new Test();
Test test2 = new Test();

Thread t1 = new Thread(() -> {
test1.func1();
}, "t1");

Thread t2 = new Thread(() -> {
test2.func2();
}, "t2");

t1.start();
t2.start();
}

}

程序执行结果:

1
2
3
func1
sleep time over
func2

synchronized 同步代码块

同步代码块,锁住的是 synchronized 后面紧接着的小括号内的对象。对于 synchronized(obj) 而言,其中的 obj 称之为同步监视器,是一个 Java 对象。

  • 同步监视器的使用规则

    • 同步监视器必须是引用数据类型,不能是基本数据类型。
    • 可以改变同步监视器堆中属性的值,但是不能改变指向堆中的地址。
    • 尽量不要用 String 和包装类型作为同步监视器,容易造成指向堆中地址的变化。
    • 一般使用共享资源作为同步监视器,也可以专门创建一个没有任何语义化含义的同步监视器。
    • 同步方法中无需同步监视器,因为同步方法的同步监视器就是 this,即对象本身,或者是 Class 对象(反射)。
  • 同步监视器的执行过程

    • 线程 1 访问,锁定同步监视器,执行代码。
    • 线程 2 访问,同步监视器已被锁定,无法执行代码块,阻塞等待。
    • 线程 1 访问完毕,解锁同步监视器。
    • 线程 2 访问,同步监视器未被锁定,线程 2 锁定同步监视器,执行代码。
    • 线程 2 访问完毕,解锁同步监视器。
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
public class Test {

public void func1() {
synchronized (this) {
System.out.println("func1");
try {
Thread.sleep(10000);
System.out.println("sleep time over");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public void func2() {
synchronized (Test.class) {
System.out.println("func2");
}
}

public static void main(String[] args) {
Test test1 = new Test();
Test test2 = new Test();

Thread t1 = new Thread(() -> {
test1.func1();
}, "t1");

Thread t2 = new Thread(() -> {
test2.func2();
}, "t2");

t1.start();
t2.start();
}

}

程序执行结果:

1
2
3
func1
func2
sleep time over

参考资料