不可变:如果一个对象子不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改。

7.1 不可变类的使用

7.1.1 问题提出

SimpleDateFormat不是线程安全的,下列代码执行时会产生线程安全问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j(topic = "SimpleDateFormat")
public class SimpleDateFormatDemo {
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
public static void main(String[] args) {
String dateStr = "2001-09-11 00:00:00.000";
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug(SDF.parse(dateStr).toString());
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at top.parak.immutable.SimpleDateFormatDemo.lambda$main$0(SimpleDateFormatDemo.java:22)
at java.lang.Thread.run(Thread.java:748)
2021-04-29 11:30:23.116 [Thread-2] DEBUG SimpleDateFormat - Sun Sep 11 00:00:00 CST 1121
2021-04-29 11:30:23.117 [Thread-9] DEBUG SimpleDateFormat - Sun Sep 11 00:00:00 CST 1121
2021-04-29 11:30:23.117 [Thread-5] DEBUG SimpleDateFormat - Tue Sep 11 00:00:00 CST 2001

7.1.2 同步锁

使用同步锁synchronized能解决安全问题,但是会带来性能问题。

1
2
3
synchronized (SDF) {
log.debug(SDF.parse(dateStr).toString());
}

7.1.3 不可变

使用JDK1.8中的不可变日期格式类DateTimeFormatter

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j(topic = "DateTimeFormatter")
public class DateTimeFormatterDemo {
private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
public static void main(String[] args) {
String dateStr = "2001-09-11 00:00:00.000";
for (int i = 0; i < 10; i++) {
new Thread(() -> {
TemporalAccessor date = DTF.parse(dateStr);
log.debug("{}", date);
}).start();
}
}
}

7.2 不可变类的设计

7.1.1 final的使用

IntegerDoubleStringDateTimeFormatter以及基本类型包装类,都是用final修饰的。

  • 属性用final修饰保证了该属性是只读的,不能修改
  • 类用final修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

7.2.2 保护性拷贝

Stringsubstring方法为例,方法的最后还是new String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}

这种创建副本对象来避免共享的手段称为保护性拷贝(defensive copy)。

7.2.3 模式之享元

定义:运用共享技术来有效地支持大量细粒度对象的复用。

优势:相同对象只保存一份,这降低了系统中对象的数量,降低内存压力。

在JDK中BooleanByteShortLongCharacter等包装类提供了valueOf方法。

例如Longh.valueOf(),在-128~127之间的Long对象,在这个范围内会用缓存对象,超过这个范围,才会信件Long对象。

1
2
3
4
5
6
7
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}

注意:

  • ByteShortLong缓存的范围:-128~127
  • Character缓存的范围:0~127
  • Integer的默认范围:-128~127,最小值不能变,最大值通过虚拟机参数-Djava.lang.Integer.IntegerCache.high来改变。
  • Boolean缓存:true / false

7.2.4 DIY连接池

例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。

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
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerArray;

/**
* @author KHighness
* @since 2021-04-29
*/
public class PoolDemo {
public static void main(String[] args) {
Pool pool = new Pool(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Connection connection = pool.get();
try {
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.free(connection);
}, "T-" + (i + 1)).start();
}
}
}

@Slf4j(topic = "Pool")
class Pool {

/**
* 连接池大小
*/
private final int poolSize;

/**
* 连接数组
*/
private final Connection[] connections;

/**
* 连接状态数组
*/
private final AtomicIntegerArray states;

/**
* 初始化连接池
*/
public Pool(int pollSize) {
this.poolSize = pollSize;
connections = new Connection[pollSize];
states = new AtomicIntegerArray(new int[pollSize]);
for (int i = 0; i < pollSize; i++) {
connections[i] = new ParaKConnection("连接" + (i + 1));
}
}

/**
* 获取一个连接
*/
public Connection get() {
while (true) {
// 查看是否有空闲连接
for (int i = 0; i < poolSize; i++) {
if (states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("get {}", connections[i]);
return connections[i];
}
}
}
// 没有空闲连接则等待
synchronized (this) {
try {
log.debug("wait...");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

/**
* 释放一个连接
*/
public void free(Connection connection) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == connection) {
states.set(i, 0);
synchronized (this) {
log.debug("free {}", connection);
this.notifyAll();
}
break;
}
}
}
}

class ParaKConnection implements Connection {

private String name;

public ParaKConnection(String name) {
this.name = name;
}

@Override
public String toString() {
return "ParaKConnection[" +
"name='" + name + '\'' +
']';
}

// ...
}

可改进点:

  • 连接的动态增长与收缩
  • 连接保活(可用性检测)
  • 等待超时处理
  • 分布式hash

7.3 final原理

7.3.1 设置final变量的原理

对于以下代码:

1
2
3
public class FinalDemo {
final int k = 3;
}

init字节码如下:

1
2
3
4
5
6
7
0 aload_0
1 invokespecial #1 <java/lang/Object.<init>>
4 aload_0
5 iconst_3
6 putfield #2 <top/parak/immutable/FinalDemo.k>
<================== 写屏障
9 return

发现final变量的赋值也会通过putfield指令来完成,同样在这条指令之后也会加入写屏障,保证在其他线程读到它的值时不会出现为0的情况。

7.3.2 获取final变量的原理

对于以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class FinalDemo {
int a = 3;
static int A = 33333;
final int b = 3;
final static int B = 33333;

public static void main(String[] args) {
System.out.println(new FinalDemo().a);
System.out.println(A);
System.out.println(new FinalDemo().b);
System.out.println(B);
}
}

main字节码如下:

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
 # 获取打印流
0 getstatic #4 <java/lang/System.out>

# 打印a
3 new #5 <top/parak/immutable/FinalDemo>
6 dup
7 invokespecial #6 <top/parak/immutable/FinalDemo.<init>>
10 getfield #2 <top/parak/immutable/FinalDemo.a>
13 invokevirtual #7 <java/io/PrintStream.println>

# 打印A
16 getstatic #4 <java/lang/System.out>
# 不加final,获取A变量的时候使用getStatic,使用共享内存
19 getstatic #8 <top/parak/immutable/FinalDemo.A>
22 invokevirtual #7 <java/io/PrintStream.println>
25 getstatic #4 <java/lang/System.out>

# 打印b
28 new #5 <top/parak/immutable/FinalDemo>
31 dup
32 invokespecial #6 <top/parak/immutable/FinalDemo.<init>>
35 invokevirtual #9 <java/lang/Object.getClass>
38 pop
39 iconst_3
40 invokevirtual #7 <java/io/PrintStream.println>

# 打印B
43 getstatic #4 <java/lang/System.out>
# 加了final,没有直接去获取A变量,而是将A复制到当前Java虚拟机栈中
46 ldc #10 <33333>
48 invokevirtual #7 <java/io/PrintStream.println>
51 return

通过观察字节码可以发现,final修饰的变量有栈内存读取速度的优化。

7.4 无状态

设计Servlet时为了保证其线程安全,都会有这样的建议,不要为Servlet设置成员变量,这种没有任何成员变量的类是线程安全的。

因为成员变量保存的数据也可以称为无状态信息,因为没有成员变量就称之为【无状态】。