业务有一个需求,我把问题描述一下:

通过代理IP访问国外某网站N,每个IP对应一个固定的网站N的COOKIE,COOKIE有失效时间。

并发下,取IP是有一定策略的,取到IP之后拿IP对应的COOKIE,发现COOKIE超过失效时间,则调用脚本访问网站N获取一次数据。

为了防止多线程取到同一个IP,同时发现该IP对应的COOKIE失效,同时去调用脚本更新COOKIE,针对IP加了锁。为了保证锁的全局唯一性,在锁前面加了标识业务的前缀,使用synchronized(lock){...}的方式,锁住"锁前缀+IP",这样保证多线程取到同一个IP,也只有一个IP会更新COOKIE。

不知道这个问题有没有说清楚,没说清楚没关系,写一段测试代码:

public class StringThread implements Runnable {

private static final String LOCK_PREFIX = "XXX---";

private String ip;

public StringThread(String ip) {

this.ip = ip;

}

@Override

public void run() {

String lock = buildLock();

synchronized (lock) {

System.out.println("[" + JdkUtil.getThreadName() + "]开始运行了");

// 休眠5秒模拟脚本调用

JdkUtil.sleep(5000);

System.out.println("[" + JdkUtil.getThreadName() + "]结束运行了");

}

}

private String buildLock() {

StringBuilder sb = new StringBuilder();

sb.append(LOCK_PREFIX);

sb.append(ip);

String lock = sb.toString();

System.out.println("[" + JdkUtil.getThreadName() + "]构建了锁[" + lock + "]");

return lock;

}

}

简单说就是,传入一个IP,尽量构建一个全局唯一的字符串(这么做的原因是,如果字符串的唯一性不强,比方说锁的”192.168.1.1″,如果另外一段业务代码也是锁的这个字符串”192.168.1.1″,这就意味着两段没什么关联的代码块却要串行执行,代码块执行时间短还好,代码块执行时间长影响极其大),针对字符串加锁。

预期的结果是并发下,比如5条线程传入同一个IP,它们构建的锁都是字符串”XXX—192.168.1.1″,那么这5条线程针对synchronized块,应当串行执行,即一条运行完毕再运行另外一条,但是实际上并不是这样。

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

public class StringThreadTest {

private static final int THREAD_COUNT = 5;

@Test

public void testStringThread() {

Thread[] threads = new Thread[THREAD_COUNT];

for (int i = 0; i < THREAD_COUNT; i++) {

threads[i] = new Thread(new StringThread("192.168.1.1"));

}

for (int i = 0; i < THREAD_COUNT; i++) {

threads[i].start();

}

for (;;);

}

}

执行结果为:

[Thread-1]构建了锁[XXX---192.168.1.1]

[Thread-1]开始运行了

[Thread-3]构建了锁[XXX---192.168.1.1]

[Thread-3]开始运行了

[Thread-4]构建了锁[XXX---192.168.1.1]

[Thread-4]开始运行了

[Thread-0]构建了锁[XXX---192.168.1.1]

[Thread-0]开始运行了

[Thread-2]构建了锁[XXX---192.168.1.1]

[Thread-2]开始运行了

[Thread-1]结束运行了

[Thread-3]结束运行了

[Thread-4]结束运行了

[Thread-0]结束运行了

[Thread-2]结束运行了

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

关于这个问题,一方面确实是我大意了以为是代码其他什么地方同步控制出现了问题,一方面也反映出我对String的理解还不够深入,因此专门写一篇文章来记录一下这个问题并写清楚产生这个问题的原因和应当如何解决。

问题原因

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

@Override

public void run() {

String lock = buildLock();

synchronized (lock) {

System.out.println("[" + JdkUtil.getThreadName() + "]开始运行了");

// 休眠5秒模拟脚本调用

JdkUtil.sleep(5000);

System.out.println("[" + JdkUtil.getThreadName() + "]结束运行了");

}

}

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

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

private String buildLock() {

StringBuilder sb = new StringBuilder();

sb.append(LOCK_PREFIX);

sb.append(ip);

String lock = sb.toString();

System.out.println("[" + JdkUtil.getThreadName() + "]构建了锁[" + 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-192.168.1.1″,但是由于StringBuilder的toString()方法每次都是new一个String出来,因此buildLock出来的对象都是不同的对象。

如何解决?

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

OK,代码这么改:

public class StringThread implements Runnable {

private static final String LOCK_PREFIX = "XXX---";

private String ip;

public StringThread(String ip) {

this.ip = ip;

}

@Override

public void run() {

String lock = buildLock();

synchronized (lock) {

System.out.println("[" + JdkUtil.getThreadName() + "]开始运行了");

// 休眠5秒模拟脚本调用

JdkUtil.sleep(5000);

System.out.println("[" + JdkUtil.getThreadName() + "]结束运行了");

}

}

private String buildLock() {

StringBuilder sb = new StringBuilder();

sb.append(LOCK_PREFIX);

sb.append(ip);

String lock = sb.toString().intern();

System.out.println("[" + JdkUtil.getThreadName() + "]构建了锁[" + lock + "]");

return lock;

}

}

看一下代码执行结果:

[Thread-0]构建了锁[XXX---192.168.1.1]

[Thread-0]开始运行了

[Thread-3]构建了锁[XXX---192.168.1.1]

[Thread-4]构建了锁[XXX---192.168.1.1]

[Thread-1]构建了锁[XXX---192.168.1.1]

[Thread-2]构建了锁[XXX---192.168.1.1]

[Thread-0]结束运行了

[Thread-2]开始运行了

[Thread-2]结束运行了

[Thread-1]开始运行了

[Thread-1]结束运行了

[Thread-4]开始运行了

[Thread-4]结束运行了

[Thread-3]开始运行了

[Thread-3]结束运行了

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

再看String

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

  1. JDK6的版本,常量池在持久代PermGen中分配;

  2. JDK7的版本,常量池在堆Heap中分配。

字符串是存储在常量池中的,有两种类型的字符串数据会存储在常量池中:

  1. 编译期就可以确定的字符串,即使用”"引起来的字符串,比如String a = “123″、String b = “1″ + B.getStringDataFromDB() + “2″ + C.getStringDataFromDB()、这里的”123″、”1″、”2″都是编译期间就可以确定的字符串,因此会放入常量池,而B.getStringDataFromDB()、C.getStringDataFromDB()这两个数据由于编译期间无法确定,因此它们是在堆上进行分配的。

  2. 使用String的intern()方法操作的字符串,比如String b = B.getStringDataFromDB().intern(),尽管B.getStringDataFromDB()方法拿到的字符串是在堆上分配的,但是由于后面加入了intern(),因此B.getStringDataFromDB()方法的结果,会写入常量池中

常量池中的String数据有一个特点:每次取数据的时候,如果常量池中有,直接拿常量池中的数据;如果常量池中没有,将数据写入常量池中并返回常量池中的数据。

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

“XXX-192.168.1.1″这个字符串尽管是使用StringBuilder的toString()方法创建的,但是由于使用了intern()方法,因此第一条线程发现常量池中没有”XXX-192.168.1.1″,就往常量池中放了一个

“XXX-192.168.1.1″,后面的线程发现常量池中有”XXX-192.168.1.1″,就直接取常量池中的”XXX-192.168.1.1″。

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

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

后记

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

这个业务场景并不复杂,整个代码实现也不是很复杂,但是运行的时候它就出了并发问题了。 如果没有扎实的基础:知道String里面除了常用的那些方法indexOf、subString、concat外还有很不常用的intern()方法 不了解一点JVM:JVM内存分布,尤其是常量池 不去看一点JDK源码:StringBuilder的toString()方法 不对并发有一些理解:synchronized锁代码块的时候怎么样才能保证多线程是串行执行代码块里面的代码的 这个问题出了,是根本无法解决的,甚至可以说如何下手去分析都不知道。

因此,并不要觉得JVM、JDK源码底层实现原理什么的没用,恰恰相反,这些都是技术人员成长路上最宝贵的东西。

记一次 synchronized 锁字符串引发的坑兼再谈 Java 字符串的更多相关文章

  1. 记一次synchronized锁字符串引发的坑兼再谈Java字符串

    问题描述 业务有一个需求,我把问题描述一下: 通过代理IP访问国外某网站N,每个IP对应一个固定的网站N的COOKIE,COOKIE有失效时间.并发下,取IP是有一定策略的,取到IP之后拿IP对应的C ...

  2. 浅谈Java字符串

    从概念上而言,Java字符串就是Unicode字符序列.由于Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义类String,每个用双引号的括起来的字符串都是String类的一个实 ...

  3. synchronized锁级别的一个坑

    在实现一次对限流接口访问时,我错误的使用了单例+synchronized修饰方法的形式实现,这样在限流方规则为不同接口不同限制,单独限制时,同一个实例中的所有被synchronized修饰的方法竞争同 ...

  4. 浅谈 Java 字符串(String, StringBuffer, StringBuilder)

    我们先要记住三者的特征: String 字符串常量 StringBuffer 字符串变量(线程安全) StringBuilder 字符串变量(非线程安全) 一.定义 查看 API 会发现,String ...

  5. 为什么Java字符串是不可变对象?

    转自 http://developer.51cto.com/art/201503/468905.htm 本文主要来介绍一下Java中的不可变对象,以及Java中String类的不可变性,那么为什么Ja ...

  6. Java 字符串简介

    从概念上讲,Java 字符串就是 Unicode 字符序列.Java 没有内置的字符串类型,而是在标准 Java 类库中提供了一个预定义类,很自然地叫做 String.每个用双引号括起来的字符串都是 ...

  7. Java多线程4:synchronized锁机制

    脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过 ...

  8. 线程 synchronized锁机制

    脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过 ...

  9. synchronized锁机制(六)

    前言 1.理解同步关键词synchronized 2.同步方法与同步代码块的区别 3.理解锁的对象this 脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的 ...

随机推荐

  1. 遇见JMS[1] —— activeMQ的简单使用

    1.JMS Java Message Service,提供API,供两个应用程序或者分布式应用之间异步通信,以传送消息. 2.相关概念 提供者:实现JMS规范的消息中间件服务器客户端:发送或接收消息的 ...

  2. hdu1425 哈希技术

    常用的技巧,把每个数字分别对应数组的下标,如果存在小于零的数字,就统一加一个数使得都能映射到一个下标上去. AC代码: #include<cstdio> #include<cstri ...

  3. 一个逼格很低的appium自动化测试框架

    Github地址: https://github.com/wuranxu 使用说明 1. 安装配置Mongo数据库 下载地址 mongo是用来存放元素定位的,截图如下: 通过case_id区分每个ca ...

  4. Hadoop序列化与Java序列化

    序列化就是把内存中的对象的状态信息转换成字节序列,以便于存储(持久化)和网络传输 反序列化就是就将收到的字节序列或者是硬盘的持久化数据,转换成内存中的对象. 1.JDK的序列化 只要实现了serial ...

  5. Bind、Apply、Call三者的区别

    1)bind与apply.call 的最大区别就是:bind不会立即调用,其他两个会立即调用 var fn = { _int: function(){return 3}, fun: function( ...

  6. Linux如何查找处理文件名后包含空格的文件

    Linux如何查找处理文件名后包含空格的文件   当Linux下文件名中出现空格这类特殊情况话,如何查找或确认那些文件名后有空格呢? 又怎么批量替换处理掉这些空格呢? 方法1: 输入文件名后使用Tab ...

  7. tms320dm6446内核启动分析

    关于达芬奇DM6446,里面内部有两个部分,一个是ARM926ejs的核,还有一个是C64+DSP的视频处理核,而我需要关心的重点是arm926ejs的核(bootload和linux内核) 从boo ...

  8. R语言︱文本挖掘之中文分词包——Rwordseg包(原理、功能、详解)

    每每以为攀得众山小,可.每每又切实来到起点,大牛们,缓缓脚步来俺笔记葩分享一下吧,please~ --------------------------- 笔者寄语:与前面的RsowballC分词不同的 ...

  9. Java中集合List,Map和Set的区别

    Java中集合List,Map和Set的区别 1.List和Set的父接口是Collection,而Map不是 2.List中的元素是有序的,可以重复的 3.Map是Key-Value映射关系,且Ke ...

  10. 基于jQuery的一个提示功能的实现

    最近有点忙,没有时间更新自己的博客,只能说我在原地踏步了,不知道你们进步了没有? 今天给大家分享一个提示的实现,有点简单,适合小白同学学习.下面是效果图 提示的功能: 当鼠标进入“我的菜单”的子菜单时 ...