1.日期转换的问题
1>.代码示例
@Slf4j
public class TestDateFormatDemo1 {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
//多个线程调用日期格式化对象的方法
new Thread(() -> {
try {
log.info("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
}
}
//由于目前使用的日期格式化对象的parse()方法并不是线程安全的,因此多线程环境下使用有很大几率出现
java.lang.NumberFormatException
或者出现不正确的日期解析结果
异常;
2>.解决方案: 同步锁
@Slf4j
public class TestDateFormatDemo1 {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
//多个线程调用日期格式化对象的方法
new Thread(() -> {
synchronized (sdf){
try {
log.info("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}
}).start();
}
}
}
这样虽能解决问题,但带来的是性能上的损失,并不算很好!
3>.解决方案: 不可变
如果一个对象不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改!这样的对象在Java中有很多,例如在Java 8后,提供了一个新的日期格式化类;
@Slf4j
public class TestDateFormatDemo1 {
public static void main(String[] args) {
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.info("{}", date);
}).start();
}
}
}
不可变对象,实际是另一种避免竞争的方式!
2.不可变设计–String
1>.大家熟悉的String类也是不可变的,以它为例,说明一下不可变设计的要素;
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
//使用final修饰,只能在构造方法中赋值!!!
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
//......
}
2.1.final的使用
从源码中得知,String类、类中属性是用final修饰的:
- ①.属性用final修饰保证了该属性是只读的,不能修改;
- ②.类用final修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性!
***注意:使用final修饰对象,只能保证对象的引用(内存地址)不被改变,但是无法保证对象的内容不被改变!
2.2.保护性拷贝
使用字符串时,也有一些跟修改相关的方法,比如substring等,那么下面就看一看这些方法是如何实现的?就以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);
}
//内部是调用String的构造方法创建了一个新字符串,再进入这个构造看看,是否对"final char[] value"做出了修改;
return (beginIndex == 0) ? 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);
}
发现并没有修改原始的"char[] value",而是在构造新字符串对象时,生成新的"char[] value",然后对内容进行复制.这种
通过创建副本对象来避免共享的手段称之为"保护性拷贝(defensive copy)"!
3.设计模式之享元模式
3.1.简介
1>.英文名称: Flyweight pattern.当需要重用数量有限的同一类对象时,为了避免频繁创建同一类对象,可以使用享元模式;
3.2.体现
3.2.1.包装类
1>.在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);
}
private static class LongCache {
private LongCache(){}
static final Long cache[] = new Long[-(-128) + 127 + 1];
static {
//缓存256个Long对象,之后使用的时候直接从缓存中取即可,避免对象的重复创建
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
//...
}
***注意:
①.Byte,Short,Long缓存的范围都是:-128~127;
②.Character缓存的范围是:0~127;
③.Integer的默认范围是:-128~127;
- –1).最小值不能变;
- –2).最大值可以通过调整虚拟机参数"-Djava.lang.Integer.IntegerCache.high"来改变;
④.Boolean缓存了TRUE和FALSE;
除了包装类,还有String类以及BigDecimal,BigInteger类等都是不可变类,体现了享元模式,也是线程安全的!
3.3.基于享元模式自定义简单的数据库连接池
1>.需求:
一个线上商城应用,QPS达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响.这时预先创建好一批连接,放入连接池.一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库;
2>.代码实现:
public class TestPool {
public static void main(String[] args) {
//创建连接池对象
Pool pool = new Pool(3);
//模拟多个线程操作连接池
for (int i = 0; i < 5; i++) {
new Thread(()->{
//获取连接
Connection connection = pool.getConnection();
//阻塞,模拟业务处理
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//归还连接
pool.freeConnection(connection);
}
},"线程"+(i+1)).start();
}
}
}
//基于享元模式自定义数据库连接池
@Slf4j
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[this.poolSize];
this.states = new AtomicIntegerArray(new int[this.poolSize]);
//循环创建connection对象,放入connection数组中
for (int i = 0; i < this.poolSize; i++) {
//假设这些连接对象都是正常可用的
this.connections[i] = new MockConnection("连接"+(i+1));
}
}
//5.获取连接对象
public Connection getConnection() {
while (true) {
for (int i = 0; i < this.poolSize; i++) {
//获取空闲连接对象
if (this.states.get(i) == 0) {
//由于该连接对象并没有被具体某个线程持有,也就是说该连接对象可能会被多个线争抢
//因此需要使用CAS机制修改该连接对象的状态
if (this.states.compareAndSet(i, 0, 1)) {
log.info("获取连接:{}", this.connections[i]);
//修改连接对象的状态成功的线程才能获取connections数组中对应下标的connection连接对象
return this.connections[i];
}
}
}
//如果没有空闲连接,线程等待
//为什么不使用CAS机制让线程一直循环运行着,而是等待?
//CAS操作适合于短时间运行的代码片段(即对锁资源的占用时间短),让线程可以在短时间内不停尝试获取锁(连接对象),但是目前场景是线程拿到锁(连接对象)之后要操作其他业务,即线程占有锁(连接对象)的时间较长,如果使用CAS机制,那么那些没有获取到锁(连接对象)的线程一直循环不停的运行,对系统资源消耗是非常大的,有可能将系统资源耗尽,最好的方式就是让这些没有获取到锁(连接对象)的线程暂时等待,释放系统资源;
synchronized (this) {
try {
log.info("{} wait...", Thread.currentThread().getName());
this.wait(); //这里可以优化,参考之前的"保护性暂停带超时版"
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//6.归还连接对象
public void freeConnection(Connection connection) {
for (int i = 0; i < this.connections.length; i++) {
if (this.connections[i] == connection) {
//由于该连接对象已经被某个具体线程持有,也就是说不存在多个线程同时使用一个连接对象的情况
//因此这里只需要使用普通的方法修改对应的连接对象的状态即可!!!
this.states.set(i, 0);
//已有空闲连接对象,唤醒等待中的线程
synchronized (this) {
log.info("归还连接:{}", connection);
this.notifyAll();
}
break;
}
}
}
}
//连接对象(由于是测试环境,因此又自定义了一个connection对象,实现connection接口)
class MockConnection implements Connection {
private String name;
public MockConnection(String name) {
this.name = name;
}
@Override
public String toString() {
return "MockConnection{" +
"name='" + name + '\'' +
'}';
}
//省略connection接口中的方法实现
}
***注意: 以上代码只是为了方便理解享元模式,相较于成熟的数据库连接池,有以下方面没有考虑到,切勿用于生产环境!
①.连接的动态增长与收缩
②.连接保活(可用性检测)
③.等待超时处理
④.分布式 hash
…
对于关系型数据库,有比较成熟的连接池实现,例如c3p0,drui等,对于更通用的对象池,可以考虑使用apache commons pool,例如redis连接池可以参考jedis中关于连接池的实现;
4.final原理
4.1.设置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的情况;
4.2.获取final变量的原理
分析:
如果获取的是
final修饰的变量
,那么jvm底层使用的是"BIPUSH/LDC"指令
,他并没有到final变量所属类中读取变量,而是把final变量的值复制一份到使用类的栈中,整个过程不涉及到共享操作;
如果是普通变量(如static变量)
,那么jvm底层使用的是"GETSTATIC指令"
,要从变量所属类中读取对应的变量值
,需要用到共享内存(/从堆中读取数据)
,其性能要低于直接使用栈内存
;
***注意:
①.如果final变量的值较小,直接复制一份到使用类的栈内存中(BIPUSH);如果final变量的值较大,也会复制一份到使用类的常量池中(LDC);性能较高;
②.如果是普通变量,就相当于在共享内存(堆)中读取(GETSTATIC)变量的值;性能较低;
5.不可变的特例–无状态
在web阶段学习时,设计Servlet时为了保证其线程安全都会有这样的建议: 不要为Servlet设置成员变量,这种没有任何成员变量的类是线程安全的
.因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为"无状态";