概述
不可变类是指类的实例一旦创建后,不能改变其成员变量的值。
与之对应的,可变类的实例创建后可以改变其成员变量的值。
Java 中八个基本类型的包装类和 String 类都属于不可变类,而其他的大多数类都属于可变类。
- 效率
当一个对象是不可变的,那么需要拷贝这个对象的内容时,就不用复制它的本身而只是复制它的地址,复制地址(通常一个指针的大小)只需要很小的内存空间,具有非常高的效率。同时,对于引用该对象的其他变量也不会造成影响(字符串常量池)。
此外,不变性保证了hashCode 的唯一性,因此可以放心地进行缓存而不必每次重新计算新的哈希码。而哈希码被频繁地使用, 比如在hashMap 等容器中。将hashCode 缓存可以提高以不变类实例为key的容器的性能。
-
线程安全
在多线程情况下,一个可变对象的值很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况同时省去了同步加锁等过程,因此不可变类是线程安全的
当然,不可变类也有缺点:不可变类的每一次“改变”都会产生新的对象,因此在使用中不可避免的会产生很多垃圾
首先来看看可变类:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 20; i++) {
Thread thread = new Thread(()->{
try {
sdf.parse("2021-02-02");
} catch (ParseException e) {
e.printStackTrace();
}
});
thread.start();
}
因为多线程的访问去修改对象中的成员属性,导致中间发生了几次java.lang.NumberFormatException异常,可以使用加锁互斥访问进行解决,但比较耗损性能;也可以使用DateTimeFormatter保证安全,还不像加锁那样损耗性能
DateTimeFormatter stf=DateTimeFormatter.ofPattern("yyyy-MM-dd");//jdk8新特性
for (int i = 0; i < 1000; i++) {
new Thread(()->{
TemporalAccessor parse = stf.parse("1951-04-21");
System.out.println(parse);
}).start();
}
DateTimeFormatter 里面的成员都是final类型的,源码如下:
//This class is immutable and thread-safe.
public final class DateTimeFormatter {
private final CompositePrinterParser printerParser;
private final Locale locale;
private final DecimalStyle decimalStyle;
private final ResolverStyle resolverStyle;
private final Set<TemporalField> resolverFields;
private final Chronology chrono;
private final ZoneId zone;
}
不可变类设计
来看看String类,看看不可变类的设计要素:
//类声明也应该是final类型的,防止子类继承,破坏不可变性
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];//String值,final类型不可变
//用于缓存hash值,使用hashCode方法直接返回该值即可,减少了计算的工作量
private int hash; //懒加载的
//构造方法
public String(char value[]) {
//拷贝value[],而不是直接把this.value=value;
this.value = Arrays.copyOf(value, value.length);
}
//普通方法
public String substring(int beginIndex, int endIndex) {
int subLen = endIndex - beginIndex;
//返回的是一个新的对象
return new String(value, beginIndex, subLen);
}
//普通方法,返回一个对象属性的拷贝
public char[] toCharArray() {
//新创建一个数组
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
}
可以看出不可变类设计因素包括:
- 类被final修饰,防止子类继承。如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。
- 类中的属性都是final类型的,不能更改,还必须是private的防止引用属性的内部被更改,防止泄漏地址。
- 没有setter方法更改属性,且getter方法不能返回原本的属性或对象,应该复制一份返回,因为怕把引用泄漏给外部,导致成员中的内容被修改。如String类的toCharArray方法,不会直接返回char[] value给外部,防止value数组中的元素被更改
- 修改对象的属性时要返回新的对象,如subString方法
- 对构造器传入的值,应该是拷贝一份,而不是用原本的值,如果使用传入的参数直接赋值,则传递的只是引用,仍然可以通过外部变量改变参数的的值,从而间接修改不可变类的值
享元模式
为了避免浪费,可以把这些不可变类的对象进行缓存,如果需要使用到的对象和缓存中的一致可以直接使用缓存中的已经创建好的对象,共享使用,因为是对象都是不可变的,所以相互共享也没什么毛病。这种共享不可变的对象是享元设计模式的一种实现。
在JDK中Boolean,Byte,Short,Integer,Long,Character等包装类提供了valueOf方法,例如Integer的valueOf会缓存-128~127(默认)之间的Integer对象,在这个范围之间会重用对象,大于这个范围,才会新建Integer对象。如下:
public static Integer valueOf(int i) {
//IntegerCache.high默认127,可以更改配置文件更改
if (i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
自定义连接池
连接池也属于享元模式的一种引用,简单的写上一个连接池代码如下:
@Slf4j
public class ConnectionPool {
private final ReentrantLock lock = new ReentrantLock();//锁对象
private final Condition waitSet = lock.newCondition();//条件变量
//存放连接对象Connection,使用队列
private LinkedList<Connection> connections;
public ConnectionPool(int size) {
if (size <= 0) {
throw new RuntimeException("size mistake");
}
this.connections = new LinkedList<>();
for (int i = 0; i < size; i++) {
connections.add(new Connection("connection" + (i + 1)));
}
}
/**
* 带超时效果获取连接池
* @param mills ms,如果小于0则无限等待,大于0最多等待多久
* @return Connection
*/
public Connection getConnection(long mills) {
lock.lock();
try {
if (mills <= 0) {
while (connections.isEmpty()) {
waitSet.await();
}
return connections.removeFirst();
} else {
long start = System.currentTimeMillis();
//剩余的等待时间
long remain = mills;
//如果连接池为空且等待时间还没等完都需要等待
while (remain > 0 && connections.isEmpty()) {
waitSet.await(remain, TimeUnit.MILLISECONDS);
//减去已经等待的时间,防止虚假唤醒
remain = mills - (System.currentTimeMillis() - start);
}
//如果超时了需要先判断判断当前是否有连接
if (!connections.isEmpty()) {
return connections.removeFirst();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return null;
}
//释放连接
public void freeConnection(Connection connection) {
if (connection != null) {
lock.lock();
try {
connections.addLast(connection);
//唤醒全部等待的线程
waitSet.signalAll();//可以单个唤醒,更简单
} finally {
lock.unlock();
}
}
}
}
//模拟数据库Connection
class Connection {
public String name;
public Connection(String name) {
this.name = name;
}
}
测试代码:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
ConnectionPool pool = new ConnectionPool(4);
for (int i = 0; i < 30; i++) {
new Thread(() -> {
Connection connection = pool.getConnection(0);
if (connection != null) {
log.debug("{}线程得到连接{}", Thread.currentThread().getName(), connection.name);
try {
Thread.sleep((int) (Math.random() * 1000 + 500));
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}线程释放{}连接", Thread.currentThread().getName(), connection.name);
pool.freeConnection(connection);
} else {
log.debug("{}线程没有获得连接", Thread.currentThread().getName());
}
}, "t" + i).start();
}
}