共享模型之不可变 发表于 2022-05-25 更新于 2025-07-26
长沙
共享模型之不可变 John Doe 2022-05-25 2025-07-26 共享模型之不可变
1. 日期转换的问题 1. 问题提出 下面的代码在运行时,由于 SimpleDateFormat 不是线程安全的
1 2 3 4 5 6 7 8 9 10 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(); }
有很大几率出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果,例如:
1 2 3 4 5 6 7 8 9 10 11 12 java.lang.NumberFormatException: empty String at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842 ) 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:2162 ) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514 ) at java.text.DateFormat.parse(DateFormat.java:364 ) at cn.itcast.n7.TestDateParse.lambda$test1$0 (TestDateParse.java:18 ) at java.lang.Thread.run(Thread.java:748 ) 19 :10 :40.857 [Thread-8 ] c.TestDateParse - Sat Apr 21 00 :00 :00 CST 1951
2. 思路 - 同步锁 这样虽能解决问题,但带来的是性能上的损失,并不算很好:
1 2 3 4 5 6 7 8 9 10 11 12 SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd" );for (int i = 0 ; i < 50 ; i++) { new Thread (() -> { synchronized (sdf) { try { log.debug("{}" , sdf.parse("1951-04-21" )); } catch (Exception e) { log.error("{}" , e); } } }).start(); }
3. 思路 - 不可变 如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!这样的对象在 Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:
1 2 3 4 5 6 7 DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd" );for (int i = 0 ; i < 10 ; i++) { new Thread (() -> { LocalDate date = dtf.parse("2018-10-01" , LocalDate::from); log.debug("{}" , date); }).start(); }
可以看 DateTimeFormatter 的文档:
1 This class is immutable and thread-safe
不可变对象,实际是另一种避免竞争的方式。
2. 不可变设计 另一个大家更为熟悉的 String 类也是不可变的,以它为例,说明一下不可变设计的要素
1 2 3 4 5 6 7 8 9 10 public final class String implements java .io.Serializable, Comparable<String>, CharSequence { private final char value[]; private int hash; }
1. final 的使用 发现该类、类中所有属性都是 final 的
属性用 final 修饰保证了该属性是只读的,不能修改
类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
2. 保护性拷贝 但有同学会说,使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是 如何实现的,就以 substring 为例:
1 2 3 4 5 6 7 8 9 10 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 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 final char[] value 做出 了修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 ; } } if (offset > value.length - count) { throw new StringIndexOutOfBoundsException (offset + count); } this .value = Arrays.copyOfRange(value, offset, offset+count); }
结果发现也没有,构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避 免共享的手段称之为【保护性拷贝(defensive copy) 】
3. 享元模式 🍁 1. 简介 **定义 **英文名称:Flyweight pattern. 当需要重用数量有限的同一类对象时
wikipedia: A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects
出自 “Gang of Four” design patterns
归类 Structual patterns
2. 体现 1. 包装类
在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了 valueOf 方法,例如 Long 的 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
2. String 串池
1 2 3 4 5 6 7 8 9 10 public final class String implements java .io.Serializable, Comparable<String>, CharSequence { private final char value[]; private int hash; }
3. BigDecimal BigInteger
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private static BigDecimal add (BigInteger fst, int scale1, BigInteger snd, int scale2) { int rscale = scale1; long sdiff = (long )rscale - scale2; if (sdiff != 0 ) { if (sdiff < 0 ) { int raise = checkScale(fst,-sdiff); rscale = scale2; fst = bigMultiplyPowerTen(fst,raise); } else { int raise = checkScale(snd,sdiff); snd = bigMultiplyPowerTen(snd,raise); } } BigInteger sum = fst.add(snd); return (fst.signum == snd.signum) ? new BigDecimal (sum, INFLATED, rscale, 0 ) : valueOf(sum, rscale, 0 ); }
3. 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 class Pool { private final int poolSize; private Connection[] connections; 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 conn) { for (int i = 0 ; i < poolSize; i++) { if (connections[i] == conn) { states.set(i, 0 ); synchronized (this ) { log.debug("free {}" , conn); 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 + '\'' + '}' ; } }
使用连接池:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Test3 { public static void main (String[] args) { Pool pool = new Pool (2 ); for (int i = 0 ; i < 5 ; i++) { new Thread (() -> { Connection conn = pool.borrow(); try { Thread.sleep(new Random ().nextInt(1000 )); } catch (InterruptedException e) { e.printStackTrace(); } pool.free(conn); }).start(); } } }
以上实现没有考虑:
连接的动态增长与收缩
连接保活(可用性检测)
等待超时处理
分布式 hash
对于关系型数据库,有比较成熟的连接池实现,例如c3p0, druid等,对于更通用的对象池,可以考虑使用apache commons pool,例如redis连接池可以参考jedis中关于连接池的实现
4. final 原理 1. 设置 final 变量的原理 理解了 volatile 原理,再对比 final 的实现就比较简单了
1 2 3 public class TestFinal { final int a = 20 ; }
字节码
1 2 3 4 5 6 7 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: retur
发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到 它的值时不会出现为 0 的情况
2. 获取 final 变量的原理 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 public class TestFinal { static int A = 10 ; static int B = Short.MAX_VALUE+1 ; final int a = 20 ; final int b = Integer.MAX_VALUE; final void test1 () { final int c = 30 ; new Thread (()->{ System.out.println(c); }).start(); final int d = 30 ; class Task implements Runnable { @Override public void run () { System.out.println(d); } } new Thread (new Task ()).start(); } } class UseFinal1 { public void test () { System.out.println(TestFinal.A); System.out.println(TestFinal.B); System.out.println(new TestFinal ().a); System.out.println(new TestFinal ().b); new TestFinal ().test1(); } } class UseFinal2 { public void test () { System.out.println(TestFinal.A); } }
3. 无状态 在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这 种没有任何成员变量的类是线程安全的
因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】
4. 小结