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设置成员变量,这种没有任何成员变量的类是线程安全的.因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为"无状态";



















