跳到正文
hello world

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

因为 thread1thread2 调用的是同一个 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 对象,所以会互斥。

也就是说,哪怕 file1file2 是两个完全不同的 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;
    }
}

这里 numberstatic 的,全类共享。

所以用类锁是合理的。

再比如某个全局缓存初始化:

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 里写了共享成员变量。

10. 对象锁和类锁到底锁的是什么 - Java并发之从Thread到CompletableFuture - 上下文网