三、Java并发编程之乐观锁、不可变类

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的区别

    • AtomicIntegerFieldUpdaterstaic 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 无状态

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值