不可变:如果一个对象子不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改。
7.1 不可变类的使用
7.1.1 问题提出
SimpleDateFormat
不是线程安全的,下列代码执行时会产生线程安全问题:
@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();
}
}
}
结果:
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
能解决安全问题,但是会带来性能问题。
synchronized (SDF) {
log.debug(SDF.parse(dateStr).toString());
}
7.1.3 不可变
使用JDK1.8中的不可变日期格式类DateTimeFormatter
。
@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的使用
Integer
、Double
、String
、DateTimeFormatter
以及基本类型包装类,都是用final修饰的。
- 属性用final修饰保证了该属性是只读的,不能修改
- 类用final修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
7.2.2 保护性拷贝
以String
的substring
方法为例,方法的最后还是new String
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中Boolean
、Byte
、Short
、Long
、Character
等包装类提供了valueOf
方法。
例如Longh.valueOf()
,在-128~127之间的Long对象,在这个范围内会用缓存对象,超过这个范围,才会信件Long对象。
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);
}
注意:
Byte
、Short
、Long
缓存的范围:-128~127Character
缓存的范围:0~127Integer
的默认范围:-128~127,最小值不能变,最大值通过虚拟机参数-Djava.lang.Integer.IntegerCache.high
来改变。Boolean
缓存:true / false
7.2.4 DIY连接池
例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
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变量的原理
对于以下代码:
public class FinalDemo {
final int k = 3;
}
init字节码如下:
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变量的原理
对于以下代码:
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字节码如下:
# 获取打印流
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设置成员变量,这种没有任何成员变量的类是线程安全的。
因为成员变量保存的数据也可以称为无状态信息,因为没有成员变量就称之为【无状态】。