自学内容网 自学内容网

synchronized可以锁字符串吗?分享使用synchronized锁定字符串存在的坑,以及代码中如何避免的方法

1.背景

       最近我在优化一段以前代码的时候,对代码性能提升使用Synchronized有如下使用心得。项目工程化过程中虽然我们可以通过堆资源的方式应对系统存在的性能瓶颈,但是当地主家也没有余粮的时候,我们还是得考虑如何优化代码逻辑以提升代码质量。面对并发会存在线程不安全等问题,如何使用同步锁来保证代码的安全性,提升代码性能,是程序员进阶的一大挑战。加锁是在多线程中最常使用的方法,通常最简单的方式是使用Synchronized进行加锁,让并发的请求串行化执行。为了追求性能我们会将锁设计到锁定最小化,通常我们只需要锁定较小的代码片段,就比如锁定字符串,但是在使用过程中,我们真的正确的使用了Synchronized锁定字符串了么?下面我们将通过以下使用Synchronized的6种情况,分享Synchronized使用过程中可能存在的坑,以及推荐的使用方法

2.实验演示分享,不加锁的(线程不安全)

static Map<String, Integer> values = new ConcurrentHashMap<>();

public static void saving(String schoolName) {
//线程进入
System.out.println(schoolName+" 开始交卷");
try {
//进入后睡眠
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (values.containsKey(schoolName)) {
values.put(schoolName, values.get(schoolName) + 1);
} else {
values.put(schoolName, 1);
}
//线程结束
System.out.println(schoolName+" 交卷完成");
}


//分别开启三个线程,模拟提交答卷,然后统计答卷数量
public static void main(String[] args) throws InterruptedException {
        
Thread thread1 = new Thread(() -> saving(new String("第一小学")));
Thread thread2 = new Thread(() -> saving(new String("第二小学")));
Thread thread3 = new Thread(() -> saving(new String("第一小学")));

thread1.start();
thread2.start();
thread3.start();
thread1.join();
thread2.join();
thread3.join();
System.out.println(values);

}

执行结果:
第二小学 开始交卷
第一小学 开始交卷
第一小学 开始交卷
第一小学 交卷完成
第二小学 交卷完成
第一小学 交卷完成
{第一小学=1, 第二小学=1}
        执行结果统计错误,三个线程同时开始,因为没有锁定提交线程导致结果错误

3.实验演示分享,锁类的情况(串行)

public static void saving(String schoolName) {
synchronized (ThreadTest.class){
//线程进入
System.out.println(schoolName+" 开始交卷");
try {
//进入后睡眠
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (values.containsKey(schoolName)) {
values.put(schoolName, values.get(schoolName) + 1);
} else {
values.put(schoolName, 1);
}
//线程结束
System.out.println(schoolName+" 交卷完成");
}
}

执行结果:

第一小学 交卷完成
第一小学 开始交卷
第一小学 交卷完成
第二小学 开始交卷
第二小学 交卷完成
{第一小学=2, 第二小学=1}

        锁类之后,我们发现类中所有线程都变成串行执行,这时基本上就与我们多线程并发无关了,不能提升代码效率。有什么办法可以改变这个现状么?当然有,我们可以缩小锁的范围

4.实验演示分享,锁字符串schoolName的情况(线程不安全)

public static void saving(String schoolName) {
//直接锁定字符串 有坑
synchronized (schoolName) {
//线程进入
System.out.println(schoolName+" 开始交卷");
try {
//进入后睡眠
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (values.containsKey(schoolName)) {
values.put(schoolName, values.get(schoolName) + 1);
} else {
values.put(schoolName, 1);
}
//线程结束
System.out.println(schoolName+" 交卷完成");
}
}

        这里面其实是有坑的,因为在Java中如果我们传递的String是都是new新的String对象,就比如调用代码如下:

        Thread thread1 = new Thread(() -> saving(new String("第一小学")));
Thread thread2 = new Thread(() -> saving(new String("第二小学")));
Thread thread3 = new Thread(() -> saving(new String("第一小学")));

 执行结果
第二小学 开始交卷
第一小学 开始交卷
第一小学 开始交卷
第一小学 交卷完成
第二小学 交卷完成
第一小学 交卷完成
{第一小学=1, 第二小学=1}

        即每次传入的对象都是一个新的String对象,那么这个锁就会不安全,因为,第一行的String对象和第三个String对象,虽说他们的值是相同的但是他们是属于两个不同的String对象,这两个对象是不相等的,所以也会造成线程不安全。那有没有其他的办法呢?当然有,我们直接锁定String常量,代码如下:

5.实验演示分享,锁字符串常量的情况(可能影响其他代码片段执行)

public static void saving(String schoolName) {
        //常量池 全局  有坑
synchronized (schoolName.intern()) {
//线程进入
System.out.println(schoolName+" 开始交卷");
try {
//进入后睡眠
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (values.containsKey(schoolName)) {
values.put(schoolName, values.get(schoolName) + 1);
} else {
values.put(schoolName, 1);
}
//线程结束
System.out.println(schoolName+" 交卷完成");
}
}

        这个代码能够确保并行且结果正确,但是也同时隐含了一个问题,因为代码锁定的字符串常量,在JVM虚拟机中所有的字符串常量都是全局可见的,这时如果另外一个程序需要锁定这个常量,就无法被锁定,所以这也会导致,虽说现在这个代码段能够正常运行,但是会隐含影响潜在的其他代码段的正常运行

6.实验演示分享,锁字特殊的符串常量的情况(推荐)

public static void saving(String schoolName) {
        //特殊的常量
synchronized (uuid+schoolName.intern()) {
//线程进入
System.out.println(schoolName+" 开始交卷");
try {
//进入后睡眠
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (values.containsKey(schoolName)) {
values.put(schoolName, values.get(schoolName) + 1);
} else {
values.put(schoolName, 1);
}
//线程结束
System.out.println(schoolName+" 交卷完成");
}
}

        从伪代码可以看出,我们锁定的不是一个共有的字符串常量,而是一个特殊的字符串,确保这段代码锁定的字符串与其他程序片段锁定字符串内容不冲突即可。

7.实验演示分享,锁字特殊的符串常量的情况(推荐)

//毕竟弄个map就会在初始化锁对象前额外加一次锁。
static ConcurrentHashMap<String, Object> lock = new ConcurrentHashMap<>();
public static void saving(String schoolName) {
//存在直接返回,不存在执行自定义逻辑
synchronized (lock.computeIfAbsent(schoolName, k -> new Object())) {
//线程进入
System.out.println(schoolName+" 开始交卷");
try {
//进入后睡眠
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (values.containsKey(schoolName)) {
values.put(schoolName, values.get(schoolName) + 1);
} else {
values.put(schoolName, 1);
}
//线程结束
System.out.println(schoolName+" 交卷完成");
}
}

        我们使用computeIfAbsent,编程中经常遇到这种数据结构,判断一个map中是否存在这个key,如果存在则处理value的数据,如果不存在,则创建一个满足value要求的数据结构放到value中。然后对这个value进行加锁,如果不存在合适的value,代码每次都加锁一个新的Object对象,如果存在合适的value,那么就锁定原有的Object对象。这种方式下,在保证能够并发的情况下,也能保证线程的安全性。

8.小结一下

        我们从代码逐步分析了Synchronized使用的一些方法,分享了Synchronized常出现的坑。


原文地址:https://blog.csdn.net/Scalzdp/article/details/142516315

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!