文章目录
6. 无锁并发 - 乐观锁
乐观锁
操作共享资源时,总是很乐观,认为自己可以成功。在操作失败时(资源被其他线程占用),并不会挂起阻塞,而仅仅是返回,并且失败的线程可以重试。
优点:
- 不会死锁
- 不会饥饿
- 不会因竞争造成系统开销
6.1 引入
不安全的代码
分析下面的转账代码,考虑是否线程安全
interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance()
+ " cost: " + (end-start)/1000_000 + " ms");
}
}
class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return balance;
}
@Override
public void withdraw(Integer amount) {
balance -= amount;
}
}
测试
Account.demo(new AccountUnsafe(10000));
结果发现余额不为0
180 cost: 105 ms
synchronized改进
@Override
public Integer getBalance() {
synchronized (this){
return this.balance;
}
}
@Override
public void withdraw(Integer amount) {
synchronized (this){
balance -= amount;
}
}
结果
0 cost: 96 ms
无锁方案
class AccountCas implements Account{
private AtomicInteger balance;
public AccountCas(int balance){
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
while(true){
int prev = balance.get(); //获取余额的最新值
int next = prev - amount;
//同步到主存中去
if(balance.compareAndSet(prev, next)){
break;//同步成功,结束循环
}
}
}
}
结果
0 cost: 86 ms
6.2 CAS
关键:compareAndSet
,简称CAS
,它虽然未使用锁,但却是原子操作
- 当线程1执行了 balance - 10 ,但尚未写入到内存中时,此刻线程2又修改了balance
- 显然如果线程1继续执行,将使得写入覆盖,导致错误
- CAS操作对其进行限制,它会在写入时判断此刻balance的值是否和之前一致,如果不一致,就返回false,从而保障安全性
CAS保障原子性的原理
参考:http://www.manongjc.com/detail/29-djgcfcqoczepwyv.html
CPU通过以下方式实现原子性
-
总线锁定
-
总线(BUS)是计算机组件间数据传输方式,也就是说通过总线,CPU与其他组件连接传输数据,就是靠总线完成的,比如CPU对内存的读写。
-
当处理器要操作共享变量时,会在总线上发出 Lock 信号,其他处理器就不能操作这个共享变量了
-
-
缓存锁定
-
总线锁定方式虽然保持了原子性,但是在锁定期间,总线锁定阻止了被阻塞处理器和所有内存之间的通信,而输出LOCK#信号的CPU可能只需要锁住特定的一块内存区域,因此总线锁定开销较大。
-
所以现代CPU为了提升性能,通过锁定范围缩小的思想设计出缓存行锁定(缓存行是CPU高速缓存存储的最小单位)
-
当缓存行中的共享变量回写到内存时,其他 CPU 会通过总线嗅探机制感知该共享变量是否发生变化,如果发生变化,让自己对应的共享变量缓存行失效,重新从内存读取最新的数据
-
乐观锁会消耗CPU资源,悲观锁节省CPU资源但效率慢(需要加锁,甚至阻塞)
CAS和volatie
public class AtomicInteger extends Number implements java.io.Serializable {
...
private volatile int value;
...
}
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
为什么无锁效率高
-
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。一旦发生上下文切换,性能将下降
-
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,如果没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换
-
CAS更适合多核CPU,线程较少的情况
CAS特点
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
- CAS 是基于
乐观锁
的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,再重试即可 - synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,一旦上锁,其他线程不能访问
- CAS 体现的是无锁并发、无阻塞并发
因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
6.3 CAS工具包
原子整数
J.U.C 并发包提供了:
- AtomicBoolean
- AtomicInteger
- AtomicLong
以AtomicInteger为例
AtomicInteger i = new AtomicInteger(0); //无参则初始值为0
//下列函数均有返回值
i.compareAndSet(int expect, int update); //借助while(true),更新i的值
i.getAndIncrement(); //类似i++
i.incrementAndGet(); //类似++i
i.decrementAndGet(); //类似--i
i.getAndDecrement(); //类似i--
i.getAndAdd(5); //先获取,再增加,返回原值
i.addAndGet(-5); //先增加,再获取,返回修改后的值
i.getAndUpdate(p -> p - 2); //获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
i.updateAndGet(p -> p + 2); //更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
i.getAndAccumulate(10, (p, x) -> p + x); //获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
i.accumulateAndGet(-10, (p, x) -> p + x); //计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
模拟updateAndGet
public static void updateAndGet(AtomicInteger i){
//模拟updateAndSet
while(true){
int prev = i.get();
int next = prev * 10;
if(i.compareAndSet(prev, next)){
break;
}
}
}
上面的写法缺乏通用性,可以把操作抽象出来
public static int updateAndGet(AtomicInteger i, IntUnaryOperator operator){
//模拟updateAndSet
while(true){
int prev = i.get();
int next = operator.applyAsInt(prev);
if(i.compareAndSet(prev, next)){
return next;
}
}
}
//使用
System.out.println(updateAndGet(i, p -> p/2));
查看源码,可以看到也是这样的思想
原子引用
- AtomicReference
- AtomicMarkableReference
- AtomicStampedReference
AtomicReference
//DecimalAccount和之前的相似
class DecimalAccountCas implements DecimalAccount{
private AtomicReference<BigDecimal> balance;
public DecimalAccountCas(BigDecimal balance){
this.balance = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBalance() {
return balance.get();
}
@Override
public void withdraw(BigDecimal amount) {
while(true){
BigDecimal prev = balance.get();
BigDecimal next = prev.subtract(amount);
if(balance.compareAndSet(prev, next)){
break;
}
}
}
}
AtomicStampedReference - ABA问题
ABA问题:一个线程把数据A变成了B,然后又重新变成了A,此时另一个线程使用compareAndSet时发现A是对的,但实际上中间已经经过了变化
需求:线程1想要感知变量 ref 的中间变化
解决方案:版本号
@Slf4j(topic = "c.test")
public class ConcurrentApplication {
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
//获取修改前的参数
String prev = ref.getReference();
int stamp = ref.getStamp();
//修改时要传入旧值进行比对:依次传入 旧值,修改值,旧的版本号,以及版本号+1
boolean flag = ref.compareAndSet(prev, "C", stamp,stamp+1);
log.debug("flag = {}, version = {}", flag, ref.getStamp());
}
}
AtomicMarkableReference
AtomicStampedReference是通过版本号,甚至可以知道中间被修改过多少次
如果仅仅需要知道有无修改过,只需要一个 boolean 变量即可
原子数组
- AtomicLongArray
- AtomicIntegerArray
- AtomicReferenceArray
准备代码
AtomicReference 修改的引用本身,现在想要修改引用的对象内容
预备知识,几个函数式接口
//Supplier:() -> 结果
public interface Supplier<T> {
T get();
}
//Function:(参数) -> 结果
public interface Function<T, R> {
R apply(T t);
}
//BiConsumer:(参数1, 参数2) -> void
public interface BiConsumer<T, U> {
void accept(T t, U u);
}
//Consumer:(参数) -> void
public interface Consumer<T> {
void accept(T t);
}
demo函数
- 创建长度为10的array数组
- 开启10个线程
- 每个线程对array数组做1万次自增,最终10万次自增均摊到每个数组元素之上
- 线程安全的情况下,每个数组元素最终为 10000
private static <T> void demo(Supplier<T> arraySupplier, Function<T, Integer> lengthFun, BiConsumer<T, Integer> putConsumer, Consumer<T> printConsumer ) {
List<Thread> ts = new ArrayList<>();
//获取调用者传来的数组
T array = arraySupplier.get();
//得到数组长度
int length = lengthFun.apply(array);
for (int i = 0; i < length; i++) {
// 每个线程对数组作 10000 次操作
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array, j%length);
}
}));
}
ts.forEach(t -> t.start()); // 启动所有线程
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); // 等所有线程结束
//打印数据结果
printConsumer.accept(array);
}
不安全的调用
demo(
()->new int[10],
(array)->array.length,
(array, index) -> array[index]++,
array-> System.out.println(Arrays.toString(array))
);
结果
[5487, 5525, 5502, 5501, 5565, 5565, 5523, 5527, 5524, 5484]
可以看到,元素并没有达到 10000,说明这是多线程下数组是线程不r安全的
安全的调用
- 使用
原子数组
demo(
()->new AtomicIntegerArray(10),
(array)->array.length(),
(array, index) -> array.getAndIncrement(index),
array-> System.out.println(array)
);
结果
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
保障了线程安全
字段更新器
- AtomicReferenceFieldUpdater // 域 字段
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
public class ConcurrentApplication {
public static void main(String[] args) throws InterruptedException {
Student stu = new Student();
AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
//这里的三个参数分别代表,object, expect, update
//如果name属性为null,则更新成功(更改为”张三“),否则更新失败
updater.compareAndSet(stu, null, "张三");
}
}
class Student{
volatile String name;
}
-
关于AtomicIntegerFieldUpdater和AtomicInteger的区别
-
AtomicIntegerFieldUpdater
是staic final
类型,即类变量,并不会占用当前对象的内存 -
当字段所属的类会被创建大量的实例时,如果用
AtomicInteger
每个实例里面都要创建AtomicInteger对象,占用更多内存
-
原子累加器
private static <T> void demo(Supplier<T> addrSupplier, Consumer<T> action){
T addr = addrSupplier.get();
List<Thread> ts = new ArrayList<>();
for(int i=0; i<4; ++i){
ts.add(new Thread(() -> {
for(int j=0; j<500000; j++){
action.accept(addr);
}
}));
}
long start = System.nanoTime();
ts.forEach(t->t.start());
ts.forEach(t->{
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();//纳秒
System.out.println(addr + " cost:" + (end-start)/1000_000);
}
使用AtomicLong做累加
demo(
()->new AtomicLong(0),
(addr) -> addr.getAndIncrement()
);
//输出:2000000 cost:35
使用LongAdder做累加
demo(
()->new LongAdder(),//只能无参,从0开始
(addr) -> addr.increment()
);
//输出:2000000 cost:18
LongAdder和AtomicLong都能保障原子性,但前者性能高出很多
-
性能提升的原因
有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的Cell 变量,因此减少了 CAS 重试失败,从而提高性能
LongAdder原理 - CAS锁
LongAdder 类有几个关键域
transient volatile Cell[] cells;
// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;
// 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;
// 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy;
}
在cells创建或扩容时,仍然需要锁机制来保障安全
CAS锁模拟cellsBusy
public class LockCas {
private AtomicInteger state = new AtomicInteger(0);
public void lock() {
while (true) {
if (state.compareAndSet(0, 1)) {
break;
}
}
}
public void unlock() {
log.debug("unlock...");
state.set(0);
}
}
测试
LockCas lock = new LockCas();
new Thread(() -> {
log.debug("begin...");
lock.lock();
try {
log.debug("lock...");
sleep(1);
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
log.debug("begin...");
lock.lock();
try {
log.debug("lock...");
} finally {
lock.unlock();
}
}).start();
LongAdder原理 - 缓存行伪共享
缓存行伪共享
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
CPU 要保证数据的一致性
,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
-
缓存行伪共享
因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象,此时
- Core-0 要修改 Cell[0]
- Core-1 要修改 Cell[1]
无论谁修改成功,都会导致对方 Core 的缓存行失效
-
@sun.misc.Contended解决伪共享
原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效
@sun.misc.Contended //防止缓存行伪共享 static final class Cell { volatile long value; Cell(long x) { value = x; } final boolean cas(long cmp, long val) { return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val); } }
LongAdder源码
-
add()
public void increment() { add(1L); } public void add(long x) { Cell[] as; long b, v; int m; Cell a; //cells数组是懒惰创建的,一开始为null,表明无竞争 //casBase对基础域进行累加,累加失败说明有竞争 if ((as = cells) != null || !casBase(b = base, b + x)) { //条件不符合,则说明存在竞争,需要使用多个累加单元 boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x)))//执行累加单元的cas longAccumulate(x, null, uncontended);//如果累加失败,还是会进入longAccumulate } }
-
longAccumulate()
cells未创建,或者cells容量不够等,会调用longAccumulate()
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) { int h; if ((h = getProbe()) == 0) { ThreadLocalRandom.current(); // force initialization h = getProbe(); wasUncontended = true; } boolean collide = false; // True if last slot nonempty for (;;) { Cell[] as; Cell a; int n; long v; //第1个if是 累加单元 是否创建的判断,如果为null,或者累加单元不够了,就执行该块代码 //第2个if是 加锁保证创建或扩容cells的原子性 //第3个if是 加锁失败,对base累加,累加成功就break,未成功就继续循环进行下一次尝试 if ((as = cells) != null && (n = as.length) > 0) {...} else if (cellsBusy == 0 && cells == as && casCellsBusy()) {...} else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))){...} } }
-
sum()
对累加单元cells进行合计
public long sum() { Cell[] as = cells; Cell a; long sum = base; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }
Unsafe
Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得
package sun.misc;
public final class Unsafe {
private static final Unsafe theUnsafe;
...
}
- park()等方法底层都是调用Unsafe的方法
- theUnsafe是私有的,只能通过反射获得
Unsafe并不是指线程不安全,而是因为操作底层,因此不建议使用,所有Unsafe
反射获取theUnsafe
//private static final Unsafe theUnsafe;
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
//如果field的name是一个static的变量,field.get(param),param是任意的都可以,返回类中当前静态变量的值
//如果是非静态变量,field.get(obj),obj必须是当前类的实例对象,返回实例对象obj的变量值
System.out.println(unsafe);
Unsafe CAS操作
- 之前是用更高层的Atomic类来使用CAS
- 可以使用Unsafe通过更底层的方式进行CAS操作
//1. 获取域的偏移地址
long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));
//2. 执行CAS操作
Teacher t = new Teacher();
unsafe.compareAndSwapInt(t, idOffset, 0, 1);//给id赋值,且是线程安全的
unsafe.compareAndSwapObject(t, nameOffset, null, "张三");
//3. 验证
System.out.println(t);
模拟原子整数类
需求:模拟一个原子整数类,实现前面的转账功能
Account接口
interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance()
+ " cost: " + (end-start)/1000_000 + " ms");
}
}
原子整数类 MyAtomicInteger
class MyAtomicInteger implements Account{
private volatile int value;
private static final long valueOffset;
static final Unsafe UNSAFE;
static {
UNSAFE = UnsafeAccessor.getUnsafe();
try {
valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
//这里MyAtomicInteger是自定义的,所以肯定不会是NoSuchFieldException,将它包装为RuntimeException
throw new RuntimeException(e);
}
}
public int getValue(){
return value;
}
public void decrement(int amount){
while(true){
int prev = this.value;
int next = prev - amount;
if(UNSAFE.compareAndSwapInt(this, valueOffset, prev, next)){
break;
}
}
}
public MyAtomicInteger(int value){
this.value = value;
}
@Override
public Integer getBalance() {
return getValue();
}
@Override
public void withdraw(Integer amount) {
decrement(amount);
}
}
测试
Account.demo(new MyAtomicInteger(10000));
7. 不可变
即便不使用悲观锁、乐观锁机制,不可变类也是安全的
7.1 可变类的线程不安全
问题引入
以可变类 SimpleDateFormat 为例
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
结果容易出现报错 “empty String”
- 原因:用到了共享变量
calendar
(详细原理暂略)
加锁方案
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (sdf){
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}
}).start();
}
不可变类
public static void main(String[] args) {
//this class is immutable and thread-safe
DateTimeFormatter stf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for(int i=0; i<10; i++){
new Thread(()->{
TemporalAccessor parse = stf.parse("1951-04-21");
log.debug("{}", parse);
}).start();
}
}
7.2 不可变类的设计
以String不可变类示例
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash; // Default to 0
...
}
发现该类、类中所有属性都是 final 的
- 属性用 final 修饰保证了该属性是只读的,不能修改
- 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
注意 final 只能保证引用不被修改,不能保证对象内容不被修改,对此,String进行了如下设置
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
//保护性拷贝
//这样设置后,即使外部的char数组被改变了,也不会影响到String对象
保护性拷贝
以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);
}
其内部是调用 String 的构造方法创建了一个新字符串
通过创建副本对象来避免共享的手段称之为保护性拷贝(defensive copy)
7.3 结构模式之享元模式
保护性拷贝每次需要创建新的对象,享元模式
(Flyweight pattern)可优化这个问题
享元模式:当需要重用数量有限的同一类对象时
包装类
包装类是典型的享元模式
- 在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了
valueOf
方法 - 例如 Long 的 valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才新建 Long 对象
//以Long为里,静态方法LongCache会事先缓存-128到127的对象
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);
}
}
//调用valueOf时,可直接返回缓存中的对象
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);
}
- Byte, Short, Long 缓存的范围都是 -128 ~ 127
- Character缓存的范围是 0 ~ 127
- Integer的默认缓存范围是 ~128 ~ 127,最小值不能变,但最大值可以调虚拟机参数
-Djava.lang.Integer.IntegerCache.high
来改变 - Boolean缓存了 TRUE 和 FALSE
除此之外,String串池
和BigDecimal
BigInteger
也是享元模式,不可变类
之前转账操作时使用BigInteger不能保证线程安全,是因为其单个方法是安全的,但不能保证多个操作的组合
是安全的
应用-自定义连接池
应用场景:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库
连接池:Pool
class MockConnection implements Connection{
private String name;
public MockConnection(String name){
this.name = name;
}
@Override
public String toString() {
return "MockConnection{" +
"name='" + name + '\'' +
'}';
}
//省略一大堆需要implements的方法
}
@Slf4j
class Pool{
//1. 连接池大小
private final int poolSize;
//2. 连接对象数组
private Connection[] connections;
//3. 连接状态数组,0表示空闲,1表示繁忙
private AtomicIntegerArray states;
//4. 构造方法初始化
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+1));
}
}
//5. 借连接
public Connection borrow(){
while(true){
for(int i=0; i<poolSize; ++i){
if(states.get(i) == 0){
if(states.compareAndSet(i, 0, 1)){
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
//如果没有空闲连接,使当前线程等待
//加入下面的wait()代码,是为了避免当前线程一直使用CAS操作,导致忙等占用cpu
synchronized (this){
try{
log.debug("waiting...");
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
//6. 归还连接
public void free(Connection conn){
for(int i=0; i<poolSize; ++i){
if(connections[i] == conn){
states.set(i, 0);
log.debug("free {}", conn);
synchronized (this){
this.notifyAll();
}
break;
}
}
}
}
测试
public static void main(String[] args) {
Pool pool = new Pool(2);
for(int i=0; i<5; ++i){
new Thread(() -> {
Connection conn = pool.borrow();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
pool.free(conn);
}
}).start();
}
}
final原理
final修饰的变量只能被赋值一次,赋值后值不再改变(final要求地址值不能改变)
- 如果final修饰的是基本数据类型,表示该基本数据类型的值一旦在初始化后便不能发生变化
- 如果final修饰的是引用数据类型,其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的
(https://blog.csdn.net/sinat_41653656/article/details/109443253)
final字节码
public class TestFinal {
final int a = 20;
}
字节码为
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
-
写final
如果是普通变量,int a = 20,则分为2步,首先开辟一块空间,然后赋值20,如果在这直接读取a,将会访问到初始值0
final通过加入一个写屏障,阻止了这种读取 -
读final
int k = a; 此时是直接将10复制了一份到 k 所在线程空间中,避免了对原有a的修改
static int A = 10;
final static int B = Short.MAX_VALUE+1;
非final的A走的是共享内存,要去另一个类中获取静态变量(GETSTATIC);final修饰的B可以去常量池中找(使用LDC指令)
内存屏障参考:https://blog.csdn.net/qq_23350817/article/details/126525990
7.4 无状态
因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为无状态