问题描述

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

通过代理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;}@Overridepublic 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;@Testpublic 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 arrayreturn new String(value, 0, count);
}

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

如何解决?

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

OK,代码这么改:

 1 public class StringThread implements Runnable {
 2
 3     private static final String LOCK_PREFIX = "XXX---";
 4
 5     private String ip;
 6
 7     public StringThread(String ip) {
 8         this.ip = ip;
 9     }
10
11     @Override
12     public void run() {
13
14         String lock = buildLock();
15         synchronized (lock) {
16             System.out.println("[" + JdkUtil.getThreadName() + "]开始运行了");
17             // 休眠5秒模拟脚本调用
18             JdkUtil.sleep(5000);
19             System.out.println("[" + JdkUtil.getThreadName() + "]结束运行了");
20         }
21     }
22
23     private String buildLock() {
24         StringBuilder sb = new StringBuilder();
25         sb.append(LOCK_PREFIX);
26         sb.append(ip);
27
28         String lock = sb.toString().intern();
29         System.out.println("[" + JdkUtil.getThreadName() + "]构建了锁[" + lock + "]");
30
31         return lock;
32     }
33
34 }

看一下代码执行结果:

[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. java 字符串转list_浅谈java 字符串,字符数组,list间的转化

    1.关于java.lang.string.split xxx.split()方法可以将一个字符串分割为子字符串,然后将结果作为字符串数组返回. 2.字符串转字符数组 String str =" ...

  2. java 字符串赋值_灵魂拷问:为什么 Java 字符串是不可变的?

    在逛 programcreek 的时候,发现了一些精妙绝伦的主题.比如说:为什么 Java 字符串是不可变的?像这类灵魂拷问的主题,非常值得深思.对于绝大多数的初级程序员来说,往往停留在"知 ...

  3. 字符串截取后两位,字符串转成数组,再转换位字符串

    4.11号笔记 字符串去掉所有空格,转成数组,再转成字符串 var str = ' 你好! 世界 * * ' var arr = str.replace(/\s/g, "").sp ...

  4. java字符串常量存哪里_浅谈JAVA中字符串常量的储存位置

    在讲述这些之前我们需要一些预备知识: Java的内存结构我们可以通过两个方面去看待它. 从该角度看的话Java内存结构包含以下部分:该部分内容可以结合:JVM简介(更加详细深入的介绍) 1.栈区:由编 ...

  5. java按空格分字符串,两种用空格分隔的java字符串的方式

    两种用空格分隔的java字符串的方式 两种方式: 1. String str = "123 456 789 111"; String [] strArray = str.split ...

  6. java字符串转换成字节数组_将Java字符串转换为字节数组

    我有一个要加密的字节数组,然后转换为字符串,以便可以传输. 当我收到字符串时,我必须将字符串转换回字节数组,以便可以对其进行解密. 我检查了接收到的字符串是否与发送的字符串(包括长度)匹配,但是当我使 ...

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

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

  8. java公平锁和非公平锁_java并发编程学习之再谈公平锁和非公平锁

    在java并发编程学习之显示锁Lock里有提过公平锁和非公平锁,我们知道他的使用方式,以及非公平锁的性能较高,在AQS源码分析的基础上,我们看看NonfairSync和FairSync的区别在什么地方 ...

  9. java定义字符串数组_java字符数组用法总结,java字符串数组的定义与使用

    java字符数组,java数组的定义与使用 二维数组 String[] str = new String[5]; //创建一个长度为5的String(字符串)型的一维数组 String[] str = ...

最新文章

  1. c语言用getchar函数输入两个字符c1,c语言:用getchar函数读入两个字符给c1,c2,用putchar和printf输出。思考问题...
  2. oracle 控制文件在哪里设置_从Oracle到PostgreSQL:最全控制文件
  3. 【设计模式】—— 模板方法Template
  4. Windows 2003 NTP 时间服务器设置
  5. oracle pdb还原为no-cdb,oracle 12c中CDB和PDB的备份还原实验
  6. Ant—使用Ant构建简单Java项目(二)
  7. python脚本调度程序_python任务调度实例分析
  8. 狮子鱼社区团购商城系统小程序v17.7.0独立版+前端 安装使用教程
  9. 北京游玩之北海首都博物馆
  10. python爬虫 | 爬取巨潮资讯上的上市公司招股说明书
  11. 置信度置信水平置信区间
  12. 一文了解肠-器官轴,肠好身体好
  13. 【NOIP2014】解方程【秦九韶】【高精度处理】
  14. 安全产品的核心逻辑-IPS/IDS
  15. 0.进校的第一张Excel表:“住宿分布表” ——《Excel“智能化”之路》 系列文章
  16. php 单笔转账到支付宝账户,php实现单笔转账到支付宝功能
  17. 【面试流水账】一年半经验前端年底求职路
  18. 最清晰的进制转换讲解 - java实现
  19. 概率论基础 - 9 - 中心极限定理
  20. Google Chrome常规浏览器设置: Adobe Flash Player

热门文章

  1. lsof查看占用高_查看端口占用情况lsof,并关闭对应进程kill
  2. 无法将文件ftp服务器上,ftp服务器上传不了文件
  3. java将html实体字符转换成正常字符
  4. Camel中的几个重要概念之Routes, RouteBuilders 和 Java DSL
  5. assets与static的区别
  6. Tornado-Lesson05-模版继承、函数和类导入、ui_methods和ui_modules
  7. Java三行代码搞定MD5加密
  8. Postgre约束详解
  9. 20140923 cin.get() getline cin
  10. notepad++的NppFTP插件远程连接linux操作系统