共享模型之不可变类
一个类对象,如果它是不可变的,即便它是线程共享的,那它也是线程安全的。
本章主要内容:
- 不可变类的使用
- 不可变类的设计
- 无状态类的设计
# 1. 日期转换问题
# 1.1 问题提出:线程不安全类 SimpleDateFormat
下面的代码在运行时,由于 SimpleDateFormat 不是线程安全的,有很大几率出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
2
3
4
5
6
7
8
9
10
11
这虽然可以给 sdf
用 synchronized 加上锁来解决,但由于其互斥性和锁的开销,会带来性能上的损失。下面介绍用不可变类来代替的解决方案。
# 1.2 DateTimeFormatter 不可变类
如果一个对象不能够被修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!
这样的对象在Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LocalDate date = dtf.parse("2018-10-01", LocalDate::from);
log.debug("{}", date);
}).start();
}
2
3
4
5
6
7
8
可以看 DateTimeFormatter 的文档:
@implSpec
This class is immutable and thread-safe.
2
不可变对象,实际是另一种避免竞争的方式。
# 2. 不可变类的设计
另一个大家更为熟悉的 String 类也是不可变的,以它为例,说明一下不可变设计的要素:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
// ...
}
2
3
4
5
6
7
8
# 2.1 final 的使用
发现该类、类中所有属性都是 final 的:
- 属性用 final 修饰保证了该属性是只读的,不能修改
- 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
# 2.2 保护性拷贝
但有同学会说,使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是如何实现的,就以 substring 为例:
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
2
3
4
5
6
7
8
9
10
发现其内部是调用 String 的构造方法创建了一个新字符串,再看一下这个构造函数的实现:
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
可以看到,构造新字符串对象时,会生成新的 char[] value,对内容进行复制。这种通过创建副本对象来避免共享的手段称之为保护性拷贝(defensive copy)
# 2.3 模式之享元模式
# 2.3.1 简介
定义:英文名称:Flyweight pattern。用于当需要重用数量有限的同一类对象时。
wikipedia: A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects
flyweight是一种通过与其他类似对象共享尽可能多的数据来最小化内存使用的对象
像线程池、JDK 内部的 Integer 等实现都是享元模式的设计思想。
在 JDK 中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了
valueOf
方法,例如 Long 的valueOf
会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象:
# 2.4 享元的应用:DIY 自定义数据库连接池
一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。
这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
下面是一个数据库连接池 Pool 的实现:
class Pool {
// 1. 连接池大小
private final int poolSize;
// 2. 连接对象数组
private Connection[] connections;
// 3. 连接状态数组 0 表示空闲, 1 表示繁忙
private AtomicIntegerArray states;
// 4. 构造方法初始化
public Pool(int poolSize) {
this.poolSize = poolSize;
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("连接" + (i+1));
}
}
// 5. 借连接
public Connection borrow() {
while(true) {
for (int i = 0; i < poolSize; i++) {
// 获取空闲连接
if(states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
// 如果没有空闲连接,当前线程进入等待
synchronized (this) {
try {
log.debug("wait...");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 6. 归还连接
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == conn) {
states.set(i, 0);
synchronized (this) {
log.debug("free {}", conn);
this.notifyAll();
}
break;
}
}
}
}
class MockConnection implements Connection {
// 实现略
}
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
连接池的使用:
Pool pool = new Pool(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Connection conn = pool.borrow();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.free(conn);
}).start();
}
2
3
4
5
6
7
8
9
10
11
12
13
但以上实现没有考虑:
- 连接的动态增长与收缩
- 连接保活(可用性检测)
- 等待超时处理
- 分布式 hash
对于关系型数据库,有比较成熟的连接池实现,例如 c3p0, druid 等。
对于更通用的对象池,可以考虑使用 Apache Commons pool,例如 redis 连接池可以参考 jedis 中关于连接池的实现。
# 3. final 使用的原理
# 3.1 设置 final 变量的原理
理解了 volatile 原理,再对比 final 的实现就比较简单了:
public class TestFinal {
final int a = 20;
}
2
3
字节码如下:
发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况。
如果没有 final 的写屏障保护,那么就可能会出问题,因为 a
的值为 20 在指令层面是分两步走的,首先被 JVM 初始化为 0,然后设置值为 20。final 所加入的写屏障保证了这个 20 对其他线程的可见性,从而防止了二义性的出现。
# 3.2 获取 final 变量的原理
// 没看懂,先放着
# 4. 无状态类
无状态类,即没有成员变量的类。
在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的。
# 5. 小结
- 不可变类使用
- 不可变类设计
- 原理方面
- final
- 模式方面
- 享元