1. 日期转换问题

问题提出,下面的代码在运行时,由于 SimpleDateFormat 不是线程安全的,有很大几率出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果。

  1. @Slf4j(topic = "c.Test1")
  2. public class Test1 {
  3. public static void main(String[] args) {
  4. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  5. for (int i = 0; i < 10; i++) {
  6. new Thread(() -> {
  7. try {
  8. log.debug("{}", sdf.parse("1951-04-21"));
  9. } catch (Exception e) {
  10. log.error("{}", e);
  11. }
  12. }).start();
  13. }
  14. }
  15. }
  1. 15:14:03.787 [Thread-3] ERROR c.Test1 - {}
  2. java.lang.NumberFormatException: For input string: ""
  3. at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
  4. at java.lang.Long.parseLong(Long.java:601)
  5. at java.lang.Long.parseLong(Long.java:631)
  6. at java.text.DigitList.getLong(DigitList.java:195)
  7. at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
  8. at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
  9. at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
  10. at java.text.DateFormat.parse(DateFormat.java:364)
  11. at com.ll.ch6.Test1.lambda$main$0(Test1.java:14)
  12. at java.lang.Thread.run(Thread.java:748)
  13. 15:14:03.786 [Thread-4] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  14. 15:14:03.786 [Thread-2] DEBUG c.Test1 - Mon Apr 21 00:00:00 CST 1119
  15. 15:14:03.786 [Thread-9] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  16. 15:14:03.786 [Thread-5] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  17. 15:14:03.786 [Thread-7] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  18. 15:14:03.786 [Thread-1] DEBUG c.Test1 - Mon Apr 21 00:00:00 CST 1119
  19. 15:14:03.786 [Thread-0] DEBUG c.Test1 - Mon Apr 21 00:00:00 CST 1119
  20. 15:14:03.786 [Thread-6] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  21. 15:14:03.786 [Thread-8] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951

1.1 使用synchronized改进

结果没问题,但是会影响性能

  1. @Slf4j(topic = "c.Test1")
  2. public class Test1 {
  3. public static void main(String[] args) {
  4. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  5. for (int i = 0; i < 10; i++) {
  6. new Thread(() -> {
  7. synchronized (sdf){
  8. try {
  9. log.debug("{}", sdf.parse("1951-04-21"));
  10. } catch (Exception e) {
  11. log.error("{}", e);
  12. }
  13. }
  14. }).start();
  15. }
  16. }
  17. }
  1. 15:16:43.154 [Thread-0] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  2. 15:16:43.162 [Thread-9] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  3. 15:16:43.162 [Thread-8] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  4. 15:16:43.162 [Thread-7] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  5. 15:16:43.163 [Thread-6] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  6. 15:16:43.163 [Thread-5] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  7. 15:16:43.163 [Thread-4] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  8. 15:16:43.163 [Thread-3] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  9. 15:16:43.164 [Thread-2] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951
  10. 15:16:43.164 [Thread-1] DEBUG c.Test1 - Sat Apr 21 00:00:00 CST 1951

1.2 使用DateTimeFormatter类

不可变对象如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改。这样的对象在 Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类 DateTimeFormatter

  1. @Slf4j(topic = "c.Test1")
  2. public class Test1 {
  3. public static void main(String[] args) {
  4. DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
  5. for (int i = 0; i < 10; i++) {
  6. new Thread(() -> {
  7. try {
  8. log.debug("{}", dateTimeFormatter.parse("1951-04-21"));
  9. } catch (Exception e) {
  10. log.error("{}", e);
  11. }
  12. }).start();
  13. }
  14. }
  15. }
  1. 15:19:53.996 [Thread-2] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
  2. 15:19:53.996 [Thread-9] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
  3. 15:19:53.996 [Thread-0] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
  4. 15:19:53.996 [Thread-3] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
  5. 15:19:53.996 [Thread-4] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
  6. 15:19:53.996 [Thread-5] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
  7. 15:19:53.996 [Thread-1] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
  8. 15:19:53.996 [Thread-8] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
  9. 15:19:53.996 [Thread-6] DEBUG c.Test1 - {},ISO resolved to 1951-04-21
  10. 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方法 // … }
  1. <a name="FRwzZ"></a>
  2. ## 2.2 保护性拷贝
  3. - String的这个构造方法会将char的数据拷贝一份之后再赋值,防止使用同一个char数组,外界会对char改变。保证了不可变。
  4. ```java
  5. public String(char value[]) {
  6. this.value = Arrays.copyOf(value, value.length);
  7. }
  • substring方法:

    1. public String substring(int beginIndex) {
    2. if (beginIndex < 0) {
    3. throw new StringIndexOutOfBoundsException(beginIndex);
    4. }
    5. int subLen = value.length - beginIndex;
    6. if (subLen < 0) {
    7. throw new StringIndexOutOfBoundsException(subLen);
    8. }
    9. return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    10. }

    其内部调用String的构造方法创建了一个新字符串

    1. public String(char value[], int offset, int count) {
    2. if (offset < 0) {
    3. throw new StringIndexOutOfBoundsException(offset);
    4. }
    5. if (count <= 0) {
    6. if (count < 0) {
    7. throw new StringIndexOutOfBoundsException(count);
    8. }
    9. if (offset <= value.length) {
    10. this.value = "".value;
    11. return;
    12. }
    13. }
    14. // Note: offset or count might be near -1>>>1.
    15. if (offset > value.length - count) {
    16. throw new StringIndexOutOfBoundsException(offset + count);
    17. }
    18. this.value = Arrays.copyOfRange(value, offset, offset+count);
    19. }

    构造新字符串对象时,会生成新的 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
    1. return LongCache.cache[(int)l + offset];
    } return new Long(l); }
  1. 注意:
  2. - Byte, Short, Long 缓存的范围都是 -128~127
  3. - Character 缓存的范围是 0~127
  4. - Integer 的默认范围是 -128~127,最小值不能变,但最大值可以通过调整虚拟机参数 "-Djava.lang.Integer.IntegerCache.high "来改变
  5. - Boolean 缓存了 TRUE FALSE
  6. **字符串常量池**<br />**BigDecimalBigInteger**
  7. <a name="BVvwP"></a>
  8. ## 3.3 实现简单的数据库连接池
  9. 例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
  10. ```java
  11. @Slf4j(topic = "c.Test2")
  12. public class Test2 {
  13. public static void main(String[] args) {
  14. Pool pool = new Pool(2);
  15. for (int i = 0; i < 5; i++) {
  16. new Thread(() -> {
  17. Connection connection = pool.borrow();
  18. try {
  19. Thread.sleep(new Random().nextInt(1000));
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. pool.free(connection);
  24. }).start();
  25. }
  26. }
  27. }
  28. @Slf4j(topic = "c.Pool")
  29. class Pool {
  30. // 连接池大小
  31. private int poolSize;
  32. // 连接对象数组
  33. private Connection[] connections;
  34. // 连接状态数组,0表示空闲,1表示繁忙
  35. private AtomicIntegerArray states;
  36. public Pool(int poolSize) {
  37. this.poolSize = poolSize;
  38. this.connections = new Connection[poolSize];
  39. this.states = new AtomicIntegerArray(new int[poolSize]);
  40. for (int i = 0; i < poolSize; i++) {
  41. connections[i] = new MockConnection("连接" + (i + 1));
  42. }
  43. }
  44. // 借连接
  45. public Connection borrow() {
  46. while (true) {
  47. for (int i = 0; i < poolSize; i++) {
  48. // 获取空闲连接
  49. if (states.get(i) == 0) {
  50. if (states.compareAndSet(i, 0, 1)) {
  51. log.debug("borrow ,{}", connections[i]);
  52. return connections[i];
  53. }
  54. }
  55. // 如果没有空闲连接,当前线程进入等待
  56. synchronized (this) {
  57. try {
  58. log.debug("wait");
  59. this.wait();
  60. } catch (InterruptedException e) {
  61. e.printStackTrace();
  62. }
  63. }
  64. }
  65. }
  66. }
  67. // 还连接
  68. public void free(Connection connection) {
  69. for (int i = 0; i < poolSize; i++) {
  70. if (connections[i] == connection) {
  71. states.set(1, 0);
  72. // 唤醒等待线程
  73. synchronized (this) {
  74. log.debug("free,{}", connection);
  75. this.notifyAll();
  76. }
  77. break;
  78. }
  79. }
  80. }
  81. }
  82. class MockConnection implements Connection {
  83. private String name;
  84. public MockConnection(String name) {
  85. this.name = name;
  86. }
  87. @Override
  88. public String toString() {
  89. return "MockConnection{" +
  90. "name='" + name + '\'' +
  91. '}';
  92. }
  93. // ...其他override方法
  94. }
  1. 16:17:17.992 [Thread-1] DEBUG c.Pool - wait
  2. 16:17:17.992 [Thread-0] DEBUG c.Pool - borrow ,MockConnection{name='连接1'}
  3. 16:17:17.996 [Thread-4] DEBUG c.Pool - wait
  4. 16:17:17.996 [Thread-3] DEBUG c.Pool - wait
  5. 16:17:17.997 [Thread-2] DEBUG c.Pool - wait
  6. 16:17:18.361 [Thread-0] DEBUG c.Pool - free,MockConnection{name='连接1'}
  7. 16:17:18.361 [Thread-2] DEBUG c.Pool - borrow ,MockConnection{name='连接2'}
  8. 16:17:18.361 [Thread-3] DEBUG c.Pool - wait
  9. 16:17:18.361 [Thread-4] DEBUG c.Pool - wait
  10. 16:17:18.361 [Thread-1] DEBUG c.Pool - wait
  11. 16:17:18.745 [Thread-2] DEBUG c.Pool - free,MockConnection{name='连接2'}
  12. 16:17:18.745 [Thread-1] DEBUG c.Pool - wait
  13. 16:17:18.746 [Thread-4] DEBUG c.Pool - wait
  14. 16:17:18.746 [Thread-3] DEBUG c.Pool - wait

以上实现没有考虑:

  • 连接的动态增长与收缩
  • 连接保活(可用性检测)
  • 等待超时处理
  • 分布式 hash

对于关系型数据库,有比较成熟的连接池的实现,例如 c3p0、druid 等
对于更通用的对象池,可以考虑用 apache commons pool,例如 redis 连接池可以参考 jedis 中关于连接池的实现。

4. final原理

4.1 设置 final 变量的原理

  1. public class TestFinal {
  2. final int a = 20;
  3. }

字节码:

  1. 0: aload_0
  2. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  3. 4: aload_0
  4. 5: bipush 20
  5. 7: putfield #2 // Field a:I
  6. <-- 写屏障
  7. 10: return

final 变量的赋值操作都必须在定义时或者构造器中进行初始化赋值,并发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况。

4.2 获取 final 变量的原理

字节码角度,final是ldc

5. 结论

  • 不可变类使用
  • 不可变类设计
  • 原理方面:final
  • 模式方面
    • 享元模式-> 设置线程池