6.1 日期转换的问题
问题提出
下面的代码在运行时,由于SimpleDateFormat不是线程安全的有很大机率出现java.lang.NumberFormatException或者出现不正确的日期解析结果,例如:
public static void test1() throws ParseException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
// 多个线程共享成员变量 calendar并修改
//simpleDateFormat.format()
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
//
log.debug("解析出日期:{}", simpleDateFormat.parse("2022-07-14"));
} catch (Exception e) {
log.error("日期解析出错", e);
}
}).start();
}
}
java.lang.NumberFormatException: For input string: ""
思路-同步锁
虽然可以解决问题,但是带来了性能损耗,并不是一个好的解决方案
public static void test2() throws ParseException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 100; i++) {
new Thread(() -> {
synchronized (simpleDateFormat) {
try {
//
log.debug("解析出日期:{}", simpleDateFormat.parse("2022-07-14"));
} catch (Exception e) {
log.error("日期解析出错", e);
}
}
}).start();
}
}
思路-不可变
如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!这样的对象在
Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类(This class is immutable and thread-safe.):
public static void test3() throws ParseException {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
log.debug("解析出日期:{}", dateTimeFormatter.parse("2022-07-14", LocalDate::from));
} catch (Exception e) {
log.error("日期解析出错", e);
}
}).start();
}
}
不可变对象,实际是另一种避免竞争的方式。
6.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
// ...
}
final 的使用
发现该类、类中所有属性都是 final 的
- 属性用 final 修饰保证了该属性是只读的,不能修改
- 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
保护性拷贝
但有同学会说,使用字符串时,也有一些跟修改相关的方法啊,比如 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);
}
发现其内部是调用 String 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 final char[] value 做出
了修改:
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);
}
6.3 设计模式之享元
6.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
**出自 **“Gang of Four” design patterns
归类 Structual patterns
6.3.2 体现
6.3.2.1 包装类
在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了 valueOf 方法,例如 Long 的
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);
}
:::info
注意:
- Byte, Short, Long 缓存的范围都是 -128~127
- Character 缓存的范围是 0~127
- Integer的默认范围是 -128~127
- 最小值不能变
- 但最大值可以通过调整虚拟机参数
-Djava.lang.Integer.IntegerCache.high
来改变
- Boolean 缓存了 TRUE 和 FALSE
:::
6.3.2.2 String 串池
6.3.2.3 BigDecimal BigInteger
6.3.3 DIY
例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时
预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约
了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
自定义一个简陋的数据库连接池,体会下享元模式(链接的重用)
@Slf4j(topic = "c.ConnectionPool")
public class ConnectionPool {
// 连接池大小
private final int poolSize;
// 连接池对象数组
private Connection[] connections;
// 链接状态数组 0 表示空闲 1 表示繁忙
private AtomicIntegerArray states;
public ConnectionPool(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));
}
}
/**
* 借出链接
*
* @return
*/
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) {
log.debug("wait...");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 归还链接
*
* @param connection
*/
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;
}
}
}
public static void main(String[] args) {
// 生成一个只有两个对象的连接池
ConnectionPool connectionPool = new ConnectionPool(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Connection connection = connectionPool.borrow();
try {
// 借出链接后随机等待一段时间再归还
Thread.sleep(RandomUtil.randomInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
connectionPool.free(connection);
}, "线程" + i).start();
}
}
}
/**
* 写一个假的connection实现
*/
class MockConnection implements Connection {
private String name;
public MockConnection(String name) {
this.name = name;
}
@Override
public String toString() {
return "MockConnection{" +
"name='" + name + '\'' +
'}';
}
// 其他实现略
}
运行代码结果如下:
:::info
21:12:54.318 c.ConnectionPool [线程2] - wait…
21:12:54.318 c.ConnectionPool [线程0] - borrow: MockConnection{name=‘连接:1’}
21:12:54.318 c.ConnectionPool [线程1] - borrow: MockConnection{name=‘连接:2’}
21:12:54.318 c.ConnectionPool [线程3] - wait…
21:12:54.318 c.ConnectionPool [线程4] - wait…
21:12:54.735 c.ConnectionPool [线程1] - free:MockConnection{name=‘连接:2’}
21:12:54.735 c.ConnectionPool [线程3] - wait…
21:12:54.735 c.ConnectionPool [线程4] - borrow: MockConnection{name=‘连接:2’}
21:12:54.735 c.ConnectionPool [线程2] - wait…
21:12:55.070 c.ConnectionPool [线程0] - free:MockConnection{name=‘连接:1’}
21:12:55.070 c.ConnectionPool [线程2] - borrow: MockConnection{name=‘连接:1’}
21:12:55.070 c.ConnectionPool [线程3] - wait…
21:12:55.093 c.ConnectionPool [线程4] - free:MockConnection{name=‘连接:2’}
21:12:55.093 c.ConnectionPool [线程3] - borrow: MockConnection{name=‘连接:2’}
21:12:55.545 c.ConnectionPool [线程3] - free:MockConnection{name=‘连接:2’}
21:12:56.013 c.ConnectionPool [线程2] - free:MockConnection{name=‘连接:1’}
:::
以上实现没有考虑:
- 连接的动态增长与收缩
- 连接保活(可用性检测)
- 等待超时处理(可参考保护性暂停模式)
- 分布式 hash
对于关系型数据库,有比较成熟的连接池实现,例如c3p0, druid等 对于更通用的对象池,可以考虑使用apache
commons pool,例如redis连接池可以参考jedis中关于连接池的实现
6.4 原理之final
6.4.1 设置 final 变量的原理
理解了 volatile 原理,再对比 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: return
发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到
它的值时不会出现为 0 的情况。
6.4.2 获取 final 变量的原理
6.5 无状态
在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的。
因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】