不可变:如果一个对象子不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改。

7.1 不可变类的使用

7.1.1 问题提出

SimpleDateFormat不是线程安全的,下列代码执行时会产生线程安全问题:

@Slf4j(topic = "SimpleDateFormat")
public class SimpleDateFormatDemo {
    private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    public static void main(String[] args) {
        String dateStr = "2001-09-11 00:00:00.000";
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    log.debug(SDF.parse(dateStr).toString());
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

结果:

java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
    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 top.parak.immutable.SimpleDateFormatDemo.lambda$main$0(SimpleDateFormatDemo.java:22)
    at java.lang.Thread.run(Thread.java:748)
2021-04-29 11:30:23.116 [Thread-2] DEBUG SimpleDateFormat - Sun Sep 11 00:00:00 CST 1121
2021-04-29 11:30:23.117 [Thread-9] DEBUG SimpleDateFormat - Sun Sep 11 00:00:00 CST 1121
2021-04-29 11:30:23.117 [Thread-5] DEBUG SimpleDateFormat - Tue Sep 11 00:00:00 CST 2001

7.1.2 同步锁

使用同步锁synchronized能解决安全问题,但是会带来性能问题。

synchronized (SDF) {
    log.debug(SDF.parse(dateStr).toString());
}

7.1.3 不可变

使用JDK1.8中的不可变日期格式类DateTimeFormatter

@Slf4j(topic = "DateTimeFormatter")
public class DateTimeFormatterDemo {
    private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
    public static void main(String[] args) {
        String dateStr = "2001-09-11 00:00:00.000";
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                TemporalAccessor date = DTF.parse(dateStr);
                log.debug("{}", date);
            }).start();
        }
    }
}

7.2 不可变类的设计

7.1.1 final的使用

IntegerDoubleStringDateTimeFormatter以及基本类型包装类,都是用final修饰的。

  • 属性用final修饰保证了该属性是只读的,不能修改
  • 类用final修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

7.2.2 保护性拷贝

Stringsubstring方法为例,方法的最后还是new String

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > value.length) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    int subLen = endIndex - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return ((beginIndex == 0) && (endIndex == value.length)) ? this
        : new String(value, beginIndex, subLen);
}

这种创建副本对象来避免共享的手段称为保护性拷贝(defensive copy)。

7.2.3 模式之享元

定义:运用共享技术来有效地支持大量细粒度对象的复用。

优势:相同对象只保存一份,这降低了系统中对象的数量,降低内存压力。

在JDK中BooleanByteShortLongCharacter等包装类提供了valueOf方法。

例如Longh.valueOf(),在-128~127之间的Long对象,在这个范围内会用缓存对象,超过这个范围,才会信件Long对象。

public static Long valueOf(long l) {
    final int offset = 128;
    if (l >= -128 && l <= 127) { // will cache
        return LongCache.cache[(int)l + offset];
    }
    return new Long(l);
}

注意:

  • ByteShortLong缓存的范围:-128~127
  • Character缓存的范围:0~127
  • Integer的默认范围:-128~127,最小值不能变,最大值通过虚拟机参数-Djava.lang.Integer.IntegerCache.high来改变。
  • Boolean缓存:true / false

7.2.4 DIY连接池

例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。

import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerArray;

/**
 * @author KHighness
 * @since 2021-04-29
 */

public class PoolDemo {
    public static void main(String[] args) {
        Pool pool = new Pool(2);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
               Connection connection = pool.get();
               try {
                   TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               pool.free(connection);
            }, "T-" + (i + 1)).start();
        }
    }
}

@Slf4j(topic = "Pool")
class Pool {

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

    /**
     * 连接数组
     */
    private final Connection[] connections;

    /**
     * 连接状态数组
     */
    private final AtomicIntegerArray states;

    /**
     * 初始化连接池
     */
    public Pool(int pollSize) {
        this.poolSize = pollSize;
        connections = new Connection[pollSize];
        states = new AtomicIntegerArray(new int[pollSize]);
        for (int i = 0; i < pollSize; i++) {
            connections[i] = new ParaKConnection("连接" + (i + 1));
        }
    }

    /**
     * 获取一个连接
     */
    public Connection get() {
        while (true) {
            // 查看是否有空闲连接
            for (int i = 0; i < poolSize; i++) {
                if (states.get(i) == 0) {
                    if (states.compareAndSet(i, 0, 1)) {
                        log.debug("get {}", connections[i]);
                        return connections[i];
                    }
                }
            }
            // 没有空闲连接则等待
            synchronized (this) {
                try {
                    log.debug("wait...");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 释放一个连接
     */
    public void free(Connection connection) {
        for (int i = 0; i < poolSize; i++) {
            if (connections[i] == connection) {
                states.set(i, 0);
                synchronized (this) {
                    log.debug("free {}", connection);
                    this.notifyAll();
                }
                break;
            }
        }
    }
}

class ParaKConnection implements Connection {

    private String name;

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

    @Override
    public String toString() {
        return "ParaKConnection[" +
                "name='" + name + '\'' +
                ']';
    }

    // ...
}

可改进点:

  • 连接的动态增长与收缩
  • 连接保活(可用性检测)
  • 等待超时处理
  • 分布式hash

7.3 final原理

7.3.1 设置final变量的原理

对于以下代码:

public class FinalDemo {
    final int k = 3;
}

init字节码如下:

0 aload_0
1 invokespecial #1 <java/lang/Object.<init>>
4 aload_0
5 iconst_3
6 putfield #2 <top/parak/immutable/FinalDemo.k>
<================== 写屏障
9 return

发现final变量的赋值也会通过putfield指令来完成,同样在这条指令之后也会加入写屏障,保证在其他线程读到它的值时不会出现为0的情况。

7.3.2 获取final变量的原理

对于以下代码:

public class FinalDemo {
    int a = 3;
    static int A = 33333;
    final int b = 3;
    final static int B = 33333;

    public static void main(String[] args) {
        System.out.println(new FinalDemo().a);
        System.out.println(A);
        System.out.println(new FinalDemo().b);
        System.out.println(B);
    }
}

main字节码如下:

 # 获取打印流
 0 getstatic #4 <java/lang/System.out>

 # 打印a
 3 new #5 <top/parak/immutable/FinalDemo>
 6 dup
 7 invokespecial #6 <top/parak/immutable/FinalDemo.<init>>
10 getfield #2 <top/parak/immutable/FinalDemo.a>
13 invokevirtual #7 <java/io/PrintStream.println>

# 打印A
16 getstatic #4 <java/lang/System.out>
# 不加final,获取A变量的时候使用getStatic,使用共享内存
19 getstatic #8 <top/parak/immutable/FinalDemo.A>  
22 invokevirtual #7 <java/io/PrintStream.println>
25 getstatic #4 <java/lang/System.out>

# 打印b
28 new #5 <top/parak/immutable/FinalDemo>
31 dup
32 invokespecial #6 <top/parak/immutable/FinalDemo.<init>>
35 invokevirtual #9 <java/lang/Object.getClass>
38 pop
39 iconst_3
40 invokevirtual #7 <java/io/PrintStream.println>

# 打印B
43 getstatic #4 <java/lang/System.out>
# 加了final,没有直接去获取A变量,而是将A复制到当前Java虚拟机栈中
46 ldc #10 <33333>
48 invokevirtual #7 <java/io/PrintStream.println>
51 return

通过观察字节码可以发现,final修饰的变量有栈内存读取速度的优化。

7.4 无状态

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

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