10. 对象锁和类锁到底锁的是什么
发布于 • 阅读量 1
上一节用了 synchronized 解决 count++ 的线程安全问题。
但 synchronized 还有一个容易绕的点:它到底锁的是什么?
比如下面几种写法:
public synchronized void method() {
}
public static synchronized void method() {
}
synchronized (this) {
}
synchronized (Demo.class) {
}
它们看起来都在加锁,但锁的对象不一样。
如果这个地方没理解清楚,后面在 Spring Boot 里看单例 Bean、多线程请求时,很容易混。
所以这一节我就专门整理一下:对象锁和类锁到底锁的是什么。
synchronized 一定要依赖一个锁对象
我现在理解 synchronized 时,会先记住一句话:
synchronized 不是凭空加锁,它一定要依赖某一个对象作为锁。
也就是说,线程之间能不能互斥,关键不是看代码里有没有 synchronized 这个单词,而是看它们竞争的是不是同一把锁。
如果多个线程抢的是同一把锁,那就会互斥。
如果多个线程抢的不是同一把锁,那就各锁各的,互不影响。
这个点很关键。
普通 synchronized 方法锁的是 this
先看普通方法上的 synchronized。
新建类:
com.succos.thread.ObjectLockDemo
代码如下:
package com.succos.thread;
public class ObjectLockDemo {
public synchronized void methodA() {
System.out.println(Thread.currentThread().getName() + " 进入 methodA");
sleep(3000);
System.out.println(Thread.currentThread().getName() + " 离开 methodA");
}
public synchronized void methodB() {
System.out.println(Thread.currentThread().getName() + " 进入 methodB");
sleep(3000);
System.out.println(Thread.currentThread().getName() + " 离开 methodB");
}
public static void main(String[] args) {
ObjectLockDemo demo = new ObjectLockDemo();
Thread thread1 = new Thread(() -> {
demo.methodA();
}, "thread-1");
Thread thread2 = new Thread(() -> {
demo.methodB();
}, "thread-2");
thread1.start();
thread2.start();
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
这里 methodA() 和 methodB() 都是普通的 synchronized 方法。
它们锁的都是当前对象,也就是:
this
因为 thread1 和 thread2 调用的是同一个 demo 对象:
ObjectLockDemo demo = new ObjectLockDemo();
所以它们竞争的是同一把对象锁。
运行时大概会看到:
thread-1 进入 methodA
等待 3 秒...
thread-1 离开 methodA
thread-2 进入 methodB
等待 3 秒...
thread-2 离开 methodB
当然,也可能是 thread-2 先进入。
但重点是:两个方法不会同时执行。
因为它们锁的是同一个对象。
synchronized 方法等价于 synchronized(this)
普通同步方法:
public synchronized void methodA() {
// 业务代码
}
大致可以理解成:
public void methodA() {
synchronized (this) {
// 业务代码
}
}
所以普通 synchronized 方法锁的是当前实例对象。
也就是说:
同一个对象上的多个 synchronized 普通方法,会互相阻塞。
这就是对象锁。
对象锁不是锁类,也不是锁方法名,而是锁具体的对象实例。
两个不同对象不会互相阻塞
再看一个例子。
如果我创建两个对象:
ObjectLockDemo demo1 = new ObjectLockDemo();
ObjectLockDemo demo2 = new ObjectLockDemo();
然后两个线程分别调用不同对象的方法:
Thread thread1 = new Thread(() -> {
demo1.methodA();
}, "thread-1");
Thread thread2 = new Thread(() -> {
demo2.methodA();
}, "thread-2");
完整代码可以这样写:
package com.succos.thread;
public class ObjectLockTwoObjectDemo {
public synchronized void methodA() {
System.out.println(Thread.currentThread().getName() + " 进入 methodA");
sleep(3000);
System.out.println(Thread.currentThread().getName() + " 离开 methodA");
}
public static void main(String[] args) {
ObjectLockTwoObjectDemo demo1 = new ObjectLockTwoObjectDemo();
ObjectLockTwoObjectDemo demo2 = new ObjectLockTwoObjectDemo();
Thread thread1 = new Thread(() -> {
demo1.methodA();
}, "thread-1");
Thread thread2 = new Thread(() -> {
demo2.methodA();
}, "thread-2");
thread1.start();
thread2.start();
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
这时两个线程大概率会同时进入:
thread-1 进入 methodA
thread-2 进入 methodA
等待 3 秒...
thread-1 离开 methodA
thread-2 离开 methodA
原因是它们锁的不是同一个对象。
thread-1 锁的是 demo1。
thread-2 锁的是 demo2。
所以它们互不影响。
这也说明,synchronized 方法不是“只要方法名一样就互斥”。
真正看的还是锁对象是不是同一个。
static synchronized 方法锁的是 Class 对象
再看静态同步方法。
新建类:
com.succos.thread.ClassLockDemo
代码如下:
package com.succos.thread;
public class ClassLockDemo {
public static synchronized void methodA() {
System.out.println(Thread.currentThread().getName() + " 进入 static methodA");
sleep(3000);
System.out.println(Thread.currentThread().getName() + " 离开 static methodA");
}
public static synchronized void methodB() {
System.out.println(Thread.currentThread().getName() + " 进入 static methodB");
sleep(3000);
System.out.println(Thread.currentThread().getName() + " 离开 static methodB");
}
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
ClassLockDemo.methodA();
}, "thread-1");
Thread thread2 = new Thread(() -> {
ClassLockDemo.methodB();
}, "thread-2");
thread1.start();
thread2.start();
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
这里两个方法都是:
static synchronized
静态同步方法锁的不是 this。
因为静态方法属于类,不属于某一个具体对象。
它锁的是这个类对应的 Class 对象,也就是:
ClassLockDemo.class
所以:
public static synchronized void methodA() {
}
大致可以理解成:
public static void methodA() {
synchronized (ClassLockDemo.class) {
// 业务代码
}
}
这就是类锁。
类锁和对象锁不是同一把锁
这里有个细节也要注意。
对象锁和类锁不是同一把锁。
比如:
public synchronized void instanceMethod() {
}
public static synchronized void staticMethod() {
}
第一个锁的是:
this
第二个锁的是:
当前类.class
它们不是同一个对象,所以不会互相阻塞。
可以写个例子:
package com.succos.thread;
public class ObjectLockAndClassLockDemo {
public synchronized void instanceMethod() {
System.out.println(Thread.currentThread().getName() + " 进入 instanceMethod");
sleep(3000);
System.out.println(Thread.currentThread().getName() + " 离开 instanceMethod");
}
public static synchronized void staticMethod() {
System.out.println(Thread.currentThread().getName() + " 进入 staticMethod");
sleep(3000);
System.out.println(Thread.currentThread().getName() + " 离开 staticMethod");
}
public static void main(String[] args) {
ObjectLockAndClassLockDemo demo = new ObjectLockAndClassLockDemo();
Thread thread1 = new Thread(() -> {
demo.instanceMethod();
}, "thread-1");
Thread thread2 = new Thread(() -> {
ObjectLockAndClassLockDemo.staticMethod();
}, "thread-2");
thread1.start();
thread2.start();
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
运行后,两个方法可能同时进入:
thread-1 进入 instanceMethod
thread-2 进入 staticMethod
因为:
instanceMethod 锁的是 demo 这个对象;
staticMethod 锁的是 ObjectLockAndClassLockDemo.class。
它们是两把不同的锁。
synchronized(this) 和 synchronized(XXX.class)
如果不写在方法上,也可以显式写锁对象。
对象锁一般这样写:
synchronized (this) {
// 需要保护的代码
}
类锁一般这样写:
synchronized (ObjectLockDemo.class) {
// 需要保护的代码
}
我现在看这种代码时,会直接问自己:
这里锁的是哪个对象?
其他线程是不是也会抢这同一个对象?
如果答案是“是”,那就能起到互斥作用。
如果答案是“不是”,那锁可能没有意义。
在 PDF 项目里怎么理解对象锁
假设我有一个普通类:
public class PdfWatermarkService {
public synchronized void addWatermark(FileItemContext fileItemContext) {
// 处理 PDF
}
}
如果我这样使用:
PdfWatermarkService service = new PdfWatermarkService();
Thread thread1 = new Thread(() -> {
service.addWatermark(file1);
});
Thread thread2 = new Thread(() -> {
service.addWatermark(file2);
});
因为两个线程用的是同一个 service 对象,所以会互斥。
也就是说,哪怕 file1 和 file2 是两个完全不同的 PDF,它们也只能一个一个处理。
这可能不是我想要的。
因为 PDF 文件之间本来互不影响,我希望它们可以并发处理。
如果我把整个 addWatermark() 方法都加上 synchronized,就可能把并发处理变成串行处理。
所以在 PDF 水印这种场景里,我一般不会轻易给整个处理方法加 synchronized。
除非里面确实访问了某个共享资源,比如共享计数器、共享缓存、共享临时文件名生成器。
Spring Boot 单例 Bean 里更要注意对象锁
在 Spring Boot 里,@Service 默认是单例。
也就是说,一个 PdfWatermarkService 在容器里通常只有一个对象。
如果我写成:
@Service
public class PdfWatermarkService {
public synchronized void addWatermark(FileItemContext fileItemContext) {
// 处理 PDF
}
}
那么多个请求同时调用这个方法时,都会竞争同一个 PdfWatermarkService 对象锁。
结果就是:
请求1 进来处理 a.pdf;
请求2 进来处理 b.pdf,但要等请求1处理完;
请求3 也只能继续等。
这可能会让接口吞吐量明显下降。
所以不要一看到多线程就给方法加 synchronized。
要先判断:
这个方法里面到底有没有共享可变状态?
是不是必须串行执行?
锁住整个方法会不会影响并发能力?
如果只是方法内部的局部变量,通常不需要锁。
对象锁适合什么场景
对象锁适合保护某个对象自己的状态。
比如一个账户对象:
public class Account {
private int balance;
public synchronized void withdraw(int amount) {
balance = balance - amount;
}
}
这里锁当前账户对象是合理的。
因为 balance 属于这个账户对象,多个线程同时修改同一个账户余额时,确实应该互斥。
再比如一个任务对象维护自己的状态:
public class TaskState {
private String status;
public synchronized void updateStatus(String status) {
this.status = status;
}
}
如果多个线程可能同时修改同一个任务状态,也可以考虑对象锁。
简单说,对象锁适合这种情况:
我要保护的是某一个具体对象内部的数据。
类锁适合什么场景
类锁适合保护全局共享资源。
比如全局序列号:
public class OrderNoGenerator {
private static int number = 0;
public static synchronized int next() {
return ++number;
}
}
这里 number 是 static 的,全类共享。
所以用类锁是合理的。
再比如某个全局缓存初始化:
public class GlobalCache {
private static boolean initialized = false;
public static synchronized void init() {
if (!initialized) {
// 初始化全局缓存
initialized = true;
}
}
}
这种场景不是某一个对象自己的状态,而是整个类级别的共享状态。
所以用类锁。
简单说,类锁适合这种情况:
我要保护的是 static 级别、全局级别的数据。
对象锁和类锁的区别
我现在会这样区分:
对象锁:锁某一个具体对象,比如 this;
类锁:锁某一个类的 Class 对象,比如 Demo.class。
再具体一点:
普通 synchronized 方法:锁 this;
static synchronized 方法:锁 当前类.class;
synchronized(this):锁当前对象;
synchronized(Demo.class):锁 Demo 这个类的 Class 对象。
这个表可以记一下:
| 写法 | 锁对象 |
|---|---|
public synchronized void method() |
当前对象 this |
public static synchronized void method() |
当前类的 Class 对象 |
synchronized (this) |
当前对象 this |
synchronized (Demo.class) |
Demo.class |
synchronized (LOCK) |
自己指定的 LOCK 对象 |
我的判断方式
以后看到 synchronized,我不想只看关键字。
我会先问三个问题:
1. 这把锁锁的是哪个对象?
2. 多个线程是不是抢同一把锁?
3. 这段代码是不是真的需要串行执行?
如果多个线程不是抢同一把锁,那加了也不一定有用。
如果这段代码根本没有共享修改,那可能不需要加锁。
如果锁的范围太大,还可能把本来可以并发的业务变成串行。
所以 synchronized 不是不能用,而是要知道自己锁住了什么。
这一节小结
这一节我主要记住几点:
1. synchronized 一定依赖某个对象作为锁;
2. 普通 synchronized 方法锁的是 this;
3. static synchronized 方法锁的是 当前类.class;
4. 对象锁和类锁不是同一把锁;
5. 多个线程只有竞争同一把锁时,才会互斥;
6. PDF 独立文件处理不要随便锁整个方法,否则可能把并发变成串行。
用一句话总结:
看 synchronized,关键不是看有没有加锁,而是看大家抢的是不是同一把锁。
下一节继续看 Spring Boot 单例 Bean。
因为在实际项目里,很多并发问题不是出在线程本身,而是出在单例 Service 里写了共享成员变量。