【精选】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了。

相关推荐
Moment13 分钟前
Node.js v25.0.0 发布——性能、Web 标准与安全性全面升级 🚀🚀🚀
前端·javascript·后端
IT_陈寒27 分钟前
Vite 3.0 性能优化实战:5个技巧让你的构建速度提升200% 🚀
前端·人工智能·后端
程序新视界1 小时前
MySQL的整体架构及功能详解
数据库·后端·mysql
绝无仅有1 小时前
猿辅导Java面试真实经历与深度总结(二)
后端·面试·github
绝无仅有1 小时前
猿辅导Java面试真实经历与深度总结(一)
后端·面试·github
Victor3562 小时前
Redis(76)Redis作为缓存的常见使用场景有哪些?
后端
Victor3562 小时前
Redis(77)Redis缓存的优点和缺点是什么?
后端
摇滚侠5 小时前
Spring Boot 3零基础教程,WEB 开发 静态资源默认配置 笔记27
spring boot·笔记·后端
天若有情6737 小时前
Java Swing 实战:从零打造经典黄金矿工游戏
java·后端·游戏·黄金矿工·swin
一只叫煤球的猫8 小时前
建了索引还是慢?索引失效原因有哪些?这10个坑你踩了几个
后端·mysql·性能优化