目录
一、日期转换问题
解决
二、不可变类String?
final的特点
保护性拷贝
三、享元模式
体现?
享元模式之自定义连接池
四、无状态?
一、日期转换问题
一般使用的最多的是SimpleDateFormat,但是因为是不可变类,在多个线程操作共享变量的时候内部的数据会改变,造成线程安全问题,接下来看一下测试案例。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {//运行10个线程
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
? 有很大几率出现?java.lang.NumberFormatException?或者出现不正确的日期解析结果?
17:27:19.884 a.ThreadTest15 [Thread-5] - {}
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 ThreadTest15.lambda$main$0(ThreadTest15.java:16)
at java.lang.Thread.run(Thread.java:748)
17:27:19.885 a.ThreadTest15 [Thread-0] - {}
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:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at ThreadTest15.lambda$main$0(ThreadTest15.java:16)
at java.lang.Thread.run(Thread.java:748)
17:27:19.884 a.ThreadTest15 [Thread-3] - {}
java.lang.NumberFormatException: multiple points
at 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 ThreadTest15.lambda$main$0(ThreadTest15.java:16)
at java.lang.Thread.run(Thread.java:748)
17:27:19.885 a.ThreadTest15 [Thread-4] - {}
java.lang.NumberFormatException: multiple points
at 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 ThreadTest15.lambda$main$0(ThreadTest15.java:16)
at java.lang.Thread.run(Thread.java:748)
17:27:19.883 a.ThreadTest15 [Thread-9] - Sat Apr 21 00:00:00 CST 1951
17:27:19.883 a.ThreadTest15 [Thread-7] - Sat Apr 21 00:00:00 CST 1951
17:27:19.883 a.ThreadTest15 [Thread-2] - Thu Feb 21 00:00:00 CST 1952
17:27:19.883 a.ThreadTest15 [Thread-6] - Sat Apr 21 00:00:00 CST 1951
17:27:19.883 a.ThreadTest15 [Thread-8] - Sat Apr 21 00:00:00 CST 1951
17:27:19.883 a.ThreadTest15 [Thread-1] - Thu Feb 21 00:00:00 CST 1952
1、可以使用管程的思想,给同步代码块加锁,防止指令交错执行(效率降低)
2、可以将共享变量变成局部变量,成为每个线程私有的不被共享(重复创建对象内存消耗大)
3、使用不可变的内,保证内部数据不会存在并发修改
这里为方法3提供一种不可变类的类演示,DateTimeFormatter 类,查看类的解释可以知道
//官方说明这个类是不可变的类同时也是线程安全的 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?@implSpec This class is immutable and thread-safe.
?演示
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();
}
具体的其他方法可查阅API,同时也有其他的线程安全的操作时间的类比如LocalDateTime
二、不可变类String?
string类是如何做的不可变的呢?查看一下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
//...省略
//String内是被final修饰的, final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
//String类中内容都存储在value的char数组中,被final修饰的保证该元素只能读不能修改
//hash是String的hash值,它是在第一次调用hashcode的时候会被赋值保存在String内部
//后面再使用的话都是直接调用hash就行,同时也是private修饰的,没提供对外的修改方法,安全的
正好通过这来总结下final的特点
1、属性用 final 修饰保证了该属性是只读的,不能修改 2、类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
那么final修饰的是value这个引用是如何保证char数组内部的值不会被改变呢?
String的构造方法
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
可以看出,在传入char数组的时候,String并未使用其外部的char数组作为内部value的值,而是通过重新拷贝创建了一个新的char数组得到一个新的引用地址,这样对外部char数组的改变影响不了String内部char数组的值,因为已经不是同一个数组了
那么subString方法不是相当于对外部提供了改变内部char数组的方法吗?
substring方法
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);
}
前面两个判断是做数据判断,不重要,关键是最后一行调用了new String()方法,进入看一下
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);
}
看到最后一行,同样也是对修改的char数组进行了裁剪拷贝,创建了一个新的char数组,这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】,可以理解成为多个线程操作同一个string对象的时候,内部分别会对每个操作线程创建不同的副本对象,可以理解成为这些副本对象成为了每个线程的私有属性了
测试一下
char[] a = {'h','e','l','l','o'};
String string = new String(a);
Thread t1 = new Thread(() -> {
log.debug(string.substring(1));
},"t1");
new Thread(() -> {
log.debug(string.substring(2));
},"t2").start();
new Thread(() -> {
log.debug(string.substring(3));
},"t3").start();
new Thread(() -> {
log.debug(string.substring(3));
},"t4").start();
//线程t2,t3,t4执行完以后t1启动,主线程获取string的值
Thread.sleep(1000);
t1.start();
log.debug(string);
18:40:47.133 a.ThreadTest15 [t3] - lo
18:40:47.133 a.ThreadTest15 [t4] - lo
18:40:47.133 a.ThreadTest15 [t2] - llo
18:40:48.137 a.ThreadTest15 [t1] - ello
18:40:48.137 a.ThreadTest15 [main] - hello
不难看出,每个线程无论对其进行了什么样的操作,都影响不了其他线程再次对其进行操作,证实了上面的说法,但是这样重复的创建副本会造成内存的消耗,有什么办法呢?String中就提供了享元模式这种解决办法。
三、享元模式
定义?英文名称:Flyweight pattern.?当需要重用数量有限的同一类对象时
什么情况下使用享元模式: (1)一个系统有大量的对象? (2)这些对象耗费大量的内存? (3)这些对象可以按照内蕴状态分成很多的组,当把外蕴对象从对象中删除时,每一个组都可以仅用一个对象代替? (4)使用享元模式需要维护一个记录了系统已有的所有享元的表,而这需要耗费资源,因此应当在有足够多的享元实现可供共享时才值的使用享元模式.?
享元模式的优缺点 享元模式的优点在于它大幅度地降低内存中的对象的数量,但是也为这一点付出了代价:? (1)为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化? (2)享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长.
比如一个字符串对象“abc”,当再次需要使用“abc”的时候直接调用已经创建了的“abc”对象
在JDK中?Boolean,Byte,Short,Integer,Long,Character?等包装类提供了?valueOf?方法,String?串池,BigDecimal BigInteger
来看一下Long中的valueof方法?
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // 当在这个范围内的时候,会直接在cache中选择
return LongCache.cache[(int)l + offset];
}
//如果不在才会创建对象
return new Long(l);
}
private static class LongCache {
private LongCache(){}
static final Long cache[] = new Long[-(-128) + 127 + 1];
//该类加载的时候创建了cache数组
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
注意:
1、Byte, Short, Long 缓存的范围都是 -128~127
2、Character 缓存的范围是 0~127
3、Integer的默认范围是 -128~127
?integer最小值不能变但最大值可以通过调整虚拟机参数 ?????????????`?-Djava.lang.Integer.IntegerCache.high` 来改变
4、Boolean 缓存了 TRUE 和 FALSE
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) {
//只有持有当前链接的对象才能归还,所以不存在安全问题,普通set方法即可
states.set(i, 0);
synchronized (this) {
log.debug("free {}", conn);
this.notifyAll();
}
break;
}
}
}
}
class MockConnection implements Connection {
// 实现略
}
以上实现没有考虑: 1、连接的动态增长与收缩 2、连接保活(可用性检测) 3、等待超时处理 4、分布式 hash 对于关系型数据库,有比较成熟的连接池实现,例如c3p0, druid等 对于更通用的对象池,可以考虑使用apache?commons pool,例如redis连接池可以参考jedis中关于连接池的实现
四、无状态?
因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】,在?设计?Servlet?时为了保证其线程安全,都会有这样的建议,不要为?Servlet?设置成员变量,这种没有任何成员变量的类是线程安全的。
|