并发编程系列学习笔记06(共享模型之不可变)

共享模型之不可变

问题案例:日期转换问题

  • 非线程安全: SimpleDateFormat
  • 线程安全:DateTimeFormatter
/**
 * @author 钦尘
 * @date 2021/8/3 22:29
 * @description TODO
 */
@Slf4j
public class TestDateFormat {

    public static void main(String[] args) {

        // 线程不安全的 SimpleDateFormat 演示
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                try {
                    log.info("日期转换:{}", format.parse("2021-08-12"));
                } catch (ParseException e) {
                    log.error("出错了", e);
                    //e.printStackTrace();
                }
            }, "t-" + i).start();
        }

        // Java8后提供一个线程安全的类
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(()-> {
                TemporalAccessor date = dtf.parse("2021-08-16");
                log.info("线程安全的类转换日期:{}", LocalDate.from(date));
            }, "tt" + i).start();
        }
    }
}
  • 错误日志
Exception in thread "t-1" Exception in thread "t-3" Exception in thread "t-7" Exception in thread "t-8" Exception in thread "t-2" Exception in thread "t-0" Exception in thread "t-4" java.lang.NumberFormatException: For input string: "E0"
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at cn.com.ebidding.xiangyuan.TestDateFormat.lambda$main$0(TestDateFormat.java:26)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: "E122"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:589)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at cn.com.ebidding.xiangyuan.TestDateFormat.lambda$main$0(TestDateFormat.java:26)
	at java.lang.Thread.run(Thread.java:748)

不可变设计

  • 参考String类

    • final class String
    • final char value[];
    • java.lang.String#substring(int) -> new String -> Arrays.copyOfRange(...)
  • 加final修饰类及成员变量

  • 保护性拷贝(通过创建对象副本进行拷贝)

  • 带来的问题:对象创建太频繁

  • 享元模式

    • 需要重用数量有限的同一对象时使用
    • 参考基本类型的包装类对象,提供的valueOf方法
    • 创建一批内置对象,放入缓存,避免对象重复创建
    • 注意对应类型的缓存数据范围
    • 思考:BigDecimal,也利用该思想包装了线程安全,但之前转账案例为什么要用原则引用类对其进展包装,包装线程安全性?
    // Cache of common small BigDecimal values.
    private static final BigDecimal zeroThroughTen[] = {
        new BigDecimal(BigInteger.ZERO,       0,  0, 1),
        new BigDecimal(BigInteger.ONE,        1,  0, 1),
        new BigDecimal(BigInteger.valueOf(2), 2,  0, 1),
        new BigDecimal(BigInteger.valueOf(3), 3,  0, 1),
        new BigDecimal(BigInteger.valueOf(4), 4,  0, 1),
        new BigDecimal(BigInteger.valueOf(5), 5,  0, 1),
        new BigDecimal(BigInteger.valueOf(6), 6,  0, 1),
        new BigDecimal(BigInteger.valueOf(7), 7,  0, 1),
        new BigDecimal(BigInteger.valueOf(8), 8,  0, 1),
        new BigDecimal(BigInteger.valueOf(9), 9,  0, 1),
        new BigDecimal(BigInteger.TEN,        10, 0, 2),
    };
  • DIY 数据库连接池实现

    • 每次操作数据库,都创建、关闭数据库连接,性能太低
    • 可考虑预先创建一批连接,放入连接池,用完后归还,实现连接的重用
    • 利用享元模式实现
/**
 * 利用享元模式实现数据库连接池
 */
public class TestJdbcPool {

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        Pool pool = new Pool(2);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                Connection conn = pool.borrow();

                // 模拟执行SQL逻辑
                Sleeper.sleep(0.5);

                // 连接释放,正常一般放在 finally代码块中
                pool.free(conn);
            }).start();
        }
    }
}

/**
 * 该案例未考虑:
 *  1. 连接动态增长与收缩;
 *  2. 连接保活(可用性测试)
 *  3. 等待超时处理;
 *  4. 分布式 hash ?
 * 日常开发:
 *  1. 连接池,数据库类型一般选用成熟的c3po druid hikari
 *  2. 通用连接池,一般选用 apache commons pool 相关 API
 *  3. redis 可以参考 jedis / lettuce 的 内部实现
 */
@Slf4j
class Pool {

    /**
     * 连接池大小
     */
    private final int poolSize;

    /**
     * 连接对象
     */
    private Connection[] connections;

    /**
     * 连接状态,0 标识空闲 1 标识 繁忙,注意要用原子整型数组
     */
    private AtomicIntegerArray states;

    /**
     * 构造方法
     *
     * @param poolSize
     */
    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 MockConnection("连接[" + i + "]");
        }
    }

    /**
     * 借连接,对比看,这种“锁”是加在了数组元素上,相比直接使用 synchronized 性能高很多
     *
     * @return
     */
    public Connection borrow() {

        while (true) {
            for (int i = 0; i < poolSize; i++) {
                if (states.get(i) == 0) {
                    // CAS
                    boolean b = states.compareAndSet(i, 0, 1);
                    if (b) {
                        Connection connection = connections[i];
                        log.info("获取到连接 {}", connection);
                        return connection;
                    }
                }
            }
            // 如果没有空闲连接,进入休息室等待,这里思考为什么要使用如下逻辑?去掉不行?
            synchronized (this) {
                try {
                    log.info("连接用完了,等待获取连接");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 归还连接
     *
     * @param conn
     */
    public void free(Connection conn) {
        for (int i = 0; i < poolSize; i++) {
            if (connections[i] == conn) {
                // 这里其实不会发生竞争,因为来归还连接的线程一定是当前持有连接的线程,所以直接用 set ?
                states.set(i, 0);
                // 唤醒等待获得连接的阻塞线程
                synchronized (this) {
                    log.info("归还连接 {}", conn);
                    this.notifyAll();
                }
                break;
            }
        }
    }
}

/**
 * mock
 */
@ToString
class MockConnection implements Connection {

    /**
     * 连接名称
     */
    private String name;

    public MockConnection(String name) {
        this.name = name;
    }

    @Override
    public Statement createStatement() throws SQLException {
        return null;
    }
    // ……  略
}
  • 测试效果
22:59:47.161 [Thread-2] INFO test.Pool - 连接用完了,等待获取连接
22:59:47.161 [Thread-0] INFO test.Pool - 获取到连接 MockConnection(name=连接[0])
22:59:47.161 [Thread-1] INFO test.Pool - 获取到连接 MockConnection(name=连接[1])
22:59:47.164 [Thread-4] INFO test.Pool - 连接用完了,等待获取连接
22:59:47.164 [Thread-3] INFO test.Pool - 连接用完了,等待获取连接
22:59:47.667 [Thread-0] INFO test.Pool - 归还连接 MockConnection(name=连接[0])
22:59:47.667 [Thread-3] INFO test.Pool - 获取到连接 MockConnection(name=连接[0])
22:59:47.667 [Thread-4] INFO test.Pool - 获取到连接 MockConnection(name=连接[1])
22:59:47.667 [Thread-2] INFO test.Pool - 连接用完了,等待获取连接
22:59:47.667 [Thread-1] INFO test.Pool - 归还连接 MockConnection(name=连接[1])
22:59:47.667 [Thread-2] INFO test.Pool - 连接用完了,等待获取连接
22:59:48.167 [Thread-3] INFO test.Pool - 归还连接 MockConnection(name=连接[0])
22:59:48.167 [Thread-2] INFO test.Pool - 获取到连接 MockConnection(name=连接[0])
22:59:48.167 [Thread-4] INFO test.Pool - 归还连接 MockConnection(name=连接[1])
22:59:48.679 [Thread-2] INFO test.Pool - 归还连接 MockConnection(name=连接[0])

final 的原理

  • 对比 volatile 理解
  • 底层是 putfield 指令实现,后面加入写屏障,保证多线程取值时不会出现 0 情况
  • 观察被 final 修饰的变量其字节码,直接生成在栈中,在栈内存中可直接获取到
  • 数字比较小时,相当于直接在栈内存中
  • 数字超过了最大缓存值时,就存储在常量池中,用LDC质量获取
  • 不加 final 时,相当于是从堆内存中获取,访问要慢

无状态设计案例

  • Servlet 举例,一般建议,不要为 Servlet 设置成员变量
  • 成员变量不可保证其线程安全性,一般成为有状态信息
  • 对应没有成员变量则成为无状态信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值