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

相关推荐
用户83562907805115 分钟前
使用 Python 保护和取消保护 Excel 工作表和工作簿
后端·python
Csvn41 分钟前
Python 开发技巧:函数式编程与 lambda 进阶
后端
考虑考虑1 小时前
nginx配置ssl
运维·后端·nginx
uhakadotcom1 小时前
Python 中,`None` 不等于 `False`导致的问题
后端·面试·github
SamDeepThinking1 小时前
面向对象真正管理的,不是对象,而是依赖
java·后端·程序员
9i编程1 小时前
5.Nacos 配置中心化改造与短信 Debug 模式动态切换实践
后端·spring cloud
PinkSun1 小时前
上线第一天,Redis和Docker联手给我上了一课
后端
清秋冷雨1 小时前
Harness Engineer系列(三):Harness工程化——AI编程的最后一里地
后端
Csvn1 小时前
itertools —— Python 最强迭代器工具箱实战
后端
冰暮流星1 小时前
flask之定义URL
后端·python·flask