【六】多线程 —— 共享模型之不可变
如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改。
比如类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性。
一、日期转换的问题
SimpleDateFormat
不是线程安全的,可能出现 java.lang.NumberFormatException
或者出现不正确的日期解析结果,例如:
public class SimpleDateFormatTest {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
ConsoleUtil.print(sdf.parse("1951-04-21").toString());
} catch (Exception e) {
ConsoleUtil.print(e.toString());
}
}).start();
}
}
}
2021-08-03 14:34:55 Sat Jul 21 00:00:00 CST 1951
2021-08-03 14:34:55 java.lang.NumberFormatException: For input string: "19511951195119511951195119511951.EE441951195119511951195119511951195119511951E41951195119511951195119511951195119511E4"
2021-08-03 14:34:55 Mon Jul 21 00:00:00 CST 178958921
2021-08-03 14:34:55 Sat Jul 21 00:00:00 CST 1951
2021-08-03 14:34:55 java.lang.NumberFormatException: empty String
2021-08-03 14:34:55 java.lang.NumberFormatException: For input string: "19511951195119511951195119511951.EE441951195119511951195119511951195119511951E4"
2021-08-03 14:34:55 Sat Jul 21 00:00:00 CST 1951
2021-08-03 14:34:55 java.lang.NumberFormatException: multiple points
2021-08-03 14:34:55 java.lang.NumberFormatException: For input string: "19511951195119511951195119511951.EE441951195119511951195119511951195119511951"
2021-08-03 14:34:55 java.lang.NumberFormatException: multiple points
使用同步锁虽能解决问题,但带来的是性能上的损失,并不算很好, 加锁耗性能。
如果一个对象在不能够修改其内部状态(属性),因为不存在并发修改,那么它就是线程安全的,这样的对象在Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:
public class SimpleDateFormatTest01 {
public static void main(String[] args) {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
ConsoleUtil.print(dtf.parse("1951-04-21").toString());
} catch (Exception e) {
ConsoleUtil.print(e.toString());
}
}).start();
}
}
}
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
2021-08-03 14:38:16 {},ISO resolved to 1951-04-21
可以看 DateTimeFormatter
的文档:
二、final 的使用
Integer
、Double
、String
、DateTimeFormatter
、String
以及基本类型包装类, 都是使用final来修饰的。
属性用 final
修饰保证了该属性是只读的,不能修改,类用 final
修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性。
final原理
public class TestFinal {
final int a = 20;
}
字节码:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 20
7: putfield #2 // Field a:I
<-- 写屏障
10: retu
final
变量的赋值通过 putfield
指令来完成,在这条指令之后会加入写屏障
,保证在其它线程读到它的值时不会出现为 0 的情况。
三、实现一个简单的连接池
一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。
这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
/**
* Description: 简易连接池
*
* @author guizy
* @date 2020/12/29 21:21
*/
public class Test2 {
public static void main(String[] args) {
/*使用连接池*/
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();
}
}
}
@Slf4j(topic = "guizy.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]);//使用AtomicIntegerArray保证states的线程安全
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)) {//使用compareAndSet保证线程安全
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
// 如果没有空闲连接,当前线程进入等待, 如果不写这个synchronized,其他线程不会进行等待,
// 一直在上面while(true), 空转, 消耗cpu资源
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 {
private String name;
public MockConnection(String name) {
this.name = name;
}
@Override
public String toString() {
return "MockConnection{" +
"name='" + name + '\'' +
'}';
}
// Connection 实现方法略
}
2021-08-03 15:01:01 wait...
2021-08-03 15:01:01 borrowMockConnection{name='连接2'}
2021-08-03 15:01:01 borrowMockConnection{name='连接1'}
2021-08-03 15:01:02 wait...
2021-08-03 15:01:02 wait...
2021-08-03 15:01:02 freeMockConnection{name='连接1'}
2021-08-03 15:01:02 borrowMockConnection{name='连接1'}
2021-08-03 15:01:02 wait...
2021-08-03 15:01:02 wait...
2021-08-03 15:01:02 freeMockConnection{name='连接1'}
2021-08-03 15:01:02 borrowMockConnection{name='连接1'}
2021-08-03 15:01:02 wait...
2021-08-03 15:01:02 freeMockConnection{name='连接2'}
2021-08-03 15:01:02 borrowMockConnection{name='连接2'}
2021-08-03 15:01:03 freeMockConnection{name='连接2'}
2021-08-03 15:01:03 freeMockConnection{name='连接1'}