Java中synchronized锁字符串

Synchronized锁字符串代码如下

public class StringThread implements Runnable{
    private static final String LOCK_PREFIX = "XXX---";

    private String taskNo;

    public StringThread(String taskNo) {
        this.taskNo = taskNo;
    }

    @Override
    public void run() {
        String lock = buildLock();
        synchronized (lock) {
            System.out.println("[" + Thread.currentThread().getName() + "]开始运行了");
            // 休眠5秒模拟脚本调用
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("[" + Thread.currentThread().getName() + "]结束运行了");
        }
    }

    private String buildLock() {
        StringBuilder sb = new StringBuilder();
        sb.append(LOCK_PREFIX);
        sb.append(taskNo);

        String lock = sb.toString();
        System.out.println("[" + Thread.currentThread().getName() + "]构建了锁[" + lock + "]");

        return lock;
    }
}

写一段测试代码,开8条线程看一下效果:

public static void main(String[] args) {
        Thread[] threads = new Thread[8];
        for (int i = 0; i < 8; i++) {
            threads[i] = new Thread(new StringThread("192.168.1.1"));
        }

        for (int i = 0; i < 8; i++) {
            threads[i].start();
        }

}

执行结果为:

[Thread-0]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-0]开始运行了
[Thread-1]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-1]开始运行了
[Thread-2]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-2]开始运行了
[Thread-3]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-3]开始运行了
[Thread-4]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-4]开始运行了
[Thread-5]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-5]开始运行了
[Thread-7]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-6]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-7]开始运行了
[Thread-6]开始运行了
[Thread-0]结束运行了
[Thread-1]结束运行了
[Thread-4]结束运行了
[Thread-3]结束运行了
[Thread-2]结束运行了
[Thread-6]结束运行了
[Thread-7]结束运行了
[Thread-5]结束运行了

看到Thread-0、Thread-1、Thread-2、Thread-3、Thread-4、Thread-5、Thread-6、Thread-7这8条线程尽管构建的锁都是同一个"XXX—asdasdasddxx1213asdsa",但是代码却是并行执行的,这并不符合我们的预期。

问题原因

这个问题既然出现了,那么应当从结果开始推导起,找到问题的原因。先看一下synchronized部分的代码:

@Override
public void run() {
    String lock = buildLock();
    synchronized (lock) {
        System.out.println("[" + Thread.currentThread().getName() + "]开始运行了");
        // 休眠5秒模拟脚本调用
        try {
              Thread.sleep(5000);
        } catch (InterruptedException e) {
              e.printStackTrace();
        }
        System.out.println("[" + Thread.currentThread().getName() + "]结束运行了");
   }
}

因为synchronized锁对象的时候,保证同步代码块中的代码执行是串行执行的前提条件是锁住的对象是同一个,因此既然多线程在synchronized部分是并行执行的,那么可以推测出多线程下传入同一个taskNo,构建出来的lock字符串并不是同一个。

接下来,再看一下构建字符串的代码:

 private String buildLock() {
        StringBuilder sb = new StringBuilder();
        sb.append(LOCK_PREFIX);
        sb.append(taskNo);

        String lock = sb.toString();
        System.out.println("[" + Thread.currentThread().getName() + "]构建了锁[" + lock + "]");

        return lock;
}

lock是由StringBuilder生成的,看一下StringBuilder的toString方法:

public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

那么原因就在这里:尽管buildLock()方法构建出来的字符串都是"XXX—asdasdasddxx1213asdsa",但是由于StringBuilder的toString()方法每次都是new一个String出来,因此buildLock出来的对象都是不同的对象。

如何解决?

上面的问题原因找到了,就是每次StringBuilder构建出来的对象都是new出来的对象,那么应当如何解决?这里我先给解决办法就是sb.toString()后再加上intern(),下一部分再说原因,因为我想对String再做一次总结,加深对String的理解。

private String buildLock() {
        StringBuilder sb = new StringBuilder();
        sb.append(LOCK_PREFIX);
        sb.append(taskNo);

        String lock = sb.toString().intern();
        System.out.println("[" + Thread.currentThread().getName() + "]构建了锁[" + lock + "]");

        return lock;
 }

看一下代码执行结果:

[Thread-2]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-2]开始运行了
[Thread-0]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-3]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-4]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-1]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-5]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-6]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-7]构建了锁[XXX---asdasdasddxx1213asdsa]
[Thread-2]结束运行了
[Thread-7]开始运行了
[Thread-7]结束运行了
[Thread-6]开始运行了
[Thread-6]结束运行了
[Thread-5]开始运行了
[Thread-5]结束运行了
[Thread-1]开始运行了
[Thread-1]结束运行了
[Thread-4]开始运行了
[Thread-4]结束运行了
[Thread-3]开始运行了
[Thread-3]结束运行了
[Thread-0]开始运行了
[Thread-0]结束运行了

可以对比一下上面没有加intern()方法的执行结果,这里很明显8条线程获取的锁是同一个,一条线程执行完毕synchronized代码块里面的代码之后下一条线程才能执行,整个执行是串行的。

再看String

JVM内存区域里面有一块常量池,关于常量池的分配:

JDK6的版本,常量池在持久代PermGen中分配
JDK7的版本,常量池在堆Heap中分配
字符串是存储在常量池中的,有两种类型的字符串数据会存储在常量池中:

编译期就可以确定的字符串,即使用"“引起来的字符串,比如
String a = “123”、String b = “1” + B.getStringDataFromDB() + “2” + C.getStringDataFromDB()、这里的"123”、“1”、"2"都是编译期间就可以确定的字符串,因此会放入常量池,而B.getStringDataFromDB()、C.getStringDataFromDB()这两个数据由于编译期间无法确定,因此它们是在堆上进行分配的
使用String的intern()方法操作的字符串,比如String b = B.getStringDataFromDB().intern(),尽管B.getStringDataFromDB()方法拿到的字符串是在堆上分配的,但是由于后面加入了intern(),因此B.getStringDataFromDB()方法的结果,会写入常量池中
常量池中的String数据有一个特点:每次取数据的时候,如果常量池中有,直接拿常量池中的数据;如果常量池中没有,将数据写入常量池中并返回常量池中的数据。

因此回到我们之前的场景,使用StringBuilder拼接字符串每次返回一个new的对象,但是使用intern()方法则不一样:

“XXX—asdasdasddxx1213asdsa"这个字符串尽管是使用StringBuilder的toString()方法创建的,但是由于使用了intern()方法,因此第一条线程发现常量池中没有"XXX—asdasdasddxx1213asdsa”,就往常量池中放了一个
“XXX—asdasdasddxx1213asdsa”,后面的线程发现常量池中有"XXX—asdasdasddxx1213asdsa",就直接取常量池中的"XXX—asdasdasddxx1213asdsa"。

因此不管多少条线程,只要取"XXX—asdasdasddxx1213asdsa",取出的一定是同一个对象,就是常量池中的"XXX—asdasdasddxx1213asdsa"

这一切,都是String的intern()方法的作用

后记

就这个问题解决完包括这篇文章写完,我特别有一点点感慨,很多人会觉得一个Java程序员能把框架用好、能把代码流程写出来没有bug就好了,研究底层原理、虚拟机什么的根本就没什么用。不知道这个问题能不能给大家一点启发:

这个业务场景并不复杂,整个代码实现也不是很复杂,但是运行的时候它就出了并发问题了。

如果没有扎实的基础:知道String里面除了常用的那些方法indexOf、subString、concat外还有很不常用的intern()方法
不了解一点JVM:JVM内存分布,尤其是常量池
不去看一点JDK源码:StringBuilder的toString()方法
不对并发有一些理解:synchronized锁代码块的时候怎么样才能保证多线程是串行执行代码块里面的代码的

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值