三、Java并发编程之乐观锁、不可变类
创始人
2025-05-28 21:29:44

文章目录

  • 6. 无锁并发 - 乐观锁
    • 6.1 引入
      • 不安全的代码
      • synchronized改进
      • 无锁方案
    • 6.2 CAS
    • 6.3 CAS工具包
      • 原子整数
      • 原子引用
        • AtomicReference
        • AtomicStampedReference - ABA问题
        • AtomicMarkableReference
      • 原子数组
        • 准备代码
        • 不安全的调用
        • 安全的调用
      • 字段更新器
      • 原子累加器
        • LongAdder原理 - CAS锁
        • LongAdder原理 - 缓存行伪共享
        • LongAdder源码
      • Unsafe
        • 反射获取theUnsafe
        • Unsafe CAS操作
        • 模拟原子整数类
  • 7. 不可变
    • 7.1 可变类的线程不安全
      • 问题引入
      • 加锁方案
      • 不可变类
    • 7.2 不可变类的设计
      • 保护性拷贝
    • 7.3 结构模式之享元模式
      • 包装类
      • 应用-自定义连接池
      • final原理
    • 7.4 无状态

6. 无锁并发 - 乐观锁

乐观锁

操作共享资源时,总是很乐观,认为自己可以成功。在操作失败时(资源被其他线程占用),并不会挂起阻塞,而仅仅是返回,并且失败的线程可以重试。

优点:

  • 不会死锁
  • 不会饥饿
  • 不会因竞争造成系统开销

6.1 引入

不安全的代码

分析下面的转账代码,考虑是否线程安全

interface Account {// 获取余额Integer getBalance();// 取款void withdraw(Integer amount);/*** 方法内会启动 1000 个线程,每个线程做 -10 元 的操作* 如果初始余额为 10000 那么正确的结果应当是 0*/static void demo(Account account) {List 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;}@Overridepublic Integer getBalance() {return balance;}@Overridepublic 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);}@Overridepublic Integer getBalance() {return balance.get();}@Overridepublic 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){//模拟updateAndSetwhile(true){int prev = i.get();int next = prev * 10;if(i.compareAndSet(prev, next)){break;}}
}

上面的写法缺乏通用性,可以把操作抽象出来

public static int updateAndGet(AtomicInteger i, IntUnaryOperator operator){//模拟updateAndSetwhile(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 balance;public DecimalAccountCas(BigDecimal balance){this.balance = new AtomicReference<>(balance);}@Overridepublic BigDecimal getBalance() {return balance.get();}@Overridepublic 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 ref = new AtomicStampedReference<>("A", 0);public static void main(String[] args) throws InterruptedException {//获取修改前的参数String prev = ref.getReference();int stamp = ref.getStamp();//修改时要传入旧值进行比对:依次传入 旧值,修改值,旧的版本号,以及版本号+1boolean 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 get();
}
//Function:(参数) -> 结果
public interface Function {R apply(T t);
}
//BiConsumer:(参数1, 参数2) -> void
public interface BiConsumer {void accept(T t, U u);
}
//Consumer:(参数) -> void
public interface Consumer {void accept(T t);
}

demo函数

  • 创建长度为10的array数组
  • 开启10个线程
  • 每个线程对array数组做1万次自增,最终10万次自增均摊到每个数组元素之上
  • 线程安全的情况下,每个数组元素最终为 10000
private static  void demo(Supplier arraySupplier, Function lengthFun, BiConsumer putConsumer, Consumer printConsumer ) {List 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的区别

    • AtomicIntegerFieldUpdaterstaic final类型,即类变量,并不会占用当前对象的内存

    • 当字段所属的类会被创建大量的实例时,如果用AtomicInteger每个实例里面都要创建AtomicInteger对象,占用更多内存

原子累加器

private static  void demo(Supplier addrSupplier, Consumer action){T addr = addrSupplier.get();List 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)))//执行累加单元的caslongAccumulate(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 initializationh = getProbe();wasUncontended = true;}boolean collide = false;                // True if last slot nonemptyfor (;;) {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 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,将它包装为RuntimeExceptionthrow 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;}@Overridepublic Integer getBalance() {return getValue();}@Overridepublic 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-safeDateTimeFormatter 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 Stringimplements java.io.Serializable, Comparable, 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 cachereturn 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;}@Overridepublic 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; iconnections[i] = new MockConnection("连接"+(i+1));}}//5. 借连接public Connection borrow(){while(true){for(int i=0; iif(states.get(i) == 0){if(states.compareAndSet(i, 0, 1)){log.debug("borrow {}", connections[i]);return connections[i];}}}//如果没有空闲连接,使当前线程等待//加入下面的wait()代码,是为了避免当前线程一直使用CAS操作,导致忙等占用cpusynchronized (this){try{log.debug("waiting...");this.wait();}catch (InterruptedException e){e.printStackTrace();}}}}//6. 归还连接public void free(Connection conn){for(int i=0; iif(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."":()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 无状态

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

相关内容

热门资讯

【数据结构】KMP算法细节详解 KMP算法细节详解前言一、字符串匹配问题1.BF算法2.KMP算法二、next数组三、手写nex思想...
【中间间】Redis与MySQ... 文章目录前言谈谈一致性三个经典的缓存模式Cache-Aside PatternCache-Aside...
【CSS】盒子模型内边距 ④ ... 文章目录一、盒子模型内部尺寸计算1、设置内边距和边框对盒子模型的影响2、盒子模型尺寸计算二、代码示例...
TIA博途中添加程序注释的具体... TIA博途中添加程序注释的具体方法示例_汇总 添加程序注释可以帮助自己和阅读程序的技术人员更好地理...
[LsSDK][tool] l... 文章目录一、首先是界面介绍。二、工具的目的三、ls_gpio.h模板四、ls_syscfg.h 模板...
Linux的目录结构 目录 一:重要性和基本介绍 二:目录结构​编辑 ​编辑 2.1 bin...
java中IO流的操作 对于java中io流的一些操作和类进行总结 io流的分类:  字节流:...
HydroD 实用教程(七)静... 目 录一、前言二、稳性分析三、Hydrostatic Rule Checks四、AVCG Analy...
记录使用Dockerfile来... 一准备一个安装了docker的虚拟机 首先准备一个安装好了docker的虚拟机,我的d...
Nginx学习笔记(三)Lin... 目录一、官网下载二、配置基本信息1.上传 Linux2.解压3.安装编译环境4.执行命令4.1 配置...
怎样展示你在项目中的重要性? 今天我们聊聊面试中,怎样介绍你的项目,以及怎样突出你的重要性。面试中除了...
LA-Lib库c++环境下的编... 下载地址一:GitHub仓库 下载地址二:(内含己编译一个...
在 AI 上训练 AI:Cha... ChatGPT 可以像 Linux 终端一样运行,并在给出以下提示时返回执行结果。下面...
import 或者requir... 最近看别人写的源码时发现一个有趣的现象, import {promises as f...
通过sysfs文件系统接口来改... 通过sysfs文件系统接口来改变内核模块中的变量值(二) 文章目录通过...
JavaWeb《一》概念、服务... 🍎道阻且长,行则将至。🍓 本文是javaweb的...
iOS上架App Store详... 上架基本需求资料 1、苹果开发者账号(如还没账号先申请- 苹果开发者账号申请教程&#x...
【LeetCode每日一题】—... 文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目...
代码随想录算法训练营第三十五天... 860.柠檬水找零 题目链接 思路: 情况一:账单是5,直...
VsCoe离线安装插件【步骤+... 文章目录1、下载插件2、安装 svn插件为例2.1、下载插件2.2、安装2.3、查看安装插件 前言...
【C++】13.多态 1.多态的概念 通俗来说,就是多种形态,具体点就是去完成某个行为...
光伏并网逆变器学习1-simu... 分为三个部分:光伏电池 、 boost+mppt 、 并网逆变 光伏电池&#...
WEB前端第三次作业——CSS... WEB前端第三次作业——CSS样式案例 做出如下图中的效果 用到的图片素材均来源于对应网站源代码 ...
Liunx下的进程空间地址理解... 文章目录前言1.进程空间地址1.从编程语言角度理解地址的划分2.进程虚拟地址3.扩展1.为啥要有虚拟...
【尊享版】你真的会休息吗?如何... 超友们,早上好~ 今天我为你带来的分享是《你真的会休息吗?如何开始正确的...
数据更新 | CnOpenDa... 证券从业人员信息数据 一、数据简介   证券从业人员是指被中国证监会依法批准的证券从业机构正式聘用或...
HTML 音频(Audio) HTML 音频(Audio) 声音在HTML中可以以不同的方式播放. 问题以及解决方法 在 HTML...
树莓派/linux/ubunt... 问题描述需要给树莓派设置静态ip并且要可以连接网络。但实际情况是有静态ip时:ping...
C语言指针操作(十)动态内存分... 目录 一、什么是内存的动态分配 二、怎样建立内存的动态分配 2.1用malloc函数开辟动态存储区 ...
测试怎么做到尽可能全和尽可能快 需求 目标 就上面这样一个很简单的功能点,但是他的测试数据产品一共给了6K多条 本...