1. 日期转换问题
问题提出,下面的代码在运行时,由于 SimpleDateFormat 不是线程安全的,有很大几率出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果。
@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("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
}
}
15:14:03.787 [Thread-3] ERROR 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:2084)
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.ll.ch6.Test1.lambda$main$0(Test1.java:14)
at java.lang.Thread.run(Thread.java:748)
15:14:03.786 [Thread-4] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:14:03.786 [Thread-2] DEBUG c.Test1 - Mon Apr 21 00:00:00 CST 1119
15:14:03.786 [Thread-9] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:14:03.786 [Thread-5] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:14:03.786 [Thread-7] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:14:03.786 [Thread-1] DEBUG c.Test1 - Mon Apr 21 00:00:00 CST 1119
15:14:03.786 [Thread-0] DEBUG c.Test1 - Mon Apr 21 00:00:00 CST 1119
15:14:03.786 [Thread-6] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:14:03.786 [Thread-8] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
1.1 使用synchronized改进
结果没问题,但是会影响性能
@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("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}
}).start();
}
}
}
15:16:43.154 [Thread-0] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:16:43.162 [Thread-9] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:16:43.162 [Thread-8] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:16:43.162 [Thread-7] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:16:43.163 [Thread-6] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:16:43.163 [Thread-5] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:16:43.163 [Thread-4] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:16:43.163 [Thread-3] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:16:43.164 [Thread-2] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
15:16:43.164 [Thread-1] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
1.2 使用DateTimeFormatter类
不可变对象如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改。这样的对象在 Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类 DateTimeFormatter
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", dateTimeFormatter.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
}
}
15:19:53.996 [Thread-2] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
15:19:53.996 [Thread-9] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
15:19:53.996 [Thread-0] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
15:19:53.996 [Thread-3] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
15:19:53.996 [Thread-4] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
15:19:53.996 [Thread-5] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
15:19:53.996 [Thread-1] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
15:19:53.996 [Thread-8] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
15:19:53.996 [Thread-6] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
15:19:53.996 [Thread-7] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
2. 不可变类的设计
2.1 final的使用
String类中不可变的体现:
- 属性用 final 修饰保证了该属性是只读的,不能修改
- 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
```java
public final class String
implements java.io.Serializable, Comparable
, 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 hash没有提供set方法 // … }
<a name="FRwzZ"></a>
## 2.2 保护性拷贝
- String的这个构造方法会将char的数据拷贝一份之后再赋值,防止使用同一个char数组,外界会对char改变。保证了不可变。
```java
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
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的构造方法创建了一个新字符串
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;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】
3. 享元模式
3.1 简介
简介定义英文名称:Flyweight pattern. 当需要重用数量有限的同一类对象时,归类为:Structual patterns
3.2 体现
包装类
在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了 valueOf 方法。
- 例如 Long 的 valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象:
```java
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
} return new Long(l); }return LongCache.cache[(int)l + offset];
注意:
- Byte, Short, Long 缓存的范围都是 -128~127
- Character 缓存的范围是 0~127
- Integer 的默认范围是 -128~127,最小值不能变,但最大值可以通过调整虚拟机参数 "-Djava.lang.Integer.IntegerCache.high "来改变
- Boolean 缓存了 TRUE 和 FALSE
**字符串常量池**<br />**BigDecimal、BigInteger**
<a name="BVvwP"></a>
## 3.3 实现简单的数据库连接池
例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
```java
@Slf4j(topic = "c.Test2")
public class Test2 {
public static void main(String[] args) {
Pool pool = new Pool(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Connection connection = pool.borrow();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.free(connection);
}).start();
}
}
}
@Slf4j(topic = "c.Pool")
class Pool {
// 连接池大小
private int poolSize;
// 连接对象数组
private Connection[] connections;
// 连接状态数组,0表示空闲,1表示繁忙
private AtomicIntegerArray states;
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));
}
}
// 借连接
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) {
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(1, 0);
// 唤醒等待线程
synchronized (this) {
log.debug("free,{}", connection);
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 + '\'' +
'}';
}
// ...其他override方法
}
16:17:17.992 [Thread-1] DEBUG c.Pool - wait
16:17:17.992 [Thread-0] DEBUG c.Pool - borrow ,MockConnection{name='连接1'}
16:17:17.996 [Thread-4] DEBUG c.Pool - wait
16:17:17.996 [Thread-3] DEBUG c.Pool - wait
16:17:17.997 [Thread-2] DEBUG c.Pool - wait
16:17:18.361 [Thread-0] DEBUG c.Pool - free,MockConnection{name='连接1'}
16:17:18.361 [Thread-2] DEBUG c.Pool - borrow ,MockConnection{name='连接2'}
16:17:18.361 [Thread-3] DEBUG c.Pool - wait
16:17:18.361 [Thread-4] DEBUG c.Pool - wait
16:17:18.361 [Thread-1] DEBUG c.Pool - wait
16:17:18.745 [Thread-2] DEBUG c.Pool - free,MockConnection{name='连接2'}
16:17:18.745 [Thread-1] DEBUG c.Pool - wait
16:17:18.746 [Thread-4] DEBUG c.Pool - wait
16:17:18.746 [Thread-3] DEBUG c.Pool - wait
以上实现没有考虑:
- 连接的动态增长与收缩
- 连接保活(可用性检测)
- 等待超时处理
- 分布式 hash
对于关系型数据库,有比较成熟的连接池的实现,例如 c3p0、druid 等
对于更通用的对象池,可以考虑用 apache commons pool,例如 redis 连接池可以参考 jedis 中关于连接池的实现。
4. final原理
4.1 设置 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 变量的赋值操作都必须在定义时或者构造器中进行初始化赋值,并发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况。
4.2 获取 final 变量的原理
5. 结论
- 不可变类使用
- 不可变类设计
- 原理方面:final
- 模式方面
- 享元模式-> 设置线程池