开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第30天,点击查看活动详情
亨元模式
简介
定义 英文名称: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
体现
包装类
在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了 valueOf 方法,例如 Long 的 valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对 象:
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
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
String 串池(不可变、线程安全)
详见jvm
BigDecimal BigInteger(不可变、线程安全)
一部分数字使用了享元模式进行了缓存。
手动实现一个连接池
例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时 预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约 了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
class Pool {
// 1. 连接池大小
private final int poolSize;
// 2. 连接对象数组
private Connection[] connections;
// 3. 连接状态数组 0 表示空闲, 1 表示繁忙
private AtomicIntegerArray states;
// 4. 构造方法初始化
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));
}
}
// 5. 借连接
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();
}
}
}
}
// 6. 归还连接
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 {
// 实现略
}
使用连接池:
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中关于连接池的实现。
原理之 final
设置 final 变量的原理
理解了 volatile 原理,再对比 final 的实现就比较简单了
public class TestFinal {
final int a = 20;
}
字节码
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: return
发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,这样对final变量的写入不会重排序到构造方法之外,保证在其它线程读到 它的值时不会出现为 0 的情况。普通变量不能保证这一点了。
读取final变量原理
有以下代码:
public class TestFinal {
final static int A = 10;
final 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);
}
}
反编译UseFinal1中的test方法:
public test()V
L0
LINENUMBER 31 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
BIPUSH 10
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L1
LINENUMBER 32 L1
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC 32768
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L2
LINENUMBER 33 L2
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW cn/itcast/n5/TestFinal
DUP
INVOKESPECIAL cn/itcast/n5/TestFinal.<init> ()V
INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class;
POP
BIPUSH 20
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L3
LINENUMBER 34 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW cn/itcast/n5/TestFinal
DUP
INVOKESPECIAL cn/itcast/n5/TestFinal.<init> ()V
INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class;
POP
LDC 2147483647
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L4
LINENUMBER 35 L4
NEW cn/itcast/n5/TestFinal
DUP
INVOKESPECIAL cn/itcast/n5/TestFinal.<init> ()V
INVOKEVIRTUAL cn/itcast/n5/TestFinal.test1 ()V
L5
LINENUMBER 36 L5
RETURN
L6
LOCALVARIABLE this Lcn/itcast/n5/UseFinal1; L0 L6 0
MAXSTACK = 3
MAXLOCALS = 1
}
可以看见,jvm对final变量的访问做出了优化:另一个类中的方法调用final变量是,不是从final变量所在类中获取(共享内存),而是直接复制一份到方法栈栈帧中的操作数栈中(工作内存),这样可以提升效率,是一种优化。
总结:
-
对于较小的static final变量:复制一份到操作数栈中
-
对于较大的static final变量:复制一份到当前类的常量池中
-
对于非静态final变量,优化同上。
final总结
final关键字的好处:
(1)final关键字提高了性能。JVM和Java应用都会缓存final变量。
(2)final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
(3)使用final关键字,JVM会对方法、变量及类进行优化。
关于final的重要知识点
1、final关键字可以用于成员变量、本地变量、方法以及类。
2、final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。
3、你不能够对final变量再次赋值。
4、本地变量必须在声明时赋值。
5、在匿名类中所有变量都必须是final变量。
6、final方法不能被重写。
7、final类不能被继承。
8、final关键字不同于finally关键字,后者用于异常处理。
9、final关键字容易与finalize()方法搞混,后者是在Object类中定义的方法,是在垃圾回收之前被JVM调用的方法。
10、接口中声明的所有变量本身是final的。
11、final和abstract这两个关键字是反相关的,final类就不可能是abstract的。
12、final方法在编译阶段绑定,称为静态绑定(static binding)。
13、没有在声明时初始化final变量的称为空白final变量(blank final variable),它们必须在构造器中初始化,或者调用this()初始化。不这么做的话,编译器会报错“final变量(变量名)需要进行初始化”。
14、将类、方法、变量声明为final能够提高性能,这样JVM就有机会进行估计,然后优化。
15、按照Java代码惯例,final变量就是常量,而且通常常量名要大写。
16、对于集合对象声明为final指的是引用不能被更改,但是你可以向其中增加,删除或者改变内容。
无状态
在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这 种没有任何成员变量的类是线程安全的 。
因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】