7. 共享模型之不可变
7.1 引言
7.1.1 提出问题
package com.rui.seven;
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("2023-09-19"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
}
}
// 某次运行结果
14:15:17 [Thread-0] c.Test1 - {}
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:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.rui.seven.Test1.lambda$main$0(Test1.java:14)
at java.lang.Thread.run(Thread.java:745)
14:15:17 [Thread-4] c.Test1 - {}
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:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.rui.seven.Test1.lambda$main$0(Test1.java:14)
at java.lang.Thread.run(Thread.java:745)
14:15:17 [Thread-3] c.Test1 - {}
java.lang.NumberFormatException: For input string: "E.4222000222323333023"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
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:2056)
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 com.rui.seven.Test1.lambda$main$0(Test1.java:14)
at java.lang.Thread.run(Thread.java:745)
14:15:17 [Thread-8] c.Test1 - {}
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.rui.seven.Test1.lambda$main$0(Test1.java:14)
at java.lang.Thread.run(Thread.java:745)
14:15:17 [Thread-9] c.Test1 - {}
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.rui.seven.Test1.lambda$main$0(Test1.java:14)
at java.lang.Thread.run(Thread.java:745)
14:15:17 [Thread-2] c.Test1 - {}
java.lang.NumberFormatException: For input string: "E.4222000222323333023E4"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
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:2056)
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 com.rui.seven.Test1.lambda$main$0(Test1.java:14)
at java.lang.Thread.run(Thread.java:745)
14:15:17 [Thread-5] c.Test1 - {}
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:2056)
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 com.rui.seven.Test1.lambda$main$0(Test1.java:14)
at java.lang.Thread.run(Thread.java:745)
14:15:17 [Thread-6] c.Test1 - {}
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:2056)
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 com.rui.seven.Test1.lambda$main$0(Test1.java:14)
at java.lang.Thread.run(Thread.java:745)
14:15:17 [Thread-1] c.Test1 - Tue Sep 19 00:00:00 CST 2220
14:15:17 [Thread-7] c.Test1 - Fri Jul 19 00:00:00 CST 2024
进程已结束,退出代码 0
上述代码的某次运行结果中出现了 java.lang.NumberFormatException 和不正确的日期解析结果
其原因是:非线程安全
7.1.2 解决思路 - 锁
package com.rui.seven;
import lombok.extern.slf4j.Slf4j;
import java.text.ParseException;
import java.text.SimpleDateFormat;
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (sdf) {
try {
log.debug("{}", sdf.parse("2023-09-19"));
} catch (ParseException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
// 某次运行结果
14:20:10 [Thread-0] c.Test1 - Tue Sep 19 00:00:00 CST 2023
14:20:10 [Thread-9] c.Test1 - Tue Sep 19 00:00:00 CST 2023
14:20:10 [Thread-8] c.Test1 - Tue Sep 19 00:00:00 CST 2023
14:20:10 [Thread-7] c.Test1 - Tue Sep 19 00:00:00 CST 2023
14:20:10 [Thread-6] c.Test1 - Tue Sep 19 00:00:00 CST 2023
14:20:10 [Thread-5] c.Test1 - Tue Sep 19 00:00:00 CST 2023
14:20:10 [Thread-4] c.Test1 - Tue Sep 19 00:00:00 CST 2023
14:20:10 [Thread-2] c.Test1 - Tue Sep 19 00:00:00 CST 2023
14:20:10 [Thread-3] c.Test1 - Tue Sep 19 00:00:00 CST 2023
14:20:10 [Thread-1] c.Test1 - Tue Sep 19 00:00:00 CST 2023
进程已结束,退出代码 0
7.1.3 解决思路 - 不可变类
DateTimeFormatter
package com.rui.seven;
import lombok.extern.slf4j.Slf4j;
import java.time.format.DateTimeFormatter;
@Slf4j(topic = "c.Test2")
public class Test2 {
public static void main(String[] args) {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
log.debug("{}", dtf.parse("2023-09-19"));
}).start();
}
}
}
// 某次运行结果
14:25:26 [Thread-3] c.Test2 - {},ISO resolved to 2023-09-19
14:25:26 [Thread-5] c.Test2 - {},ISO resolved to 2023-09-19
14:25:26 [Thread-4] c.Test2 - {},ISO resolved to 2023-09-19
14:25:26 [Thread-9] c.Test2 - {},ISO resolved to 2023-09-19
14:25:26 [Thread-8] c.Test2 - {},ISO resolved to 2023-09-19
14:25:26 [Thread-6] c.Test2 - {},ISO resolved to 2023-09-19
14:25:26 [Thread-1] c.Test2 - {},ISO resolved to 2023-09-19
14:25:26 [Thread-2] c.Test2 - {},ISO resolved to 2023-09-19
14:25:26 [Thread-0] c.Test2 - {},ISO resolved to 2023-09-19
14:25:26 [Thread-7] c.Test2 - {},ISO resolved to 2023-09-19
进程已结束,退出代码 0
7.2 不可变类的设计
通过 final、保护性拷贝保证不可变性
以 String 为例
7.2.1 final
package java.lang;
// import...
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
// ...
}
final 修饰 value[],保证了 value[] 的引用地址不能被修改
final 修饰 String,保证了 String 不能被其他类继承
7.2.2 保护性拷贝
final 修饰 value[],保证了 value[] 的引用地址不能被修改,但 value[] 的值仍可被修改
package java.lang;
// import...
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
}
可见,构造新字符串对象时,会生成新的 char value[] 并对旧的 char value[] 中的内容进行复制,即保护性拷贝
7.3 享元模式
7.3.1 简介
若频繁使用保护性拷贝,则会频繁创建新的对象
抛出疑问:这些对象中的值有重复的可能吗?
答案是:有的
通过重用对象代替创建新的对象,即享元模式的思维模式
7.3.2 体现
1. S包装类
Byte、Short、Long 缓存的范围均为 -128~127
Integer 缓存的范围为 -128~127,但最大值可以通过调整虚拟机参数来改变
-Djava.lang.Integer.IntegerCache.high
Character 缓存的范围为 0~127
Boolean 缓存了 true 和 false
若符合范围,则重用对象;若不符合范围,则创建新的对象
2. String 串池
3. BigDecimal 和 BigInteger
7.3.3 DIY
package com.rui.seven;
import lombok.extern.slf4j.Slf4j;
import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class DIY {
public static void main(String[] args) {
Pool p = new Pool(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
Connection conn = p.borrow();
Thread.sleep(new Random().nextInt(1000));
p.free(conn);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
@Slf4j(topic = "c.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() throws InterruptedException {
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];
}
}
}
// 若没有空闲连接,则当前线程进入阻塞(wait)状态
synchronized (this) {
log.debug("wait...");
this.wait();
}
}
}
// 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 {}...", connections[i]);
this.notifyAll();
}
break;
}
}
}
}
class MockConnection implements Connection {
// 连接名
private String name;
// 构造方法
public MockConnection(String name) {
this.name = name;
}
@Override
public String toString() {
return "MockConnection{" +
"name='" + name + '\'' +
'}';
}
// ...
}
// 某次运行结果
16:50:30 [Thread-1] c.Pool - wait...
16:50:30 [Thread-2] c.Pool - borrow MockConnection{name='连接1'}...
16:50:30 [Thread-0] c.Pool - borrow MockConnection{name='连接2'}...
16:50:30 [Thread-4] c.Pool - wait...
16:50:30 [Thread-3] c.Pool - wait...
16:50:30 [Thread-2] c.Pool - free MockConnection{name='连接1'}...
16:50:30 [Thread-3] c.Pool - borrow MockConnection{name='连接1'}...
16:50:30 [Thread-4] c.Pool - wait...
16:50:30 [Thread-1] c.Pool - wait...
16:50:31 [Thread-0] c.Pool - free MockConnection{name='连接2'}...
16:50:31 [Thread-1] c.Pool - borrow MockConnection{name='连接2'}...
16:50:31 [Thread-4] c.Pool - wait...
16:50:31 [Thread-1] c.Pool - free MockConnection{name='连接2'}...
16:50:31 [Thread-4] c.Pool - borrow MockConnection{name='连接2'}...
16:50:31 [Thread-3] c.Pool - free MockConnection{name='连接1'}...
16:50:31 [Thread-4] c.Pool - free MockConnection{name='连接2'}...
进程已结束,退出代码 0
7.3.4 final 原理
1. 设置 final 变量的原理
使用 final 关键字修饰变量时,会在对变量执行完写操作后设置写屏障
package com.rui.seven;
public class Test3 {
final int a = 5;
}
final int a;
a = 5;
// 写屏障
防止线程读到 a = 0 的情况
2. 获取 final 变量的原理
说些废话
本篇文章为博主日常学习记录,故而会概率性地存在各种错误,若您在浏览过程中发现一些,请在评论区指正,望我们共同进步,谢谢!