08 不可变类的使用与设计,享元模式的理解和应用

一 不可变对象的设计与实现

1-1 为什么需要不可变类(可变类在多线程环境下的安全性实例)

  • 不可变类具有线程安全的特点
SimpleDateFormat类的使用问题
package chapter7;
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
@Slf4j(topic = "c.test1")
public class test1 {
    public static void main(String[] args) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        for(int i = 0;i < 20;++i){
            new Thread(()->{
                try{
                    log.warn("{}",sdf.parse("1951-04-21"));
                }catch (Exception e){
                    log.error("{}",e);
                }
            }).start();
        }
    }
}

执行结果

  • SimpleDateFormat类在多线程环境下使用会导致NumberFormatException错误。
  • SimpleDataFormat类是可变的类。
[Thread-1] WARN c.test1 - Fri Apr 21 00:00:00 CST 1195
[Thread-2] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-6] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-16] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-14] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-9] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-15] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-12] WARN c.test1 - Fri Apr 21 00:00:00 CST 21210000
[Thread-4] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-7] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-17] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-11] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-13] WARN c.test1 - Fri Apr 21 00:00:00 CST 21210000
[Thread-3] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-8] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-10] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-5] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-0] ERROR c.test1 - {}
java.lang.NumberFormatException: For input string: ".44E1.44E1"
	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:2056)
	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 chapter7.test1.lambda$main$0(test1.java:14)
	at java.lang.Thread.run(Thread.java:748)
[Thread-18] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
[Thread-19] WARN c.test1 - Sat Apr 21 00:00:00 CST 1951
SimpleDateFormat类的问题的解决

思路1:采用synchronized或者reentrantlock 在调用方法加锁。

思路2: 采用SimpleDateFormat类对应的不可变类解决线程安全问题。

  • JDK8中提供了DateTimeFormatter这个线程安全的不可变类

DateTimeFormatter.java源码中有这样的注释

  • This class is immutable and thread-safe.
  • 注意这个类也是final修饰的
 * @implSpec
 * This class is immutable and thread-safe.
 *
 * @since 1.8
 */
public final class DateTimeFormatter {
package chapter7;
import lombok.extern.slf4j.Slf4j;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
@Slf4j(topic = "c.test1")
public class test1 {
    public static void main(String[] args) {
        DateTimeFormatter stf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        for(int i = 0;i < 20;++i){
            new Thread(()->{
                TemporalAccessor parse = stf.parse("1951-04-21");
                log.warn("{}",parse);
            }).start();
        }
    }
}

执行结果

  • 不可变对象保证了线程的安全性
[Thread-6] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-16] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-8] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-18] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-17] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-7] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-12] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-3] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-10] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-5] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-14] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-19] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-9] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-2] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-0] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-11] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-4] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-1] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-13] WARN c.test1 - {},ISO resolved to 1951-04-21
[Thread-15] WARN c.test1 - {},ISO resolved to 1951-04-21
问题:String类为什么设计成final?

答(从效率与安全性考虑):1.为了实现字符串池 2.为了线程安全 3.为了实现String可以创建HashCode不可变性。

1-2 如何设计不可变类

1-2-1 String类的分析

JDK1.8的源码

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    // final保证value引用不可被更改
    private final char value[];

    /** Cache the hash code for the string */
    // private
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
  • 属性(成员变量)采用保证成员都是只读的,不可修改的
  • 类采用final方法该类中的方法不会被override,避免子类破坏类的不可变性。
1-2-2 String如何保证数组中的内容中的不可变性呢?

分析源码的构造方法

  • 对于传入的String对象,会与原始String对象共用value数组。
  • 对于char数组,会将char数组的内容拷贝到value数组中。
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
1-2-3 String类中的保护性拷贝思想(defensive copy)的体现

保护性拷贝的定义:通过创建副本对象来避免共享的手段称之为保护性拷贝(defensive copy).

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);
	}
	return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
// ---------------------------------------------------------------------------
// 可以看到substring的value数组也是通过拷贝原有value数组的一部分内容. 没有对之前的value数组进行修改。
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);
}
1-2-4 BigDecimcal BigInteger

该类也是线程安全的,同样采用了保护性拷贝保证类的不可变性。

    private static BigDecimal add(long xs, long ys, int scale){
        long sum = add(xs, ys);
        if (sum!=INFLATED)
            return BigDecimal.valueOf(sum, scale);
        return new BigDecimal(BigInteger.valueOf(xs).add(ys), scale);
    }
  • 可以看到上面的add方法返回的是一个新建的BigDecimal对象。

重点复习:类单个方法的原子性不代表几个方法组合使用的原子性。

二 不可变类与享元模式(Flyweight Pattern)

享元模式的定义

2-1 享元模式的定义

定义

 flyweight is a software design pattern. A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects; it is a way to use objects in large numbers when a simple repeated representation would use an unacceptable amount of memory. Often some parts of the object state can be shared, and it is common practice to hold them in external data structures and pass them to the objects temporarily when they are used.
  • 通过与其他相似的obeject共享尽可能多的数据的objects(减少内存的占用)

2-2 JDK中享元模式的体现(待完善)

2-2-1 包装类中享元模式的体现
  • 在Integer.java中有class IntegerCache。这个类中缓存了数值范围为 [-128, 127] 的Integer对象
  • 缓存会在第一次使用的时候初始化(The cache is initialized on first usage.)

    /**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     *
     * The cache is initialized on first usage.  The size of the cache
     * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * sun.misc.VM class.
     */

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

其他的包装类也有类似的机制:

1. Byte,Long,Short的缓存范围都是 -128~127
2. Character的缓存范围是 0~127.
3. Integer的默认范围是-128-127,最小值无法改变,最大值可以通过调整虚拟机参数 `
-Djava.lang.Integer.IntegerCache.high` 来改变。
4. Boolean 缓存了 TRUE 和 FALSE
2-2-2 String类中享元模式的体现(串池)

三 基于享元模式的自定义连接池

3-1 实现

  • 使用原子引用类型保存连接池的使用状态
  • 当线程发现没有连接可以用的时候,需要wait,当线程释放连接的时候,需要去notify
package chapter7;
import lombok.extern.slf4j.Slf4j;
import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicIntegerArray;

@Slf4j(topic = "c.pool")
class Pool{
    // 01 连接池大小
    private final int poolSize;
    // 02 连接对象数组
    private Connection[] connections;
    // 03 连接状态数组,0表示空闲,1表示繁忙
    private AtomicIntegerArray states;
    Pool(int poolSize){
        this.poolSize = poolSize;
        this.states = new AtomicIntegerArray(new int[poolSize]);
        this.connections = new Connection[poolSize];
        for(int i = 0;i < poolSize;++i){
            this.connections[i] = new MockConnection();
        }
    }
    // 04 获取一个连接对象
    public Connection borrow(){
        while(true){
            for(int i = 0;i < poolSize;++i){
                if(states.get(i) == 0){
                    // 注意:这里必须采用CAS操作确保多个线程状态变量的安全性
                    states.compareAndSet(i,0,1);
                    log.warn("borrow connection {}",i);
                    return connections[i];
                }
            }

            // 如果没有空闲线程,则让该线程进入waitset等待其他线程释放连接资源
            
            synchronized (this){
                try{
                    log.warn("wait");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    // 05 释放一个连接对象。
    public void free(Connection conn){
        for(int i = 0;i < poolSize;++i){
            if(connections[i] == conn){
                states.set(i,0);
                log.warn("free connection {}",i);
                // 释放资源,并通知其他线程已经有资源释放了
                synchronized (this){
                    this.notifyAll();
                }
                break;
            }
        }
    }
}


public class test2 {
    public static void main(String[] args) {
        Pool pool = new Pool(2);
        for(int i = 0;i < 5;++i){
            new Thread(()->{
                Connection tmp = pool.borrow();
                try {
                    Thread.sleep(new Random().nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                pool.free(tmp);
            }).start();
        }
    }
}

// mock:虚假的
// 这里实现了一个虚假的连接池对象
class MockConnection implements Connection{
    .....
}

上面的连接池实现:多线程环境下,通过原子引用类型提供CAS机制(原子引用类型封装的变量中有volatile)实现了对连接对象的原子更新,不需要通过synchronized去锁住整个对象,因此程序的并发度很高。

3-1-1 为什么线程需要在没有连接池资源的情况下,调用wait?

如果不调用wait,线程会不停的进行循环,会增加CAS操作的机率,由于CAS操作适合线程数不超过CPU核心数的场景,因此我们需要让无法获得资源的线程wait从而避免无用的CAS冲突。

3-2 线程池可以优化的点?

  • 连接的动态增长与收缩(即实现线程池大小的自适应调整)
  • 连接保活(无法保证所有连接都是有效的)
  • 等待超时(wait()的超时处理)
  • 分布式hash

3-3 实际项目中连接池的使用

对于关系型数据库,有比较成熟的连接池实现,例如c3p0, druid等 对于更通用的对象池,可以考虑使用apache
commons pool,例如redis连接池可以参考jedis中关于连接池的实现

四 从volatile理解final变量的原理

4-0 final的复习

  • 当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;
  • 如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。
  • final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式
    • 一种是在变量声明的时候初始化
    • 第二种方法是在声明变量的时候不赋初值,在这个变量所在的类的所有的构造函数中对这个变量赋初值。

final关键字的好处:

(1)final关键字提高了性能。JVM和Java应用都会缓存final变量。

(2)final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。

(3)使用final关键字,JVM会对方法、变量及类进行优化。

JAVA面试50讲之2:final关键字的底层原理是什么?

4-1 设置final变量的原理(写屏障)

public class TestFinal {
    final int a = 20;
}

字节码

  • 变量的赋值也会通过 putfield 指令来完成
  • 同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况
    • 写屏障,保证对final变量的写入在屏障前面
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

4-2 获取final变量的原理

package chapter7;

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;
    final void test1(){
    }
}

class UseFinal{
    public void test(){
        /* A由于加了final修饰,UseFinal读取的时候会直接将A拷贝到栈内存然后读取(优化),不会从TestFinal获取 ,效率高*/
        System.out.println(TestFinal.A);
        /* B由于加了final修饰,UseFinal读取的时候会直接将B拷贝到常量池读取(优化),不会从TestFinal获取,效率高*/
        System.out.println(TestFinal.B);
        System.out.println(new TestFinal().a);
        System.out.println(new TestFinal().b);

    }
}
  • 上面的代码中的变量如果不加final,则在堆中读取变量。

五 无状态的类也是线程安全的

​ 设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这
没有任何成员变量的类是线程安全的

  • 因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】

六 总结

  • 不可变类使用
  • 不可变类设计
  • 原理方面
    final
  • 模式方面
    享元

七 参考资料

多线程基础课程


20210328

原文地址:https://www.cnblogs.com/kfcuj/p/14604871.html