Synchronized 用法总结

Synchronized 是 Java 多线程编程中常用关键字。Synchronized 是防止线程干扰和内存一致性的简单策略。

Synchronized 关键字用法

类对象:类名.class 创建的对象。对类对象上锁,可以叫做类锁
实例对象:当前类,通过构造方法创建的实例对象。对实例对象上锁,可以叫做实例锁(对象锁)

Synchronized 是一种同步锁,也是可重入锁。在不同地方使用 Synchronized 都会获取一把锁,但这把锁是谁的呢?

修饰普通方法

锁的是当前实例对象 —— 实例锁

1
2
3
public synchronized void run() {
// some code...
}

此写法等价于

1
2
3
4
5
public void run() {
synchronized(this) {
// some code...
}
}

修饰静态方法

锁的是当前类对象 —— 类锁

1
2
3
public synchronized static void run() {
// some code...
}

此写法等价于

1
2
3
4
5
public void run() {
synchronized(className.class) {
// some code...
}
}

修饰代码块

obj 可以为类锁 (ClassName.class),也可以为实例锁 (new ClassName)

1
2
3
4
5
public void run() {
synchronized(obj) {
// some code...
}
}

不可使用的对象锁

同步块不应该加在 String 或基本包装类型上,如 Byte、Short、Integer、Long、Float、Double、Boolean、Character。
String 不能用作同步块的参数是因为 String 为不可变对象,任何 String 对象的改变都将产生一个新的 String 对象,这也将导致前面加的锁不会被释放。
Integer、Boolean、Double、Long 不能作为同步块参数的原因是他们是基本包装类型,包装类型有特殊的逻辑,用一句话说就是 Java 的自动封箱和解箱操作会导致这些对象在经过运算后不再是原来的对象。
用复杂的话说就是:当把基本变量赋值给包装类型的变量(其实编译过后的操作就是调用包装类型的静态方法 valueOf)或者调用静态 valueOf 方法时:

  • Boolean 返回的是缓存的对象。
  • 整型(Byte,Short,Integer,Long)会检查该数字是否在 1 个字节可表示的有符号整数范围内(-128~127),是则返回缓存对象,否则返回新对象。
  • Character 会缓存整型值为 0~127 的字符,同样会检查字符是否落在缓存范围中,是则返回,否则返回新对象。
  • Double 和 Float 的 valueOf 方法始终返回新对象。

同步代码块异常

当持有锁的线程,发生异常,此时锁会释放。所以需要注意在业务中,如果代码发生异常,是否会出现错误数据的情况。

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

public static void main(String[] args) {
SyncThread syncThread = new SyncThread();
Thread thread1 = new Thread(syncThread, "SyncThread1");
Thread thread2 = new Thread(syncThread, "SyncThread2");
thread1.start();
thread2.start();
}

}


class SyncThread implements Runnable {

int i = 0;

@SneakyThrows
public synchronized void run() {
while (true) {
System.out.println("Thread name : " + Thread.currentThread().getName() + " -> i = " + i);
Thread.sleep(1000);
if (i++ == 3) {
int result = 1 / 0;
}
}
}

}

结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
Thread name : SyncThread1 -> i = 0
Thread name : SyncThread1 -> i = 1
Thread name : SyncThread1 -> i = 2
Thread name : SyncThread1 -> i = 3
Exception in thread "SyncThread1" java.lang.ArithmeticException: / by zero
at cn.liaocp.amireux.base.util.SyncThread.run(SynchronizedTest.java:31)
at java.base/java.lang.Thread.run(Thread.java:829)
Thread name : SyncThread2 -> i = 4
Thread name : SyncThread2 -> i = 5
Thread name : SyncThread2 -> i = 6
Thread name : SyncThread2 -> i = 7
Thread name : SyncThread2 -> i = 8
...

分析结果

按代码逻辑,如果没有 int result = 1 / 0;,线程 2 将永远没有机会执行。但在线程 1 发生异常后,线程 2 执行了,说明 线程 1 发生异常,释放了锁,线程 2 获得了锁,线程 2 执行。

用法总结

  1. 无论 synchronized 关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果 synchronized 作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。

  2. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。

  3. 需注意线程发生异常处理。

参考链接
Java 中 Synchronized 的用法
为什么 Synchronized 不能加在 String 和 Integer 等基本包装类型上