【精选】synchronized对象锁?如何用synchronized锁字符串对象,这里面的坑要注意

文章目录

写在前面

我们使用synchronized通常都有这样一个误区:synchronized锁住的代码块一定是所有线程都互斥的。

其实不然!

首先我们明确一点,synchronized锁住的是一个对象!如果锁住的这个对象,在多个线程中相同,那么这些线程访问synchronized修饰的代码块时,总是互斥的。

但是!如果锁住的这个对象,在多个线程中是不同的,那么这些线程访问synchronized修饰的代码块,是不会互斥的!

synchronized锁对象

固定测试类

java 复制代码
class T1{
    public T1(String id) {
        this.id = id;
    }

    private String id;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

synchronized锁固定对象

java 复制代码
public class SynchronzedStringTest {

    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() ->{
            try {
                m1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        m1();

        Thread.sleep(10000);
    }

    /**
     * 加锁
     */
    public static void m1() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "进来了" + new Date());
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + "获取到锁" + new Date());
            Thread.sleep(2000);
        }
        System.out.println(Thread.currentThread().getName() + "结束了" + new Date());
    }
}

执行结果:

java 复制代码
main进来了Mon Aug 15 10:19:47 CST 2022
main获取到锁Mon Aug 15 10:19:47 CST 2022
Thread-0进来了Mon Aug 15 10:19:47 CST 2022
Thread-0获取到锁Mon Aug 15 10:19:49 CST 2022
main结束了Mon Aug 15 10:19:49 CST 2022
Thread-0结束了Mon Aug 15 10:19:51 CST 2022

我们可以看到,Thread-0线程和main线程,执行synchronized代码块是互斥的。

但是,这种加锁方式太笨重,每一个线程执行到synchronized代码块都会互斥。

synchronized锁新建对象

java 复制代码
public class SynchronzedStringTest {

    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1("t1");
        T1 t2 = new T1("t1");
        new Thread(() ->{
            try {
                m1(t1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        m1(t2);

        Thread.sleep(10000);
    }

    /**
     * 加锁
     */
    public static void m1(T1 t1) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "进来了" + new Date());
        synchronized (t1) {
            System.out.println(Thread.currentThread().getName() + "获取到锁" + new Date());
            Thread.sleep(2000);
        }
        System.out.println(Thread.currentThread().getName() + "结束了" + new Date());
    }
}

执行结果:

java 复制代码
main进来了Mon Aug 15 10:45:40 CST 2022
main获取到锁Mon Aug 15 10:45:40 CST 2022
Thread-0进来了Mon Aug 15 10:45:40 CST 2022
Thread-0获取到锁Mon Aug 15 10:45:40 CST 2022
main结束了Mon Aug 15 10:45:42 CST 2022
Thread-0结束了Mon Aug 15 10:45:42 CST 2022

我们可以看到,Thread-0线程和main线程,执行synchronized代码块并不互斥。

因为两个线程传参的T1其实是两个不同的对象(虽然对象中的id值是一样的),所以synchronized虽然加锁了,但是由于两个对象是不一样的,所以两个线程并不会互斥。

synchronized锁对象的业务id

id已经存在字符串常量池中

java 复制代码
public class SynchronzedStringTest {

    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1("t1");
        T1 t2 = new T1("t1");
        new Thread(() ->{
            try {
                m1(t1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        m1(t2);

        Thread.sleep(10000);
    }

    /**
     * 加锁
     */
    public static void m1(T1 t1) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "进来了" + new Date());
        synchronized (t1.getId()) {
            System.out.println(Thread.currentThread().getName() + "获取到锁" + new Date());
            Thread.sleep(2000);
        }
        System.out.println(Thread.currentThread().getName() + "结束了" + new Date());
    }
}

执行结果:

java 复制代码
Thread-0进来了Mon Aug 15 10:48:11 CST 2022
Thread-0获取到锁Mon Aug 15 10:48:11 CST 2022
main进来了Mon Aug 15 10:48:11 CST 2022
main获取到锁Mon Aug 15 10:48:13 CST 2022
Thread-0结束了Mon Aug 15 10:48:13 CST 2022
main结束了Mon Aug 15 10:48:15 CST 2022

我们可以发现,Thread-0线程和main线程,执行synchronized代码块是互斥的。

因为synchronized锁住的字符串,其实是已经定义在字符串常量池中的。

其中t1和t2中显式定义的id,已经存在了字符串常量池中,而t1和t2的id,在字符串常量池中的指向是一致的,所以synchronized锁住的字符串被认为是同一个对象。

关于字符串常量池的说法请移步String的Intern()方法,详解字符串常量池!

id不存在字符串常量池中

java 复制代码
public class SynchronzedStringTest {

    public static void main(String[] args) throws InterruptedException {
        String s1 = "a";
        String s2 = "b";
        T1 t1 = new T1(s1 + s2);
        T1 t2 = new T1(s1 + s2);
        new Thread(() ->{
            try {
                m1(t1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        m1(t2);

        Thread.sleep(10000);
    }

    /**
     * 加锁
     */
    public static void m1(T1 t1) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "进来了" + new Date());
        synchronized (t1.getId()) {
            System.out.println(Thread.currentThread().getName() + "获取到锁" + new Date());
            Thread.sleep(2000);
        }
        System.out.println(Thread.currentThread().getName() + "结束了" + new Date());
    }
}

执行结果:

java 复制代码
Thread-0进来了Mon Aug 15 10:52:41 CST 2022
Thread-0获取到锁Mon Aug 15 10:52:41 CST 2022
main进来了Mon Aug 15 10:52:41 CST 2022
main获取到锁Mon Aug 15 10:52:41 CST 2022
Thread-0结束了Mon Aug 15 10:52:43 CST 2022
main结束了Mon Aug 15 10:52:43 CST 2022

我们可以发现,Thread-0线程和main线程,执行synchronized代码块不是互斥的。

这是因为虽然t1和t2的id都是一样的,但是字符串a+b 最终的结果实际是new 的新的String,并不会存在字符串常量池中,所以synchronized认为其锁住的字符串其实并不是同一个对象。

手动将id存在字符串常量池中

java 复制代码
public class SynchronzedStringTest {

    public static void main(String[] args) throws InterruptedException {
        String s1 = "a";
        String s2 = "b";
        T1 t1 = new T1(s1 + s2);
        T1 t2 = new T1(s1 + s2);
        new Thread(() ->{
            try {
                m1(t1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        m1(t2);

        Thread.sleep(10000);
    }

    /**
     * 加锁
     */
    public static void m1(T1 t1) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "进来了" + new Date());
        synchronized (t1.getId().intern()) {
            System.out.println(Thread.currentThread().getName() + "获取到锁" + new Date());
            Thread.sleep(2000);
        }
        System.out.println(Thread.currentThread().getName() + "结束了" + new Date());
    }
}

执行结果:

java 复制代码
Thread-0进来了Mon Aug 15 10:58:48 CST 2022
Thread-0获取到锁Mon Aug 15 10:58:48 CST 2022
main进来了Mon Aug 15 10:58:48 CST 2022
Thread-0结束了Mon Aug 15 10:58:50 CST 2022
main获取到锁Mon Aug 15 10:58:50 CST 2022
main结束了Mon Aug 15 10:58:52 CST 2022

我们可以发现,Thread-0线程和main线程,执行synchronized代码块是互斥的。

因为我们调用了.intern()方法,将字符串统一放到字符串常量池中管理,相同的字符串代表的对象地址是一样的。

总结

synchronized是一个对象锁,在单机环境下,我们最好不要使用固定的对象进行加锁,使用业务id对指定业务进行加锁可以提高很多并发量。

集群环境下还是不要使用synchronized了。

相关推荐
鱼弦2 小时前
Agent智能体 vs 传统运维:职业天花板的3倍差距?
后端
暗夜行者之光2 小时前
LangGraph 实战:用 LangSmith 可视化追踪 AI 智能体执行轨迹
后端
鱼弦2 小时前
Agent 的工具选择策略:从硬编码到动态决策
后端
Lost of 程序猿2 小时前
建造者模式实战:告别“十参数构造函数“的数据导出任务
后端·设计模式·c#·asp.net
SensorFlow2 小时前
ClickHouse 埋点事件表:先验证时间与重复,再谈漏斗看板
后端
Dreams_l3 小时前
基于SpringBoot实现的抽奖系统(一)
java·spring boot·后端
余槐i3 小时前
rollbackFor 不设置,Checked 异常不回滚:Spring 默认规则与两行修复的取舍
java·spring boot·后端·spring·事务管理
孙启超4 小时前
【AI开发之Rust】第 17 课:项目总览与核心架构 —— AI 助手 Rust 核心从 0 到 1
开发语言·后端·rust
EatFan4 小时前
JunoYi 框架实践:Spring Boot 项目为什么拆成 framework、module、server 三层?
java·spring boot·后端·framework·module·模块化·junoyi
汉堡大王95274 小时前
一张图三句需求,我用 Trae Work 做了一块能看日出日落和月相的天文机械表
前端·后端·github