目录
SImpleDateFormat格式转换
多线程下安全格式转化
不可变类的设计
保护性拷贝
DIY一个连接池
共享模型中不可变的解释是对象属性不可以更改。
接下来查看一个对象属性可以进行更改产生的线程安全问题。
在多线程下,格式转化使用SimpleDateFormat可能会报错。这是因为线程之间互相影响导致。具体原因请查看另一篇博客SimpleDateFormat在多线程下的安全问题-CSDN博客
问题代码如下
public class test {
public static void main(String[] args) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
Date parse = simpleDateFormat.parse("2003-6-03");
System.out.println(parse);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
因为SimpleDateFormat类中的calendar属性可以在多个线程中共享,可以在多个线程中同时进行改变,因此产生了线程安全问题。
在JDK8之后提供了线程安全的格式转换DateTimeFormatter类。使用方法如下。
public class test {
public static void main(String[] args) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
TemporalAccessor parse = dateTimeFormatter.parse("2003-06-03");
System.out.println(parse);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
在Java中,有许多类都采用了不可变设计来避免线程不安全问题。接下来以String为例简单介绍一下,以下是String类中的字符存储方式,字符串使用char数组来进行存储。hash是用来存储哈希值,当第一次调用hashCode时会存储在hash变量中做缓存。
类与属性均采用final关键词进行修饰:
这里需要搞清楚,String中value虽然添加了final来修饰,代表的是char数组的引用不可变,不代表char数组中的值不可变。而对于基本类型来说,使用final修饰才代表值不可变。
以下是String类的构造方法。
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
对于char数组,如果将参数value的引用直接赋值给this.value的话,实际上this.value存储的是一个数组地址,而不是存储的值。当外部还存在一个char数组引用了String当中的参数数组时,当外部的char数组中的值发生改变时,String对象中的值也跟着改变。因此要拷贝出一样的char数组让value来引用拷贝出来的地址。
接下来看一个没有采用保护性拷贝导致的对象值改变的情况
public class Test {
public static void main(String[] args) {
//定义一个char数组
char[] name = new char[]{'z','s'};
//另一个数组也引用name的地址
char[] changeName = name;
Student student = new Student(name);
System.out.println(student.name);
//修改changName的值
changeName[0]='l';
System.out.println(student.name);
}
static class Student{
char[] name;
public Student(char[] name) {
this.name = name;
}
}
}
输出结果如下
zs
ls
我们修改的是对象外char数组的值,但是对象中的值也随之发生改变,从此我们可以看出保护性拷贝的重要性。
类似于String类容易频繁创建对象,这时通常会用享元模式来减少对象的创建。对于享元模式不熟悉的小伙伴可以查看我的另一篇文章。JDK使用了享元模式的源码解析
连接池作用是可以避免在高并发的情况下反复建立连接浪费系统性能,实现连接复用。基于享元模式实现的。
连接池代码实现
public 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 MyConnect("nameIs"+i);
}
}
//获取连接
public Connection getConnect(){
while (true){
for (int i = 0; i < poolSize; i++) {
//如果该连接空闲
if (states.get(i)==0){
//修改该连接的状态
if (states.compareAndSet(i,0,1)){
System.out.println("获取连接"+connections[i]);
return connections[i];
}
}
}
//如果没有空闲连接
synchronized (this){
try {
System.out.println("没有空闲连接");
//进入阻塞,等待其他线程释放来连接
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//释放连接
public void freeConnect(Connection connection){
//判断传过来的连接是否是连接池中的。
for (int i = 0; i < poolSize; i++) {
if (connections[i]==connection){
//因为只有一个线程拿到该连接,因此不会发生线程安全问题,直接使用set即可
states.set(i,0);
System.out.println("释放连接:"+connection);
synchronized (this){
//唤醒其他阻塞的线程
this.notifyAll();
}
break;
}
}
}
}
测试
public class Test{
public static void main(String[] args) {
//连接池大小为3
Pool pool = new Pool(3);
for (int i = 0; i < 5; i++) {
new Thread(()->{
Connection connect = pool.getConnect();
try {
//拿到连接的线程进行随机休眠
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
//释放连接
pool.freeConnect(connect);
}).start();
}
}
}
运行结果如下
获取连接MyConnect{name='nameIs0'}
获取连接MyConnect{name='nameIs1'}
没有空闲连接
获取连接MyConnect{name='nameIs2'}
没有空闲连接
释放连接:MyConnect{name='nameIs0'}
获取连接MyConnect{name='nameIs0'}
没有空闲连接
释放连接:MyConnect{name='nameIs2'}
获取连接MyConnect{name='nameIs2'}
释放连接:MyConnect{name='nameIs0'}
释放连接:MyConnect{name='nameIs2'}
释放连接:MyConnect{name='nameIs1'}
至此就是共享模型之不可变的全部内容。