不可变:如果一个对象子不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改。

7.1 不可变类的使用

7.1.1 问题提出

SimpleDateFormat不是线程安全的,下列代码执行时会产生线程安全问题:

  1. @Slf4j(topic = "SimpleDateFormat")
  2. public class SimpleDateFormatDemo {
  3. private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
  4. public static void main(String[] args) {
  5. String dateStr = "2001-09-11 00:00:00.000";
  6. for (int i = 0; i < 10; i++) {
  7. new Thread(() -> {
  8. try {
  9. log.debug(SDF.parse(dateStr).toString());
  10. } catch (ParseException e) {
  11. e.printStackTrace();
  12. }
  13. }).start();
  14. }
  15. }
  16. }

结果:

  1. java.lang.NumberFormatException: multiple points
  2. at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
  3. at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
  4. at java.lang.Double.parseDouble(Double.java:538)
  5. at java.text.DigitList.getDouble(DigitList.java:169)
  6. at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
  7. at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
  8. at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
  9. at java.text.DateFormat.parse(DateFormat.java:364)
  10. at top.parak.immutable.SimpleDateFormatDemo.lambda$main$0(SimpleDateFormatDemo.java:22)
  11. at java.lang.Thread.run(Thread.java:748)
  12. 2021-04-29 11:30:23.116 [Thread-2] DEBUG SimpleDateFormat - Sun Sep 11 00:00:00 CST 1121
  13. 2021-04-29 11:30:23.117 [Thread-9] DEBUG SimpleDateFormat - Sun Sep 11 00:00:00 CST 1121
  14. 2021-04-29 11:30:23.117 [Thread-5] DEBUG SimpleDateFormat - Tue Sep 11 00:00:00 CST 2001

7.1.2 同步锁

使用同步锁synchronized能解决安全问题,但是会带来性能问题。

  1. synchronized (SDF) {
  2. log.debug(SDF.parse(dateStr).toString());
  3. }

7.1.3 不可变

使用JDK1.8中的不可变日期格式类DateTimeFormatter

  1. @Slf4j(topic = "DateTimeFormatter")
  2. public class DateTimeFormatterDemo {
  3. private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
  4. public static void main(String[] args) {
  5. String dateStr = "2001-09-11 00:00:00.000";
  6. for (int i = 0; i < 10; i++) {
  7. new Thread(() -> {
  8. TemporalAccessor date = DTF.parse(dateStr);
  9. log.debug("{}", date);
  10. }).start();
  11. }
  12. }
  13. }

7.2 不可变类的设计

7.1.1 final的使用

IntegerDoubleStringDateTimeFormatter以及基本类型包装类,都是用final修饰的。

  • 属性用final修饰保证了该属性是只读的,不能修改
  • 类用final修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

7.2.2 保护性拷贝

Stringsubstring方法为例,方法的最后还是new String

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

这种创建副本对象来避免共享的手段称为保护性拷贝(defensive copy)。

7.2.3 模式之享元

定义:运用共享技术来有效地支持大量细粒度对象的复用。

优势:相同对象只保存一份,这降低了系统中对象的数量,降低内存压力。

在JDK中BooleanByteShortLongCharacter等包装类提供了valueOf方法。

例如Longh.valueOf(),在-128~127之间的Long对象,在这个范围内会用缓存对象,超过这个范围,才会信件Long对象。

  1. public static Long valueOf(long l) {
  2. final int offset = 128;
  3. if (l >= -128 && l <= 127) { // will cache
  4. return LongCache.cache[(int)l + offset];
  5. }
  6. return new Long(l);
  7. }

注意:

  • ByteShortLong缓存的范围:-128~127
  • Character缓存的范围:0~127
  • Integer的默认范围:-128~127,最小值不能变,最大值通过虚拟机参数-Djava.lang.Integer.IntegerCache.high来改变。
  • Boolean缓存:true / false

7.2.4 DIY连接池

例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。

  1. import java.sql.*;
  2. import java.util.Map;
  3. import java.util.Properties;
  4. import java.util.Random;
  5. import java.util.concurrent.Executor;
  6. import java.util.concurrent.TimeUnit;
  7. import java.util.concurrent.atomic.AtomicIntegerArray;
  8. /**
  9. * @author KHighness
  10. * @since 2021-04-29
  11. */
  12. public class PoolDemo {
  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.get();
  18. try {
  19. TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. pool.free(connection);
  24. }, "T-" + (i + 1)).start();
  25. }
  26. }
  27. }
  28. @Slf4j(topic = "Pool")
  29. class Pool {
  30. /**
  31. * 连接池大小
  32. */
  33. private final int poolSize;
  34. /**
  35. * 连接数组
  36. */
  37. private final Connection[] connections;
  38. /**
  39. * 连接状态数组
  40. */
  41. private final AtomicIntegerArray states;
  42. /**
  43. * 初始化连接池
  44. */
  45. public Pool(int pollSize) {
  46. this.poolSize = pollSize;
  47. connections = new Connection[pollSize];
  48. states = new AtomicIntegerArray(new int[pollSize]);
  49. for (int i = 0; i < pollSize; i++) {
  50. connections[i] = new ParaKConnection("连接" + (i + 1));
  51. }
  52. }
  53. /**
  54. * 获取一个连接
  55. */
  56. public Connection get() {
  57. while (true) {
  58. // 查看是否有空闲连接
  59. for (int i = 0; i < poolSize; i++) {
  60. if (states.get(i) == 0) {
  61. if (states.compareAndSet(i, 0, 1)) {
  62. log.debug("get {}", connections[i]);
  63. return connections[i];
  64. }
  65. }
  66. }
  67. // 没有空闲连接则等待
  68. synchronized (this) {
  69. try {
  70. log.debug("wait...");
  71. this.wait();
  72. } catch (InterruptedException e) {
  73. e.printStackTrace();
  74. }
  75. }
  76. }
  77. }
  78. /**
  79. * 释放一个连接
  80. */
  81. public void free(Connection connection) {
  82. for (int i = 0; i < poolSize; i++) {
  83. if (connections[i] == connection) {
  84. states.set(i, 0);
  85. synchronized (this) {
  86. log.debug("free {}", connection);
  87. this.notifyAll();
  88. }
  89. break;
  90. }
  91. }
  92. }
  93. }
  94. class ParaKConnection implements Connection {
  95. private String name;
  96. public ParaKConnection(String name) {
  97. this.name = name;
  98. }
  99. @Override
  100. public String toString() {
  101. return "ParaKConnection[" +
  102. "name='" + name + '\'' +
  103. ']';
  104. }
  105. // ...
  106. }

可改进点:

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

7.3 final原理

7.3.1 设置final变量的原理

对于以下代码:

  1. public class FinalDemo {
  2. final int k = 3;
  3. }

init字节码如下:

  1. 0 aload_0
  2. 1 invokespecial #1 <java/lang/Object.<init>>
  3. 4 aload_0
  4. 5 iconst_3
  5. 6 putfield #2 <top/parak/immutable/FinalDemo.k>
  6. <================== 写屏障
  7. 9 return

发现final变量的赋值也会通过putfield指令来完成,同样在这条指令之后也会加入写屏障,保证在其他线程读到它的值时不会出现为0的情况。

7.3.2 获取final变量的原理

对于以下代码:

  1. public class FinalDemo {
  2. int a = 3;
  3. static int A = 33333;
  4. final int b = 3;
  5. final static int B = 33333;
  6. public static void main(String[] args) {
  7. System.out.println(new FinalDemo().a);
  8. System.out.println(A);
  9. System.out.println(new FinalDemo().b);
  10. System.out.println(B);
  11. }
  12. }

main字节码如下:

  1. # 获取打印流
  2. 0 getstatic #4 <java/lang/System.out>
  3. # 打印a
  4. 3 new #5 <top/parak/immutable/FinalDemo>
  5. 6 dup
  6. 7 invokespecial #6 <top/parak/immutable/FinalDemo.<init>>
  7. 10 getfield #2 <top/parak/immutable/FinalDemo.a>
  8. 13 invokevirtual #7 <java/io/PrintStream.println>
  9. # 打印A
  10. 16 getstatic #4 <java/lang/System.out>
  11. # 不加final,获取A变量的时候使用getStatic,使用共享内存
  12. 19 getstatic #8 <top/parak/immutable/FinalDemo.A>
  13. 22 invokevirtual #7 <java/io/PrintStream.println>
  14. 25 getstatic #4 <java/lang/System.out>
  15. # 打印b
  16. 28 new #5 <top/parak/immutable/FinalDemo>
  17. 31 dup
  18. 32 invokespecial #6 <top/parak/immutable/FinalDemo.<init>>
  19. 35 invokevirtual #9 <java/lang/Object.getClass>
  20. 38 pop
  21. 39 iconst_3
  22. 40 invokevirtual #7 <java/io/PrintStream.println>
  23. # 打印B
  24. 43 getstatic #4 <java/lang/System.out>
  25. # 加了final,没有直接去获取A变量,而是将A复制到当前Java虚拟机栈中
  26. 46 ldc #10 <33333>
  27. 48 invokevirtual #7 <java/io/PrintStream.println>
  28. 51 return

通过观察字节码可以发现,final修饰的变量有栈内存读取速度的优化。

7.4 无状态

设计Servlet时为了保证其线程安全,都会有这样的建议,不要为Servlet设置成员变量,这种没有任何成员变量的类是线程安全的。

因为成员变量保存的数据也可以称为无状态信息,因为没有成员变量就称之为【无状态】。