一、日期转换的问题
1)可变类SimpleDateFormat
有线程安全问题
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1905-04-21"));
} catch (ParseException e) {
throw new RuntimeException(e);
}
}).start();
}
2)不可变类DateTimeFormatter
JDK 8以后引入不可变类,没有线程安全问题
DateTimeFormatter stf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
log.debug("{}", stf.parse("1905-04-21"));
}).start();
}
二、不可变设计
String类是不可变的
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; // final 不可变
/** Cache the hash code for the string */
private int hash; // Default to 0 没有提供set方法 不可变
//
}
final 的使用
发现该类中所有属性包括类上都加了 final
- 属性 用 final 保证了该属性是只读,不能修改
- 类用 final 保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
保护性拷贝
String.substring方法
发现内部创建了一个新的字符串,在进入构造方法,是否对 final char[] value 做出了修改:
结果发现也没有,构造新字符串对象时,会生成新的char[] value, 对内容进行拷贝。
这种通过创建副本对象来避免共享的手段称之为 【保护性拷贝(defensive copy)】
三、 享元模式
1) 简介
- 23种设计模式之一
- 保护性拷贝的创建的对象太多,浪费空间,所以需要考虑相同对象重用,这时候就用到享元模式
2) 体现
- String 类 体现是 串池
- BigDecimal BIgInteger 实现也都是使用了享元模式
这些不可变类中,单个方法使用时线程安全的,但是如果涉及到使用了方法,然后对原对象进行了赋值操作
多线程情况下,例如 AtomicReferecce 进行一个线程安全保护。
3) DIY (do it yourself)
背景:
手写简易版连接池
package com.itheima.test6;
import lombok.extern.slf4j.Slf4j;
import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class Test2Pool {
public static void main(String[] args) {
Pool pool = new Pool(2);
for (int i = 0; i < 5; i ++ ) {
new Thread(() -> {
Connection borrow = pool.borrow();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.free(borrow);
}).start();
}
}
}
@Slf4j(topic = "c.Pool")
class Pool {
// 连接池的大小
private final int poolSize;
// 连接池数组
private Connection[] connection;
// 连接池判断是否可用 0表示可用 1表示不可用
private AtomicIntegerArray status;
public Pool(int poolSize) {
this.poolSize = poolSize;
connection = new Connection[poolSize];
status = new AtomicIntegerArray(poolSize);
for (int i = 0; i < poolSize; i ++ ) {
connection[i] = new MyMarkConnection("连接" + i);
}
}
// 取连接
public Connection borrow() {
while (true) {
for (int i = 0; i < poolSize; i ++ ) {
if (status.get(i) == 0) {
if (status.compareAndSet(i, 0, 1)) {
log.debug("borrow");
return connection[i];
}
}
}
synchronized (this) {
try {
log.debug("wait ... ");
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
// 归换连接
public void free(Connection connection) {
for (int i = 0; i < poolSize; i ++ ) {
if (this.connection[i] == connection) {
status.set(i, 0);
synchronized (this) {
log.debug("free");
this.notifyAll();
}
break;
}
}
}
}
class MyMarkConnection implements Connection{
//记得补全实现一下所有方法
}
四、final原理
1)设置final 变量的原理
共享变量,静态成员变量, 成员变量
public class TestFinal{
final int a = 20;
}
字节码
跟 volatile 一样,final也会在变量后面加一个写屏障,保证可见性和有序性
这样保证final 修改的变量的值在别的线程访问的时候已经是最新的值了
如果没有写屏障,就会有指令重排,导致先初始化空间 int = a,然后默认值是0,然后在赋值,所以就会导致获取不到最新的值。
2)获取 final 变量的原理
public class TestFinal{
final static int A = 10;
final static int B = Short.MAX_VALUE + 1;
final int a = 20;
final int b = Integer.MAX_VALUE;
}
class UseFinal {
public static void test() {
System.out.println(TestFinal.A); // 字节码层面会将final修饰的复制一份到当前方法的栈中
System.out.println(TestFinal.B); // 字节码层面会将final修饰,但是超过了Short最大值的数设置到常量池中。
System.out.println(new TestFinal().a); // 同理
System.out.println(new TestFinal().b); // 同理
}
}
如果没有加 final ,则会取堆中找。加 final 效率比 堆中找的效率高。
- 数字比较小,存放在调用者的方法栈中
- 数字超过短整型,存放在类的常量池中
- 不加final 都在堆中,读取效率都比不上前两者