不可变:如果一个对象子不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改。
7.1 不可变类的使用 7.1.1 问题提出 SimpleDateFormat
不是线程安全的,下列代码执行时会产生线程安全问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf4j(topic = "SimpleDateFormat") public class SimpleDateFormatDemo { private static final SimpleDateFormat SDF = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss.SSS" ); public static void main (String[] args) { String dateStr = "2001-09-11 00:00:00.000" ; for (int i = 0 ; i < 10 ; i++) { new Thread (() -> { try { log.debug(SDF.parse(dateStr).toString()); } catch (ParseException e) { e.printStackTrace(); } }).start(); } } }
结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 java.lang.NumberFormatException: multiple points at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890 ) at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110 ) at java.lang.Double.parseDouble(Double.java:538 ) at java.text.DigitList.getDouble(DigitList.java:169 ) at java.text.DecimalFormat.parse(DecimalFormat.java:2089 ) 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 top.parak.immutable.SimpleDateFormatDemo.lambda$main$0 (SimpleDateFormatDemo.java:22 ) at java.lang.Thread.run(Thread.java:748 ) 2021 -04 -29 11 :30 :23.116 [Thread-2 ] DEBUG SimpleDateFormat - Sun Sep 11 00 :00 :00 CST 1121 2021 -04 -29 11 :30 :23.117 [Thread-9 ] DEBUG SimpleDateFormat - Sun Sep 11 00 :00 :00 CST 1121 2021 -04 -29 11 :30 :23.117 [Thread-5 ] DEBUG SimpleDateFormat - Tue Sep 11 00 :00 :00 CST 2001
7.1.2 同步锁 使用同步锁synchronized
能解决安全问题,但是会带来性能问题。
1 2 3 synchronized (SDF) { log.debug(SDF.parse(dateStr).toString()); }
7.1.3 不可变 使用JDK1.8中的不可变日期格式类DateTimeFormatter
。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Slf4j(topic = "DateTimeFormatter") public class DateTimeFormatterDemo { private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS" ); public static void main (String[] args) { String dateStr = "2001-09-11 00:00:00.000" ; for (int i = 0 ; i < 10 ; i++) { new Thread (() -> { TemporalAccessor date = DTF.parse(dateStr); log.debug("{}" , date); }).start(); } } }
7.2 不可变类的设计 7.1.1 final的使用 Integer
、Double
、String
、DateTimeFormatter
以及基本类型包装类,都是用final修饰的。
属性用final修饰保证了该属性是只读的,不能修改
类用final修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
7.2.2 保护性拷贝 以String
的substring
方法为例,方法的最后还是new String
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public String substring (int beginIndex, int endIndex) { if (beginIndex < 0 ) { throw new StringIndexOutOfBoundsException (beginIndex); } if (endIndex > value.length) { throw new StringIndexOutOfBoundsException (endIndex); } int subLen = endIndex - beginIndex; if (subLen < 0 ) { throw new StringIndexOutOfBoundsException (subLen); } return ((beginIndex == 0 ) && (endIndex == value.length)) ? this : new String (value, beginIndex, subLen); }
这种创建副本对象来避免共享的手段称为保护性拷贝(defensive copy)。
7.2.3 模式之享元 定义:运用共享技术来有效地支持大量细粒度对象的复用。
优势:相同对象只保存一份,这降低了系统中对象的数量,降低内存压力。
在JDK中Boolean
、Byte
、Short
、Long
、Character
等包装类提供了valueOf
方法。
例如Longh.valueOf()
,在-128~127之间的Long对象,在这个范围内会用缓存对象,超过这个范围,才会信件Long对象。
1 2 3 4 5 6 7 public static Long valueOf (long l) { final int offset = 128 ; if (l >= -128 && l <= 127 ) { return LongCache.cache[(int )l + offset]; } return new Long (l); }
注意:
Byte
、Short
、Long
缓存的范围:-128~127
Character
缓存的范围:0~127
Integer
的默认范围:-128~127,最小值不能变,最大值通过虚拟机参数-Djava.lang.Integer.IntegerCache.high
来改变。
Boolean
缓存:true / false
7.2.4 DIY连接池 例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 import java.sql.*;import java.util.Map;import java.util.Properties;import java.util.Random;import java.util.concurrent.Executor;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicIntegerArray;public class PoolDemo { public static void main (String[] args) { Pool pool = new Pool (2 ); for (int i = 0 ; i < 5 ; i++) { new Thread (() -> { Connection connection = pool.get(); try { TimeUnit.MILLISECONDS.sleep(new Random ().nextInt(1000 )); } catch (InterruptedException e) { e.printStackTrace(); } pool.free(connection); }, "T-" + (i + 1 )).start(); } } } @Slf4j(topic = "Pool") class Pool { private final int poolSize; private final Connection[] connections; private final AtomicIntegerArray states; public Pool (int pollSize) { this .poolSize = pollSize; connections = new Connection [pollSize]; states = new AtomicIntegerArray (new int [pollSize]); for (int i = 0 ; i < pollSize; i++) { connections[i] = new ParaKConnection ("连接" + (i + 1 )); } } public Connection get () { while (true ) { for (int i = 0 ; i < poolSize; i++) { if (states.get(i) == 0 ) { if (states.compareAndSet(i, 0 , 1 )) { log.debug("get {}" , 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(i, 0 ); synchronized (this ) { log.debug("free {}" , connection); this .notifyAll(); } break ; } } } } class ParaKConnection implements Connection { private String name; public ParaKConnection (String name) { this .name = name; } @Override public String toString () { return "ParaKConnection[" + "name='" + name + '\'' + ']' ; } }
可改进点:
连接的动态增长与收缩
连接保活(可用性检测)
等待超时处理
分布式hash
7.3 final原理 7.3.1 设置final变量的原理 对于以下代码:
1 2 3 public class FinalDemo { final int k = 3 ; }
init字节码如下:
1 2 3 4 5 6 7 0 aload_0 1 invokespecial #1 <java/lang/Object.<init>> 4 aload_0 5 iconst_3 6 putfield #2 <top/parak/immutable/FinalDemo.k> <================== 写屏障 9 return
发现final变量的赋值也会通过putfield指令来完成,同样在这条指令之后也会加入写屏障,保证在其他线程读到它的值时不会出现为0的情况。
7.3.2 获取final变量的原理 对于以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class FinalDemo { int a = 3 ; static int A = 33333 ; final int b = 3 ; final static int B = 33333 ; public static void main (String[] args) { System.out.println(new FinalDemo ().a); System.out.println(A); System.out.println(new FinalDemo ().b); System.out.println(B); } }
main字节码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # 获取打印流 0 getstatic #4 <java/lang/System.out> # 打印a 3 new #5 <top/parak/immutable/FinalDemo> 6 dup 7 invokespecial #6 <top/parak/immutable/FinalDemo.<init>> 10 getfield #2 <top/parak/immutable/FinalDemo.a> 13 invokevirtual #7 <java/io/PrintStream.println> # 打印A 16 getstatic #4 <java/lang/System.out> # 不加final,获取A变量的时候使用getStatic,使用共享内存 19 getstatic #8 <top/parak/immutable/FinalDemo.A> 22 invokevirtual #7 <java/io/PrintStream.println> 25 getstatic #4 <java/lang/System.out> # 打印b 28 new #5 <top/parak/immutable/FinalDemo> 31 dup 32 invokespecial #6 <top/parak/immutable/FinalDemo.<init>> 35 invokevirtual #9 <java/lang/Object.getClass> 38 pop 39 iconst_3 40 invokevirtual #7 <java/io/PrintStream.println> # 打印B 43 getstatic #4 <java/lang/System.out> # 加了final,没有直接去获取A变量,而是将A复制到当前Java虚拟机栈中 46 ldc #10 <33333> 48 invokevirtual #7 <java/io/PrintStream.println> 51 return
通过观察字节码可以发现,final修饰的变量有栈内存读取速度的优化。
7.4 无状态 设计Servlet时为了保证其线程安全,都会有这样的建议,不要为Servlet设置成员变量,这种没有任何成员变量的类是线程安全的。
因为成员变量保存的数据也可以称为无状态信息,因为没有成员变量就称之为【无状态】。