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

文章目录

  • 写在前面
  • synchronized锁对象
    • synchronized锁固定对象
    • synchronized锁新建对象
    • synchronized锁对象的业务id
      • id已经存在字符串常量池中
      • id不存在字符串常量池中
      • 手动将id存在字符串常量池中
  • 总结

写在前面

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

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

synchronized锁对象

固定测试类

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锁固定对象

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());
    }
}

执行结果:

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锁新建对象

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());
    }
}

执行结果:

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已经存在字符串常量池中

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());
    }
}

执行结果:

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不存在字符串常量池中

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());
    }
}

执行结果:

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存在字符串常量池中

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());
    }
}

执行结果:

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了。

你可能感兴趣的:(java,java,jvm,面试)