String的替代品线程安全问题 | Java Debug 笔记

776 阅读2分钟

本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看<活动链接>

前言

  • 书接上文啊,之前我在代码审查中被领导批斗了因为大量使用String来进行字符串的操作。我们也分析了问题后来改用了StringBuilder。本以为万事大吉关机下班了。没想到第二天审查依然不通过

问题描述

  • 虽然改用StringBuilder避免了不必要的GC和内存空间使用。但是StringBuilder方法只是适用于单线程。在多线程操作就会出现并发问题
public static void main(String[] args) throws InterruptedException {
    StringBuilder res = new StringBuilder("");
    List<Thread> threadList = new ArrayList<>();
    CountDownLatch latch = new CountDownLatch(1000);
    for (int i = 0; i < 1000; i++) {
        int finalI = i;
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                res.append(","+ finalI);
                latch.countDown();
            }
        });
        threadList.add(thread);
    }
    for (Thread thread : threadList) {
        thread.start();
    }
    latch.await();
    System.out.println("@@@@@@"+res.toString().split(",").length);
}
  • 不出意外最终得到的长度并不是1001。这里只能说可能不是1001 。 因为并发并不是100%就一定会出错。如果不是1001 。 读者可以多试几次

问题定位

  • 问题很明显是多并发导致append出错。因为append原理是获取到原字符然后在原字符后面添加字符。这是两个步骤并不是原子性这就是在获取完之后原字符被另外一个线程修改了然后本线程将旧数据新增的字符统一写会内存中这就导致另外一个线程写入的数据丢失。
  • 我们自己也可以解决这个问题就是在调用append的方法之前加一把锁Lock或者synchronized

再次送审

  • 毫无意外这次还是没有通过。经理给出的回复是加锁太笨重了。自己回想一下的确不太友好。在append这里加锁不仅增加了代码的复杂性还容易忘记释放锁。

  • 这时候打开百度开始取经。网络上都推荐使用StringBuffer因为他是线程安全的。

  • 修改工作量也不大就是将StringBuilder全部替换成StirngBuffer

  • 但是点开源码对比一下会发现StringBuffer#append并没有做啥特殊的事情,也只是通过synchronized加了一把锁并且多了一个参数的设置

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}
  • 我也只能安慰自己经理不让我用synchronized的原因是因为怕我操作不好了。不过StringBuffer中的toStringCache的作用就是在tostring的时候将最后一个字符缓存起来提高使用性吧。

总结

  • 经验就是时间的积累。如果在我看来我就仅仅加把锁完事解决。但是因为没有经历过并发的洗礼可能操作不好锁的事情
  • java内置提供的尽量使用别人的。不要造轮子但是得知道轮子的建造过程

欢迎点赞啊