1、不可变类的的使用
有如下代码
import lombok.extern.slf4j.Slf4j;
import java.text.ParseException;
import java.text.SimpleDateFormat;
@Slf4j(topic = "c.DateFormatMutable")
public class DateFormatMutable {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}",sdf.parse("2021-10-01"));
} catch (ParseException e) {
log.error("{}", e);
}
}).start();
}
}
}
// 测试结果
Exception in thread "Thread-4" Exception in thread "Thread-3" Exception in thread "Thread-1" Exception in thread "Thread-2" java.lang.NumberFormatException: multiple points
解决方法:
- 加锁: 可以实现但是又性能问题
- 使用不可变类:DateTimeFormatter
代码如下:
import lombok.extern.slf4j.Slf4j;
import java.time.format.DateTimeFormatter;
@Slf4j(topic = "c.DateFormatMutable")
public class DateFormatImmutable {
public static void main(String[] args) {
DateTimeFormatter sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
log.debug("{}",sdf.parse("2021-10-01"));
}).start();
}
}
}
// 测试结果
[INFO] --- exec-maven-plugin:3.0.0:exec (default-cli) @ concurrent ---
2021-10-01 10:26:04.363 DEBUG [Thread-8] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-3] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-4] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-2] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-10] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-6] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-1] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-9] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-5] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-7] c.DateFormatMutable - {},ISO resolved to 2021-10-01
简单看下改类的源码
* @implSpec
* This class is immutable and thread-safe.
*
* @since 1.8
- 该类为不可变类和线程安全的
- jdk1.8新引入的类
2、不可变类的设计
另一个我们熟悉的String类也是不可变,以它为例说明一下不可变的要素
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
...
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);
}
...
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);
}
...
}
-
final的使用
- 类中属性用final修饰,保证该属性只读,不能修改
- 类用final修饰保证该类中方法不能被覆盖,防止子类无意间破坏不可变性
-
保护性拷贝
有一些跟修改相关的方法,比如subString,它是如何实现的呢,见源码。发现其内部调用了String的构造方法创建了一个新的字符串。而对应的构造方法在构造新的字符串时,会生成新的char[] value,对内容进行复制。
这种通过创建副本对象来避免共享的手段称之为保护性拷贝(defensive copy)。
3、享元模式-定义和体现
3.1、定义
不可变类在尝试修改对象时,当前对象会创建一个新对象,保证线程安全,但是这样带来一个问题就是对象创建过多,会消耗资源,性能变低。如何解决呢?不可变类一般会通过关联设计模式-享元模式类解决。
定义:a flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects。
适用:当需要重用数量有限的同一类对象
归类:structual pattern
3.2、体现
3.2.1、包装类
在JDK中Boolean、Byte、short、Integer、Long、Character等包装类提供了valueOf()方法。例如Long的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);
}
private static class LongCache {
private LongCache(){}
static final Long cache[] = new Long[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
tips
- Byte,Short,Long缓存的范围都是-128~127
- Character缓存范围:0~127
- Integer默认缓存范围-128~127,最小值不可变;单最大值可以通过调整虚拟机参数-Djava.lang.Integer.IntegerCache.high来改变
- Boolean缓存了TRUE和FALSE
3.2.2、String串池
自行查阅相关文档,这里不做详解,等学习JVM时在讨论。
3.2.3、BigDecimal BigInteger
自行查阅相关文档,这里不做详解,等学习JVM时在讨论。
4、享元模式-不可变线程安全辨析
我们说不可变类是线程安全的,但是之前我们的银行取款案例中为什么要用AtomicReference包含BigDecimal的线程安全呢? 来看下代码
import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
/**
* CAS无锁实现
*/
public class AccountDecimalCAS implements AccountDecimal {
private final AtomicReference<BigDecimal> balance;
public AccountDecimalCAS(BigDecimal balance) {
this.balance = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBalance() {
return balance.get();
}
@Override
public void withdraw(BigDecimal amount) {
balance.getAndUpdate(m -> m.subtract(amount));
}
// @Override
// public void withdraw(BigDecimal amount) {
// while (true) {
// BigDecimal prev = balance.get();
// BigDecimal next = prev.subtract(amount);
// if (balance.compareAndSet(prev, next)) {
// break;
// }
// }
// }
}
观察被注释的部分,其中BigDecimal的substract方法是原子的,源码
public BigDecimal subtract(BigDecimal subtrahend) {
if (this.intCompact != INFLATED) {
if ((subtrahend.intCompact != INFLATED)) {
return add(this.intCompact, this.scale, -subtrahend.intCompact, subtrahend.scale);
} else {
return add(this.intCompact, this.scale, subtrahend.intVal.negate(), subtrahend.scale);
}
} else {
if ((subtrahend.intCompact != INFLATED)) {
// Pair of subtrahend values given before pair of
// values from this BigDecimal to avoid need for
// method overloading on the specialized add method
return add(-subtrahend.intCompact, subtrahend.scale, this.intVal, this.scale);
} else {
return add(this.intVal, this.scale, subtrahend.intVal.negate(), subtrahend.scale);
}
}
}
private static BigDecimal add(final long xs, int scale1, final long ys, int scale2) {
long sdiff = (long) scale1 - scale2;
if (sdiff == 0) {
return add(xs, ys, scale1);
} else if (sdiff < 0) {
int raise = checkScale(xs,-sdiff);
long scaledX = longMultiplyPowerTen(xs, raise);
if (scaledX != INFLATED) {
return add(scaledX, ys, scale2);
} else {
BigInteger bigsum = bigMultiplyPowerTen(xs,raise).add(ys);
return ((xs^ys)>=0) ? // same sign test
new BigDecimal(bigsum, INFLATED, scale2, 0)
: valueOf(bigsum, scale2, 0);
}
} else {
int raise = checkScale(ys,sdiff);
long scaledY = longMultiplyPowerTen(ys, raise);
if (scaledY != INFLATED) {
return add(xs, scaledY, scale1);
} else {
BigInteger bigsum = bigMultiplyPowerTen(ys,raise).add(xs);
return ((xs^ys)>=0) ?
new BigDecimal(bigsum, INFLATED, scale1, 0)
: valueOf(bigsum, scale1, 0);
}
}
}
当前while选好中其他任意单个方法也是原子的,但是方法组合起来不是原子操作,所以需要AtomicReference来保证整个方法的原子性,实现线程安全。
-
结论
- 不可变类的单个方法是原子的,线程安全的
- 但是多个方法组合不是原子的,需要额外的方式保证线程安全性
5、享元模式-自定义连接池
学习过不可变类,了解享元模式后,我们利用所学知识来实现个简单的自定义连接池。
-
场景:一个线上商城应用,QPS达到数千,如果每次都重新创建和关闭数据库连接,性能都会受到极大影响。这时,预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接;使用完毕后,归还连接池中。这样既节约了时间和资源,实现了重用,能及时响应客户端请求,不至于给数据库造成过大的负担。
-
代码如下
import lombok.extern.slf4j.Slf4j; import java.sql.Connection; import java.util.concurrent.atomic.AtomicIntegerArray; /** * 自定义线程连接池 */ @Slf4j(topic = "c.Pool") public final class Pool { /** 连接池大小 */ private final int poolSize; /** 连接数组 */ private final Connection[] conns; /** 连接是否被占用标记,0-未占用,1-已占用 */ private final AtomicIntegerArray busies; /** * 构造方法:初始化连接池 * @param poolSize 初始连接池大小 */ public Pool(int poolSize) { this.poolSize = poolSize; conns = new Connection[poolSize]; busies = new AtomicIntegerArray(new int[poolSize]); for (int i = 0; i < poolSize; i++) { conns[i] = new MockConnection("conn" + i); } } /** * 从连接池获取连接 * @return 连接 */ public Connection getConnection() { // 检查是否有空闲连接 while (true) { for (int i = 0; i < poolSize; i++) { // 有空闲连接返回连接 if (busies.get(i) == 0) { // 空闲,修改标记,返回连接 if (busies.compareAndSet(i, 0, 1)) { log.debug("get {}", conns[i]); return conns[i]; } } } // 没有空闲连接等待 synchronized (this) { try { log.debug("wait..."); this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * 归还连接 * @param conn 要归还的连接 */ public void close(Connection conn) { for (int i = 0; i < poolSize; i++) { if (conns[i] == conn) { // 归还连接不会发生竞争 busies.set(i, 0); synchronized (this) { log.debug("close {}", conn); this.notifyAll(); } break; } } } } /** * 连接 */ @Data @NoArgsConstructor @AllArgsConstructor public class MockConnection implements Connection { private String name; ... } import java.sql.Connection; import java.util.concurrent.TimeUnit; /** * 测试连接池 */ public class PoolTest { public static void main(String[] args) { Pool p = new Pool(2); for (int i = 0; i < 5; i++) { new Thread(() -> { Connection connection = p.getConnection(); try { // 模拟连接使用耗时 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } finally { p.close(connection); } }).start(); } } } // 测试结果 2021-10-01 11:33:32.443 DEBUG [Thread-3] c.Pool - wait... 2021-10-01 11:33:32.443 DEBUG [Thread-1] c.Pool - get MockConnection(name=conn0) 2021-10-01 11:33:32.443 DEBUG [Thread-2] c.Pool - get MockConnection(name=conn1) 2021-10-01 11:33:32.445 DEBUG [Thread-5] c.Pool - wait... 2021-10-01 11:33:32.445 DEBUG [Thread-4] c.Pool - wait... 2021-10-01 11:33:33.445 DEBUG [Thread-2] c.Pool - close MockConnection(name=conn1) 2021-10-01 11:33:33.445 DEBUG [Thread-4] c.Pool - get MockConnection(name=conn0) 2021-10-01 11:33:33.445 DEBUG [Thread-5] c.Pool - get MockConnection(name=conn1) 2021-10-01 11:33:33.445 DEBUG [Thread-3] c.Pool - wait... 2021-10-01 11:33:33.445 DEBUG [Thread-1] c.Pool - close MockConnection(name=conn0) 2021-10-01 11:33:33.445 DEBUG [Thread-3] c.Pool - wait... 2021-10-01 11:33:34.445 DEBUG [Thread-4] c.Pool - close MockConnection(name=conn0) 2021-10-01 11:33:34.445 DEBUG [Thread-3] c.Pool - get MockConnection(name=conn0) 2021-10-01 11:33:34.445 DEBUG [Thread-5] c.Pool - close MockConnection(name=conn1) 2021-10-01 11:33:35.445 DEBUG [Thread-3] c.Pool - close MockConnection(name=conn0)
问题:
- 连接池的动态增加和减少
- 连接保活(可用性检测)
- 等待超时处理
- 分布式hash
- …
在实际应用中,我们应该使用已经成熟的连接池实现。关系型数据库,例如C3P0、druid等。更通用的对象连接池,可以考虑apache commons pool,等等。以后学习框架相关知识的时候,有时间我们可以看下成熟连接的是如何实现的(源码)。
6、final原理
6.1、设置final变量的原理
理解了volatile原理,在对比final的实现就毕竟简单了,简单示例及字节码
public class FinalTest {
final int a = 20;
}
// class version 52.0 (52)
// access flags 0x21
public class com/gaogzhen/final01/FinalTest {
// compiled from: FinalTest.java
// access flags 0x10
final I a = 20
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 4 L1
ALOAD 0
BIPUSH 20
PUTFIELD com/gaogzhen/final01/FinalTest.a : I
<-- 写屏障
RETURN
L2
LOCALVARIABLE this Lcom/gaogzhen/final01/FinalTest; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}
发现final变量的赋值也会通过putfield指令来完成,同样在这条指令之后也会加入写屏障,保证其它线程读到它的值时不会出现为0的情况。
6.2、获取final变量的原理
final读取优化
- 较小数值:复制一份到栈内存读取、
- 较大数值:常量池
- 不加final读取堆内存,性能较低
7、无状态
在web阶段学习时,设计Servlet时,为了保证其线程安全,都会有这样的建议,不要为Servlet设置成员变量,这种没有任何成员变量的类是线程安全的。
因为成员变量保存的数据也可以称之为状态信息,没有成员变量称之为无状态。
QQ:806797785
仓库地址:https://gitee.com/gaogzhen/concurrent