线程不安全——Part-1

概述

线程不安全类:如果一个类的对象同时被多个线程访问,若不做相应的同步或并发处理,容易出现线程不安全的现象,比如:抛出异常、逻辑处理错误等。

StringBiulder

看一个例子:(借助之前的例子结构)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Slf4j
@NotThreadSafe
public class StringExample1 {

public static int clientTotal = 5000;

public static int threadTotal = 200;

public static StringBuilder stringBuilder = new StringBuilder();

public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
update();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("size:{}", stringBuilder.length());
}

private static void update() {
stringBuilder.append("1");
}
}

结果分析:

  1. 多次运行,结果几乎未达5000,显然StringBuilder是非线程安全的。
  2. 通过定义stringBuilder对象,核心方法为update() -> 每次拼接一个字符串,最后取其长度length。

StringBuffer

例子代码结构跟上面一样,只需将StringBiulder换为StringBuffer,且其两者方法名相同。

运行结果:
多次运行,结果均为5000。StringBuffer是线程安全的!

查看StringBuffer源码
1
2
3
4
5
6
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
1
2
3
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence{XXX省略}
1
2
3
4
5
6
7
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}

源码分析:

  1. 几乎所有复写的方法都有toStringCache变量。为对象方便转为String类型的字段,调用Arrays.copyOfRange(value, 0, count),return new String(toStringCache, true):其中该构造方法是String包私有的构造方法,以确保数值分享的效率。
  2. StringBuffer继承自AbstractStringBuilder,并几乎重写了所有继承来的方法。调用父辈super的append方法,即AbstractStringBuilder的方法。且StringBuffer对象一经修改,toStringCache清空为null。类似String包装类的对象,避免多线程并发问题。(以后细说String包装类…)
  3. 为了线程安全,几乎所有复写的方法都用synchronized进行标识,即使效率较低。
  4. StringBiulder性能好,但不适用于多线程。但适用于场景为方法内的局部变量操作(上篇线程封闭的手记中说到:隐式的堆栈封闭),线程安全且性能较好。

SimpleDateFormate

Java提供的供日期转换的类。该例子结构仍然和上述例子相同。只需定义一个simpleDateFormat实例,核心方法换为parse(xxx:日期语句)

运行结果:
出现异常:parse exception -- java.lang.NumberFormatException: multiple points
非常简单,说明了该日期转换方法是非线程间安全的。

正确写法:(通过堆栈封闭->声明为核心方法内的局部变量,即每次声明一个新的对象进行调用)

仍然是之前的测试结构,但还是贴出来看看吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Slf4j
@ThreadSafe
public class DateFormatExample2 {

// 请求总数
public static int clientTotal = 5000;

// 同时并发执行的线程数
public static int threadTotal = 200;

public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
update();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
}

private static void update() {
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
simpleDateFormat.parse("20180208");
} catch (Exception e) {
log.error("parse exception", e);
}
}
}

运行结果: 多次运行,不会报错!是线程安全!

Joda Time

该类本质上并不属于Java提供。需引入jar包。例子仍然是之前的测试结构。只不过日志输出了次数及当时日期

1
2
3
4
private static void update(int i) {
//转换完后DateTime,调用toDate()转为Date对象。
log.info("{}, {}", i == 4999 ? i+"--------------------------" : i, DateTime.parse("20180728", dateTimeFormatter).toDate());
}

运行结果:
由于调用是多并发的,调用次序是乱序,但总数一定!
由于i是从0到4999,即当i = 4999时说明已运行慢5000个,即线程安全!
图片截图中字体小了,可以点开看~
图示

(后接该手记part-2……)

SupriseMF wechat
欢迎关注微信订阅号【星球码】,分享学习编程奇淫巧技~
喜欢就支持我呀(*^∇^*)~

热评文章