不可变:如果一个对象子不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改。
7.1 不可变类的使用
7.1.1 问题提出
SimpleDateFormat不是线程安全的,下列代码执行时会产生线程安全问题:
@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();}}}
结果:
java.lang.NumberFormatException: multiple pointsat 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 11212021-04-29 11:30:23.117 [Thread-9] DEBUG SimpleDateFormat - Sun Sep 11 00:00:00 CST 11212021-04-29 11:30:23.117 [Thread-5] DEBUG SimpleDateFormat - Tue Sep 11 00:00:00 CST 2001
7.1.2 同步锁
使用同步锁synchronized能解决安全问题,但是会带来性能问题。
synchronized (SDF) {log.debug(SDF.parse(dateStr).toString());}
7.1.3 不可变
使用JDK1.8中的不可变日期格式类DateTimeFormatter。
@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
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对象。
public static Long valueOf(long l) {final int offset = 128;if (l >= -128 && l <= 127) { // will cachereturn LongCache.cache[(int)l + offset];}return new Long(l);}
注意:
Byte、Short、Long缓存的范围:-128~127Character缓存的范围:0~127Integer的默认范围:-128~127,最小值不能变,最大值通过虚拟机参数-Djava.lang.Integer.IntegerCache.high来改变。Boolean缓存:true / false
7.2.4 DIY连接池
例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
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;/*** @author KHighness* @since 2021-04-29*/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;}@Overridepublic String toString() {return "ParaKConnection[" +"name='" + name + '\'' +']';}// ...}
可改进点:
- 连接的动态增长与收缩
- 连接保活(可用性检测)
- 等待超时处理
- 分布式hash
7.3 final原理
7.3.1 设置final变量的原理
对于以下代码:
public class FinalDemo {final int k = 3;}
init字节码如下:
0 aload_01 invokespecial #1 <java/lang/Object.<init>>4 aload_05 iconst_36 putfield #2 <top/parak/immutable/FinalDemo.k><================== 写屏障9 return
发现final变量的赋值也会通过putfield指令来完成,同样在这条指令之后也会加入写屏障,保证在其他线程读到它的值时不会出现为0的情况。
7.3.2 获取final变量的原理
对于以下代码:
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字节码如下:
# 获取打印流0 getstatic #4 <java/lang/System.out># 打印a3 new #5 <top/parak/immutable/FinalDemo>6 dup7 invokespecial #6 <top/parak/immutable/FinalDemo.<init>>10 getfield #2 <top/parak/immutable/FinalDemo.a>13 invokevirtual #7 <java/io/PrintStream.println># 打印A16 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># 打印b28 new #5 <top/parak/immutable/FinalDemo>31 dup32 invokespecial #6 <top/parak/immutable/FinalDemo.<init>>35 invokevirtual #9 <java/lang/Object.getClass>38 pop39 iconst_340 invokevirtual #7 <java/io/PrintStream.println># 打印B43 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设置成员变量,这种没有任何成员变量的类是线程安全的。
因为成员变量保存的数据也可以称为无状态信息,因为没有成员变量就称之为【无状态】。
