不可变
一、 日期转换的问题
1、引入
下面的代码在运行时,由于 SimpleDateFormat 不是线程安全的
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
System.out.println(sdf.parse("1951-04-21"));
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
运行之后会出现下面的结果:
Mon Apr 21 00:00:00 CST 4
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Thu Apr 21 00:00:00 CST 214
Fri Apr 21 00:00:00 CST 4000
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:2056)
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 concurrent.ch7_immutable.DataFormatTest.lambda$main$0(DataFormatTest.java:12)
at java.lang.Thread.run(Thread.java:745)
Sat Apr 21 00:00:00 CST 1951
Wed Apr 21 00:00:00 CST 1
Fri Apr 21 00:00:00 CST 4000
不仅结果不对,而且还会出现NumberFormatException异常。
2、解决方法:锁
要解决SimpleDateFormat的线程安全问题,一种方法时使用synchronized锁,代码如下:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//对sdf对象上锁
synchronized (sdf) {
try {
System.out.println(sdf.parse("1951-04-21"));
} catch (ParseException e) {
e.printStackTrace();
}
}
}).start();
}
结果:
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
可以看到结果没有问题,但是我们之前也介绍过使用synchronized加锁会带来性能上的问题,所以又引入另外一种解决方法:不可变类
2、解决方法:不可变
如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改。这样的对象在 Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(dtf.parse("1951-04-21"));
}).start();
}
结果:
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
虽然和上面的输出格式不同,但是结果是没问题的。DateTimeFormatter的注释中也声明它是一个不可变的线程安全类,不可变对象,实际是另一种避免竞争的方式。
/ *
* @implSpec
* This class is immutable and thread-safe.
*
* @since 1.8
*/
public final class DateTimeFormatter {
二、不可变设计
另一个大家更为熟悉的 String 类也是不可变的,以它为例,说明一下不可变设计的要素
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
//...
}
1、final的使用
我们发现String类和类中所有属性都是 final 的
- 属性用 final 修饰保证了该属性是只读的,不能修改
- 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
2、保护性拷贝
当需要修改属性的值,如char value[]
时,并没有改变其中的值,而是直接新建(拷贝)一个对象赋值给该属性。
例如:
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
这里没有直接使用this.value = value
,而是使用Arrays.copyOf
进行拷贝,就是为了防止外部引用对参数value的改变会影响到this.value,而Arrays.copyOf
创建了一个值参数一样但不是同一个对象,就避免了这种问题。
再以以 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);
}
//上面都是参数合法性检查,最后调用new String创建了新的对象
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
可以看到substring并没有直接修改成员变量char value[]
的值,而是创建了一个新的对象。而该构造函数中也使用了Arrays.copyOfRange
进行拷贝。构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避 免共享的手段称之为保护性拷贝(defensive copy)。
public String(char value[], int offset, int count) {
//...(参数合法性检查)
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
三、设计模式:享元模式
1、 简介
定义 英文名称:Flyweight pattern. 当需要重用数量有限的同一类对象时,可以使用享元模式
wikipedia: A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects
2、体现
在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) {
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 等。
3、实践DIY
例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时 预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约 了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库
//连接池实现类
class Pool {
//定义连接池大小
private final int poolSize;
//连接对象数组
private Connection[] connections;
//定义连接状态数组 0:表示空闲,1:表示繁忙
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("conn " + i);
}
}
//借连接
public Connection borrow() {
while (true) {
//遍历连接状态数组,找出空闲的连接并返回
for (int i = 0; i < poolSize; i++) {
if (states.get(i) == 0) {
//为了先线程安全,需要使用CAS对连接状态进行设置
if (states.compareAndSet(i, 0, 1)) {
System.out.println(Thread.currentThread().getName() + " borrow " + connections[i]);
return connections[i];
}
}
}
//当前没有连接池时,进入等待状态
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName() + " wait...");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//归还连接
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == conn) {
//由于此时只有一个线程持有connections[i],所以不会有线程安全问题
states.set(i, 0);
//归还之后,通知等待的线程
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " free " + conn);
this.notifyAll();
}
break;
}
}
}
}
//连接实现类,无具体内容,省略
class MockConnection implements Connection {
//...
}
测试代码:
public static void main(String[] args) {
//创建2两个连接
Pool pool = new Pool(2);
//创建5个线程使用链接
for (int i = 0; i < 5; i++) {
new Thread(() -> {
//创建连接
Connection conn = pool.borrow();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放连接
pool.free(conn);
}
}, "线程 " + i).start();
}
}
结果:
线程 1 borrow MockConnection{name='conn 0'}
线程 0 borrow MockConnection{name='conn 1'}
线程 2 wait...
线程 3 wait...
线程 4 wait...
线程 1 free MockConnection{name='conn 0'}
线程 4 borrow MockConnection{name='conn 0'}
线程 3 wait...
线程 2 wait...
线程 0 free MockConnection{name='conn 1'}
线程 2 borrow MockConnection{name='conn 1'}
线程 3 wait...
线程 4 free MockConnection{name='conn 0'}
线程 3 borrow MockConnection{name='conn 0'}
线程 2 free MockConnection{name='conn 1'}
线程 3 free MockConnection{name='conn 0'}
以上实现没有考虑:
- 连接的动态增长与收缩
- 连接保活(可用性检测)
- 等待超时处理
- 分布式 hash
对于关系型数据库,有比较成熟的连接池实现,例如c3p0, druid等
对于更通用的对象池,可以考虑使用apache commons pool,例如redis连接池可以参考jedis中关于连接池的实现
四、final 原理
1、设置 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 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到 它的值时不会出现为 0 的情况
2、获取 final 变量的原理
读取final变量时,如果值较小可以直接放入使用类的操作数栈中,如果值较大会放入使用类的常量池中。
五、无状态
在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这 种没有任何成员变量的类是线程安全的
因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为**【无状态】**