关于线上synchronized锁住字符串引起的坑

2022-03-07  本文已影响0人  名字是乱打的

关于synchronized锁的都是对象,以及他的原理,原因相比大家都知道,具体可以看之前写的分析https://www.jianshu.com/p/62b66ab308a7
那么既然可以锁对象,我们这里锁字符串自然也可以,这个没毛病,但是我们学过java的都知道字符串的常量池之说,即使两个字符串值一样,未必两个是同一个对象;
这就引起了一些坑了

一 背景介绍

我们这边有个做金融信息的APP,每天有各种各样的券商(野村,华泰,中信,申宏万源这样的)给我们的邮箱投递pdf,我们需要获取这批pdf并且利用京东科学院的一些AI能力提取解析为一些摘要信息,关键词信息,以及公司涨跌幅这样,那我们肯定不能一个个手动处理,我们这里就搞了个多线程的自动邮件爬取解析任务

这边有个小功能是在获取投递给我们的行业和证券公司名后去系统内判断系统是否行业内是否已存在这个证券公司,如果存在就拿出这个公司的id,如果不存在就去创建,那么这里我同事想的也没毛病,并发向数据库查的时候肯定存在两个线程都读到这个公司栏目不存在的情况,可能会并发创建一样的,于是就在这里把行业+公司名拼接起来了,在这个字符串上加了一个synchronized锁,这样同一个行业下同一个公司名只有一个线程能去查询和创建,这样这里就做到了排队效果;

二 原因分析

上面我们同事想法挺好,但是这里他忽略了可能不是同一个对象的情况,它这里拼接方法伪代码大家可以看一下

 private static String getString() {
        return new StringBuilder().append("行业名").append("公司名").toString();
 }

那么这里其实就会出现问题了,SB类字符串构造器在tostring时候返回的是一个新的字符串对象,尽管他们值是相同的


image

这里其实在测试环境没有测试出来,因为测试环境由于接触不到外网,只是从我们自己的内部邮箱爬爬邮件,特别少,也没有什么并发,但是在生产环境,有大量的券商给我们进行投递,就出现了问题,我就被我们领导派过去查了一下问题,在这里就发现了问题;

三解决方案

其实这里我发现呢就存在一个问题,就是锁的对象尽管值是一样的,但是他们内存地址不一样,那我们就只需要让他们有一样的内存地址即可,这里我就想到了常量池,如果把这些券商都放常量池内,我们锁的就都是常量池的对象了,这就可以保障一个一致性了;

我们可以具体看下intern()方法的jdk解释;


image

When the intern method is invoked, if the pool already contains a
string equal to this {@code String} object as determined by
the {@link #equals(Object)} method, then the string from the pool is
returned. Otherwise, this {@code String} object is added to the
pool and a reference to this {@code String} object is returned.

调用intern方法时,如果池中已经包含一个字符串,该字符串等于equals(object)方法确定的这个字符串对象,则返回池中的字符串。否则,此字符串对象将添加到池中,并返回对此字符串对象的引用

从释义来看,这明显满足我们的需求了,下面就测试了一下,大家可以看看也可以自己拿过去试试;

五测试

我们这里模拟了并发锁同一个值一样的字符串,看看在锁住的时候,别的线程是否能获取到;

5.1测试未加intern()的情况
public class SynchronizedString {
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (getString()) {
                try {
                    System.out.println("已锁->"+getString());
                    Thread.sleep(5000);
                    System.out.println("打印了A,释放->"+getString());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();


        new Thread(() -> {
            synchronized (getString()) {
                System.out.println("打印了B");
            }
        }).start();

        new Thread(() -> {
            synchronized (getString()) {
                System.out.println("打印了C");
            }
        }).start();

        new Thread(() -> {
            synchronized (getString()) {
                System.out.println("打印了D");
            }
        }).start();
    }

    private static String getString() {
        return new StringBuilder("校验锁X").toString().intern();
    }

}

校验结果,在A已经获取到“校验锁X”这个字符串的锁并且持有五秒内,其他线程依然可以继续进入
5.2测试加了intern()的情况
public class SynchronizedString {
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (getString()) {
                try {
                    System.out.println("已锁->"+getString());
                    Thread.sleep(5000);
                    System.out.println("打印了A,释放->"+getString());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();


        new Thread(() -> {
            synchronized (getString()) {
                System.out.println("打印了B");
            }
        }).start();

        new Thread(() -> {
            synchronized (getString()) {
                System.out.println("打印了C");
            }
        }).start();

        new Thread(() -> {
            synchronized (getString()) {
                System.out.println("打印了D");
            }
        }).start();
    }

    private static String getString() {
        return new StringBuilder("校验锁X").toString().intern();
    }
}

测试结果,在A拿到“校验锁X”后,其他线程必须等待A释放锁后才可以执行
上一篇下一篇

猜你喜欢

热点阅读