6.不可变共享模型
-
不可变类的使用
-
不可变类设计
-
无状态类设计
6.1问题引入
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();
}
19:10:40.859 [Thread-2] c.TestDateParse - {}
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
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.859 [Thread-1] c.TestDateParse - {}
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
19:10:40.857 [Thread-9] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951
19:10:40.857 [Thread-6] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951
19:10:40.857 [Thread-4] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951
19:10:40.857 [Thread-5] c.TestDateParse - Mon Apr 21 00:00:00 CST 178960645
19:10:40.857 [Thread-0] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951
19:10:40.857 [Thread-7] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951
19:10:40.857 [Thread-3] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951
解决思路:
- 思路 - 同步锁
- 思路 - 不可变
如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!这样的对象在
Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:
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();
}
@implSpec
This class is immutable and thread-safe. DateTimeFormatter
6.2不可变设计
eg.String类为不可变类型
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
final 的使用
发现该类、类中所有属性都是 final 的
-
属性用 final 修饰保证了该属性是只读的,不能修改
-
类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
可以看到value[]是final类型,也就是不可以被修改的,那么
substring()
之类的方法怎么返回的字符串呢?
保护性拷贝
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);
}
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;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count); // ***
}
对final数组里的字符进行数组拷贝,这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】
6.3享元模式
简介
定义 英文名称:Flyweight pattern. 当需要重用数量有限的同一类对象时
体现
-
包装类:在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 串池 : 参照上一小节
-
BigDecimal BigInteger
-
连接池:
import lombok.extern.slf4j.Slf4j;
import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicIntegerArray;
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();
}
}
}
@Slf4j(topic = "c.Pool")
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 {
private String name;
public MockConnection(String name) {
this.name = name;
}
}
Q&A一些思考:
为什么不让线程空转而是wait呢?
释放线程占用资源等待唤醒,减少空闲线程对处理机的占用。提高系统整体并发效率
以上实现没有考虑:
-
连接的动态增长与收缩
-
连接保活(可用性检测)
-
等待超时处理
-
分布式 hash
对于关系型数据库,有比较成熟的连接池实现,例如c3p0, druid等 对于更通用的对象池,可以考虑使用apache
commons pool,例如redis连接池可以参考jedis中关于连接池的实现
6.4final原理
设置 final 变量的原理
public class TestFinal {
final int a = 11;
}
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 11
7: putfield #2 // Field a:I
<-- 写屏障
10: return
- 发现 fifinal 变量的赋值也会通过 putfifield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到
它的值时不会出现为 0 的情况
- 如果不加final,int a = 20 其实是经历了 从初始值为0 到 赋值为20的过程,这个过程不是原子的,在多线程下不安全
获取 final 变量的原理
public class test {
final static int A =20;
final static int B=Short.MAX_VALUE+1;
final int C =20;
final int D=Short.MAX_VALUE+1;
public static void main(String[] args) {
System.out.println(test0522.A);
System.out.println(test0522.B);
System.out.println(new test0522().C);
System.out.println(new test0522().D);
}
L0
LINENUMBER 19 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
BIPUSH 20
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L1
LINENUMBER 20 L1
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC 32768
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L2
LINENUMBER 21 L2
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW com/example/demo/com/example/demo/test0522
DUP
INVOKESPECIAL com/example/demo/com/example/demo/test0522.<init> ()V
GETFIELD com/example/demo/com/example/demo/test0522.C : I
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L3
LINENUMBER 22 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW com/example/demo/com/example/demo/test0522
DUP
INVOKESPECIAL com/example/demo/com/example/demo/test0522.<init> ()V
GETFIELD com/example/demo/com/example/demo/test0522.D : I
INVOKEVIRTUAL java/io/PrintStream.println (I)V
可以清楚的看到,被final修饰的变量在变量值较小的时候直接从栈内存中获取(bitpush,范围-128-127),当变量超出这个范围时,则从常量池中获取(ldc或sipush等),而没有被final修饰的变量则从堆内存中获取(getfield),final等于做了一个在读取上的优化。
补充:
final修饰的静态字段(数据类型为基本类型或String)在编译时会生成ConstantValue属性,在类加载的准备阶段利用ConstantValue属性初始化赋值
而没有被final修饰或者非基本类型和String类型的静态变量,尽管也有ConstantValue属性,也是在方法中初始化(类加载的初始化阶段)。
对于成员变量则是在创建对象时才会分配内存,其中被final修饰的需要在构造函数结束前初始化
6.5无状态
在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的
因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】