138.【JUC并发编程- 03】

JUC并发编程- 03

  • (六)、共享模型之无锁
    • 1.问题提出
        • (1).为什么不安全?
        • (2).安全实现_使用锁
        • (3).安全实现_使用CAS
    • 2.CAS与volatile
        • (1).CAS_原理介绍
        • (2).CAS_Debug分析
        • (3).volatile
        • (4).为什么无锁效率高
        • (5).CAS的特点
    • 3.原子整形
        • (1).原子整数类型_ 自增自减
        • (2).原子整数类型_乘除模
    • 4. 原子引用类型
        • (1).不安全的实现
        • (2).安全实现_使用锁
        • (3).安全实现_使用 CAS (AtomicReference)
        • (4).ABA 问题
        • (5).ABA问题解决1_AtomicStampedReference
        • (6).ABA问题解决2_AtomicMarkableReference
    • 5.原子数组
        • (1).不安全的实现
        • (2).安全实现_ 使用CAS
    • 6.字段更新器
        • (1).未使用voliatile修饰的时候会报异常
        • (2).使用voliatile修饰后
        • (3).存在问题ABA问题
    • 7.原子累加器
        • (1).运用累加器类性能更高
        • (2).为什么性能高呢?
        • (4).源码之 LongAdder
        • (5).cas 锁源码
        • (6).原理之伪共享
        • (7).add源码
    • 8.UnSafe
        • (1).概述
        • (2).获取UnSafe实列
        • (3).UnSafe的Case操作
        • (4).模拟原子整数
  • (七)、共享模型之不可变
    • 1.日期转换的问题
        • (1).问题提出
        • (2).思路 - 同步锁
        • (3).思路 - 不可变
    • 2.不可变设计
        • (1).final 的使用
        • (2).保护性拷贝
    • 3.享元模式
        • (1). 简介
        • (2).体现
        • (3).DIY
    • 4.finnal原理
        • (1).设置 final 变量的原理
        • (2).获取final 变量的原理
    • 5.无状态

(六)、共享模型之无锁

  • CAS 与 volatile
  • 原子整数
  • 原子引用
  • 原子累加器
  • Unsafe

1.问题提出

有如下需求,能保证 account.withdraw 取款方法的线程安全嘛?

package com.jsxs.Test;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author Jsxs
 * @Date 2023/10/11 14:58
 * @PackageName:com.jsxs.Test
 * @ClassName: Account
 * @Description: TODO
 * @Version 1.0
 */
public 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");
    }
}

原有实现并不是线程安全的

package com.jsxs.Test;

/**
 * @Author Jsxs
 * @Date 2023/10/11 14:59
 * @PackageName:com.jsxs.Test
 * @ClassName: AccountUnsafe
 * @Description: TODO
 * @Version 1.0
 */
public 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;
    }
	// 进行测试
    public static void main(String[] args) {
        Account.demo(new AccountUnsafe(10000));
    }
}

某次的执行结果

138.【JUC并发编程- 03】_第1张图片

(1).为什么不安全?

withdraw 方法: 对共享资源产生了竞态条件

    public void withdraw(Integer amount) {
        balance -= amount;
    }

对应的字节码

ALOAD 0 // <- this
ALOAD 0
GETFIELD cn/itcast/AccountUnsafe.balance : Ljava/lang/Integer; // <- this.balance
INVOKEVIRTUAL java/lang/Integer.intValue ()I // 拆箱
ALOAD 1 // <- amount
INVOKEVIRTUAL java/lang/Integer.intValue ()I // 拆箱
ISUB // 减法
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; // 结果装箱
PUTFIELD cn/itcast/AccountUnsafe.balance : Ljava/lang/Integer; // -> this.balance

多线程执行流程

ALOAD 0 // thread-0 <- this 
ALOAD 0 
GETFIELD cn/itcast/AccountUnsafe.balance // thread-0 <- this.balance 
INVOKEVIRTUAL java/lang/Integer.intValue // thread-0 拆箱
ALOAD 1 // thread-0 <- amount 
INVOKEVIRTUAL java/lang/Integer.intValue // thread-0 拆箱
ISUB // thread-0 减法
INVOKESTATIC java/lang/Integer.valueOf // thread-0 结果装箱
PUTFIELD cn/itcast/AccountUnsafe.balance // thread-0 -> this.balance 
 
ALOAD 0 // thread-1 <- this 
ALOAD 0 
GETFIELD cn/itcast/AccountUnsafe.balance // thread-1 <- this.balance 
INVOKEVIRTUAL java/lang/Integer.intValue // thread-1 拆箱
ALOAD 1 // thread-1 <- amount 
INVOKEVIRTUAL java/lang/Integer.intValue // thread-1 拆箱
ISUB // thread-1 减法
INVOKESTATIC java/lang/Integer.valueOf // thread-1 结果装箱
PUTFIELD cn/itcast/AccountUnsafe.balance // thread-1 -> this.balance
(2).安全实现_使用锁

首先想到的是给 Account 对象加锁

138.【JUC并发编程- 03】_第2张图片

(3).安全实现_使用CAS
package com.jsxs.Test;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author Jsxs
 * @Date 2023/10/11 15:15
 * @PackageName:com.jsxs.Test
 * @ClassName: AccountSafe
 * @Description: TODO
 * @Version 1.0
 */
public class AccountSafe implements Account {

    // 1. AtomicInteger 原子性整形  ⭐
    private AtomicInteger balance;

    public AccountSafe(Integer balance) {

        this.balance = new AtomicInteger(balance);  // ⭐⭐ 通过AtomicInteger 进行构造
    }

    @Override
    public Integer getBalance() {
        return balance.get();  //  ⭐⭐⭐ 调用原子类的get方法
    }

    @Override
    public void withdraw(Integer amount) {

        while (true) {
            // 获取余额的最新值 调用get方法   ⭐⭐⭐
            int prev = balance.get();
            // 修改后的余额=余额的最新值减去取款金额  ⭐⭐⭐⭐
            int next = prev - amount;
            // 局部变量都是存储在线程的工作内存上(并不是主存上),  调用这个方法就是讲内存和主存同步 ⭐⭐⭐⭐⭐
            if (balance.compareAndSet(prev, next)) { // 第一个参数: 修改前的值,第二个参数: 修改后的值
                break;
            }
        }

        // 可以简化为下面的方法
        // balance.addAndGet(-1 * amount);
        
    }

    public static void main(String[] args) {
        Account.demo(new AccountSafe(10000));
    }
}

138.【JUC并发编程- 03】_第3张图片

2.CAS与volatile

(1).CAS_原理介绍

前面看到的 AtomicInteger 的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?

    public void withdraw(Integer amount) {
        // 需要不断尝试,直到成功为止
        while (true) {
            // 比如拿到了旧值 1000
            int prev = balance.get();
            // 在这个基础上 1000-10 = 990
            int next = prev - amount;
 /*
 compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
 - 不一致了,next 作废,返回 false 表示失败
 比如,别的线程已经做了减法,当前值已经被减成了 990
 那么本线程的这次 990 就作废了,进入 while 下次循环重试
 - 一致,以 next 设置为新值,返回 true 表示成功
 */
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }

其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。

解释:

比如说线程1先获取余额为100,然后减去10结果为90,此时还没有来得及进行cas的操作,线程2已经把原来100的值修改为了90。经比较线程2的将prev90更新到主存,主存prev为90与线程1获取的prev值100是不相等,那么返回fasle,线程1再次进行重新-10操作.

138.【JUC并发编程- 03】_第4张图片

注意

  • 其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。

  • 在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。

(2).CAS_Debug分析

1. 设置线程数为1,然后在CAS的地方进行断点
138.【JUC并发编程- 03】_第5张图片
2.查看断点达到的信息,并且给共享变量赋值9000
138.【JUC并发编程- 03】_第6张图片
3.获取最新值再次比较,如果其他线程没有再次改变那么就返回true
138.【JUC并发编程- 03】_第7张图片

(3).volatile

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。

它可以用来修饰成员变量静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存即一个线程对 volatile 变量的修改,对另一个线程可见。

注意
volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,也能保证程序的有序性,但不能解决指令交错问题(不能保证原子性)

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

(4).为什么无锁效率高
  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞
  • 打个比喻:线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
  • 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

线程数不超过CPU的核定线程数都很高效,假如超过了效率就会大幅降低。

(5).CAS的特点

结合 CASvolatile 可以实现无锁并发适用于线程数少、多核 CPU 的场景下

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
  • CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。

3.原子整形

J.U.C 并发包提供了:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

以 AtomicInteger 为例

(1).原子整数类型_ 自增自减

原子类的所有方法都是线程安全的,都具有原子性CAS自旋的操作。所以刚才的取账问题我们可以用 addAndGet() 方法进行替换。

package com.jsxs.Test;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author Jsxs
 * @Date 2023/10/11 17:11
 * @PackageName:com.jsxs.Test
 * @ClassName: Test09
 * @Description: TODO
 * @Version 1.0
 */
public class Test09 {
    public static void main(String[] args) {
        AtomicInteger integer = new AtomicInteger(1);
        System.out.println("先获取再自增的操作" + integer.getAndIncrement());  // i++
        System.out.println("先自增再获取的操作:" + integer.incrementAndGet());  // ++i
        System.out.println("先自减再获取的操作:"+integer.decrementAndGet());   // --i
        System.out.println("先获取再自检的操作:"+integer.getAndDecrement());   // i--
        System.out.println("最终得值为:"+integer);
        System.out.println("================================");
        System.out.println("自定义后增加:"+integer.getAndAdd(4));  // 先获取再+4
        System.out.println("最终得值为:"+integer);
        System.out.println("自定义先增加:"+integer.addAndGet(4));  // 先自增4 在获取
        System.out.println("最终得值为:"+integer);
        System.out.println("================================");
        
    }
}

138.【JUC并发编程- 03】_第8张图片
默认都是cas自旋的操作
138.【JUC并发编程- 03】_第9张图片

(2).原子整数类型_乘除模
package com.jsxs.Test;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author Jsxs
 * @Date 2023/10/11 17:11
 * @PackageName:com.jsxs.Test
 * @ClassName: Test09
 * @Description: TODO
 * @Version 1.0
 */
public class Test09 {
    public static void main(String[] args) {
        AtomicInteger integer = new AtomicInteger(1);
        // 这里是一个接口类型返回值类型是int,且参数类型是int,   value是读取到的值, value*10是设置的值  会发生CAS自旋的操作
        System.out.println(integer.updateAndGet((value)->{return value*10;}));  // 先修改在获取之
        System.out.println(integer.getAndUpdate((value)->{return value*10;}));  // 先获取值再修改
        
    }
}

138.【JUC并发编程- 03】_第10张图片
默认都是CAS自旋的操作
138.【JUC并发编程- 03】_第11张图片

4. 原子引用类型

为什么需要原子引用类型?

  • AtomicReference
  • AtomicMarkableReference
  • AtomicStampedReference

有如下方法

package com.jsxs.Test;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author Jsxs
 * @Date 2023/10/11 19:28
 * @PackageName:com.jsxs.Test
 * @ClassName: DecimalAccount
 * @Description: TODO
 * @Version 1.0
 */
public interface DecimalAccount {
    
    // 获取余额:  钱币类型
    BigDecimal getBalance();

    // 取款
    void withdraw(BigDecimal amount);

    /**
     * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
     * 如果初始余额为 10000 那么正确的结果应当是 0
     */
    static void demo(DecimalAccount account) {

        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(BigDecimal.TEN);
            }));
        }
        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(account.getBalance());
    }
}
(1).不安全的实现
package com.jsxs.Test;

import java.math.BigDecimal;

/**
 * @Author Jsxs
 * @Date 2023/10/11 19:30
 * @PackageName:com.jsxs.Test
 * @ClassName: DecimalAccountUnsafe
 * @Description: TODO
 * @Version 1.0
 */
public class DecimalAccountUnsafe implements DecimalAccount {

    BigDecimal balance;

    public DecimalAccountUnsafe(BigDecimal balance) {
        this.balance = balance;
    }

    @Override
    public BigDecimal getBalance() {
        return balance;
    }

    @Override
    public void withdraw(BigDecimal amount) {
        BigDecimal balance = this.getBalance();
        this.balance = balance.subtract(amount);
    }

    public static void main(String[] args) {
        DecimalAccount.demo(new DecimalAccountUnsafe(new BigDecimal("10000")));
    }
}

138.【JUC并发编程- 03】_第12张图片

(2).安全实现_使用锁
package com.jsxs.Test;

import java.math.BigDecimal;

/**
 * @Author Jsxs
 * @Date 2023/10/11 20:09
 * @PackageName:com.jsxs.Test
 * @ClassName: DecimalAccountSafeLock
 * @Description: TODO
 * @Version 1.0
 */
public class DecimalAccountSafeLock implements DecimalAccount{
    private final Object lock= new Object();

    BigDecimal balance;

    public DecimalAccountSafeLock(BigDecimal balance) {
        this.balance = balance;
    }

    @Override
    public BigDecimal getBalance() {
        return balance;
    }

    @Override
    public void withdraw(BigDecimal amount) {
        synchronized (lock){
            BigDecimal balance = this.getBalance();
            // 旧余额 - 取款 = 新余额
            this.balance = balance.subtract(amount);
        }
    }

    public static void main(String[] args) {
        DecimalAccount.demo(new DecimalAccountSafeLock(new BigDecimal("10000")));
    }

}

(3).安全实现_使用 CAS (AtomicReference)
package com.jsxs.Test;

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @Author Jsxs
 * @Date 2023/10/11 19:38
 * @PackageName:com.jsxs.Test
 * @ClassName: DecimalAccountSafeCas
 * @Description: TODO
 * @Version 1.0
 */
public class DecimalAccountSafeCas implements DecimalAccount {
    // 1.我们这里使用原子引用类型进行修饰 ⭐
    AtomicReference<BigDecimal> ref;

    public DecimalAccountSafeCas(BigDecimal balance) {
        // 2.创建我们的原子引用类 ⭐⭐
        ref = new AtomicReference<>(balance);
    }

    @Override
    public BigDecimal getBalance() {
        // 3.调用get()返回最新值 ⭐⭐⭐
        return ref.get();
    }

    @Override
    public void withdraw(BigDecimal amount) {
        // 4. CAS  ⭐⭐⭐⭐
        while (true) {
            // 获取现在的余额 (旧值)
            BigDecimal prev = ref.get();
            // 返回的新余额(新值) = 现在的余额 - 取款金额
            BigDecimal next = prev.subtract(amount);
            if (ref.compareAndSet(prev, next)) {  // 进行我们的CAS操作,  主存与新值进行比较,一样则false。
                break;
            }
        }
    }

    public static void main(String[] args) {
        DecimalAccount.demo(new DecimalAccountSafeCas(new BigDecimal("10000")));  // 构造金额为10000
    }
}

138.【JUC并发编程- 03】_第13张图片

(4).ABA 问题
  1. 无线程冲突的时候
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicReference;

/**
 * @Author Jsxs
 * @Date 2023/10/11 20:18
 * @PackageName:com.jsxs.Test
 * @ClassName: Test10
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test10")
public class Test10 {
    // 1. 设置原子引用类型
    static AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取值 A
        String prev = ref.get();
        Thread.sleep(1000);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
    }
}

138.【JUC并发编程- 03】_第14张图片

  1. ABA问题的展现

A->B->A->C

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicReference;

/**
 * @Author Jsxs
 * @Date 2023/10/11 20:18
 * @PackageName:com.jsxs.Test
 * @ClassName: Test10
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test10")
public class Test10 {
    // 1. 设置原子引用类型
    static AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取值 A
        // 这个共享变量被它线程修改过?
        String prev = ref.get();
        other();
        Thread.sleep(1000);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "A"));
    }

    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
        }, "t1").start();
        Thread.sleep(500);
        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
        }, "t2").start();
    }
}

ABA问题成立!!!!
138.【JUC并发编程- 03】_第15张图片

  1. 估计将A的地址进行修改之后
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicReference;

/**
 * @Author Jsxs
 * @Date 2023/10/11 20:18
 * @PackageName:com.jsxs.Test
 * @ClassName: Test10
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test10")
public class Test10 {
    // 1. 设置原子引用类型
    static AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取值 A
        // 这个共享变量被它线程修改过?
        String prev = ref.get();
        other();
        Thread.sleep(1000);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "A"));
    }

    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
        }, "t1").start();
        Thread.sleep(500);
        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.get(), new String("A")));
        }, "t2").start();
    }
}

我们发现ABA问题将不成立!!!!
138.【JUC并发编程- 03】_第16张图片
主线程仅能判断出共享变量的值最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程希望:

(5).ABA问题解决1_AtomicStampedReference

只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @Author Jsxs
 * @Date 2023/10/11 20:18
 * @PackageName:com.jsxs.Test
 * @ClassName: Test10
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test10")
public class Test10 {
    // 1. 设置原子版本引用类型
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取值 A
        String prev = ref.getReference();
        // 获取版本号 (提前获取⭐在其他线程进入前)
        int stamp = ref.getStamp();
        log.debug("版本 {}", stamp);
        // 如果中间有其它线程干扰,发生了 ABA 现象
        other();
        Thread.sleep(1000);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));  // 判断stamp当前版本号,与主存中的版本号是否一致
    }

    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t1").start();

        Thread.sleep(500);

        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t2").start();
    }
}

138.【JUC并发编程- 03】_第17张图片

AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A ->C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次

(6).ABA问题解决2_AtomicMarkableReference

但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference.
138.【JUC并发编程- 03】_第18张图片

package com.jsxs.Test;

/**
 * @Author Jsxs
 * @Date 2023/10/11 21:23
 * @PackageName:com.jsxs.Test
 * @ClassName: GarbageBag
 * @Description: TODO
 * @Version 1.0
 */
public class GarbageBag {

    String desc;

    public GarbageBag(String desc) {
        this.desc = desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return super.toString() + " " + desc;
    }
}
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicMarkableReference;

/**
 * @Author Jsxs
 * @Date 2023/10/11 21:24
 * @PackageName:com.jsxs.Test
 * @ClassName: TestABAAtomicMarkableReference
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test007")
public class TestABAAtomicMarkableReference {
    public static void main(String[] args) throws InterruptedException {

        GarbageBag bag = new GarbageBag("装满了垃圾");

        // 参数2 mark 可以看作一个标记,true表示垃圾袋满了  ⭐
        AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);

        log.debug("主线程 start...");
        // 1.获取余额
        GarbageBag prev = ref.getReference();
        log.debug(prev.toString());

        // 2.保洁阿姨打扫卫生...
        new Thread(() -> {
            log.debug("打扫卫生的线程 start...");
            bag.setDesc("空垃圾袋");
            // 3. 保洁阿姨对判断是否是空垃圾袋  (也就是新的和主存是否一致)
            while (!ref.compareAndSet(bag, bag, true, false)) {
            }
            log.debug(bag.toString());
        }).start();

        Thread.sleep(1000);

        log.debug("主线程想换一只新垃圾袋?");
        boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
        log.debug("换了么?" + success);
        log.debug(ref.getReference().toString());
    }
}

依然是同一个垃圾袋...

138.【JUC并发编程- 03】_第19张图片

5.原子数组

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

原子数组主要保护的是我们数组中的元素!!!

(1).不安全的实现
package com.jsxs.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * @Author Jsxs
 * @Date 2023/10/13 19:15
 * @PackageName:com.jsxs.Test
 * @ClassName: Test11
 * @Description: TODO
 * @Version 1.0
 */
public class Test11 {
    public static void main(String[] args) {

        demo(
                ()->new int[10],   // ⭐非原子数组的创建
                (array)->array.length,
                (array,index)->array[index]++,
                (array)-> System.out.println(Arrays.toString(array))
        );
    }

    /**
     * 参数1,提供数组、可以是线程不安全数组或线程安全数组
     * 参数2,获取数组长度的方法
     * 参数3,自增方法,回传 array, index
     * 参数4,打印数组的方法
     */
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
    private static <T> void demo(

            Supplier<T> arraySupplier,  // 1.没有参数一个结果
            Function<T, Integer> lengthFun,  // 2. 一个参数一个结果
            BiConsumer<T, Integer> putConsumer,  // 3.两个参数,一个结果的
            Consumer<T> printConsumer) {  // 4.一个参数,但没有返回值 (用作打印)

        List<Thread> ts = new ArrayList<>();
        // 1.获取数组
        T array = arraySupplier.get();
        // 2.设置数组的长度
        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);
    }
}

138.【JUC并发编程- 03】_第20张图片

(2).安全实现_ 使用CAS
package com.jsxs.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * @Author Jsxs
 * @Date 2023/10/13 19:15
 * @PackageName:com.jsxs.Test
 * @ClassName: Test11
 * @Description: TODO
 * @Version 1.0
 */
public class Test11 {
    public static void main(String[] args) {

        demo(
                ()->new AtomicIntegerArray(10),  // 1.创建数组 
                (array)->array.length(),   // 2. 提供长度
                (array,index)->array.getAndIncrement(index),  // 3.对原子数组中的元素(下标)进行 +1 的操作
                (array)-> System.out.println(array)
        );
    }

    /**
     * 参数1,提供数组、可以是线程不安全数组或线程安全数组
     * 参数2,获取数组长度的方法
     * 参数3,自增方法,回传 array, index
     * 参数4,打印数组的方法
     */
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
    private static <T> void demo(

            Supplier<T> arraySupplier,  // 1.没有参数一个结果
            Function<T, Integer> lengthFun,  // 2. 一个参数一个结果
            BiConsumer<T, Integer> putConsumer,  // 3.两个参数,一个结果的
            Consumer<T> printConsumer) {  // 4.一个参数,但没有返回值 (用作打印)

        List<Thread> ts = new ArrayList<>();
        // 1.获取数组
        T array = arraySupplier.get();
        // 2.设置数组的长度
        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);
    }
}

138.【JUC并发编程- 03】_第21张图片

6.字段更新器

  • AtomicReferenceFieldUpdater // 域 字段
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

字段更新器保护的是对象的属性和成员变量!!!

利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常。

(1).未使用voliatile修饰的时候会报异常
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 * @Author Jsxs
 * @Date 2023/10/13 20:10
 * @PackageName:com.jsxs.Test
 * @ClassName: Test12
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test12")
public class Test12 {
    public static void main(String[] args) {
        Student student = new Student();

        // 1.第一个参数: 类名; 第二个参数: 参数类型; 第三个: 参数名
        AtomicReferenceFieldUpdater updater =AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");

        // 1.第一个参数: 对象名; 第二个参数:属性的原始值; 第三个参数: 修改成什么值
        updater.compareAndSet(student,null,"张三");
    }
}

class Student {
    String name;  // 我们这里没有使用 voliate

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

138.【JUC并发编程- 03】_第22张图片

(2).使用voliatile修饰后

为什么要添加voliatile进行修饰呢? 为了保证我们保护的成员变量的可见性!!!

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 * @Author Jsxs
 * @Date 2023/10/13 20:10
 * @PackageName:com.jsxs.Test
 * @ClassName: Test12
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test12")
public class Test12 {
    public static void main(String[] args) {
        Student student = new Student();

        // 1.第一个参数: 类名; 第二个参数: 参数类型; 第三个: 参数名
        AtomicReferenceFieldUpdater updater =AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");

        // 1.第一个参数: 对象名; 第二个参数:属性的原始值; 第三个参数: 修改成什么值
        updater.compareAndSet(student,null,"张三");

        System.out.println(student.toString());
    }
}

class Student {
    volatile String name;

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

138.【JUC并发编程- 03】_第23张图片

(3).存在问题ABA问题
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 * @Author Jsxs
 * @Date 2023/10/13 20:10
 * @PackageName:com.jsxs.Test
 * @ClassName: Test12
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test12")
public class Test12 {
    public static void main(String[] args) {
        Student student = new Student();

        // 1.第一个参数: 类名; 第二个参数: 参数类型; 第三个: 参数名  ⭐
        AtomicReferenceFieldUpdater updater =AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");

        // 2.第一个参数: 对象名; 第二个参数:属性的原始值; 第三个参数: 修改成什么值 ⭐⭐
        updater.compareAndSet(student,null,"张三");
        System.out.println(student.toString());

        // 3.原始值指的正确,那么会成功。⭐⭐⭐

        updater.compareAndSet(student,"张三","张四");
        System.out.println(student.toString());

        // 4.原始值指的更改过的,那么会失败。 底层原理还是CAS进行比较的 ⭐⭐⭐⭐
        updater.compareAndSet(student,"张三","张五");
        System.out.println(student.toString());

    }
}

class Student {
    volatile String name;

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

138.【JUC并发编程- 03】_第24张图片

7.原子累加器

累加器性能比较

(1).运用累加器类性能更高
package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * @Author Jsxs
 * @Date 2023/10/13 20:33
 * @PackageName:com.jsxs.Test
 * @ClassName: Test13
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test13")
public class Test13 {
    public static void main(String[] args) {

        demo(
                ()->new AtomicLong(0),   // 1.设置我们的原子长整型
                (adder)->adder.getAndIncrement()  //2.进行自增的操作
        );

        System.out.println("======上面是原子长整型类,下面是长整型累加器=======");

        demo(
                ()->new LongAdder(),
                (adder)->adder.increment()
        );

    }


    /**
     *
     * @param adderSupplier  : ()->结果 ,提供累加器对象
     * @param action :  (参数)-> 无返回结果,  提供累加操作
     * @param 
     */
    private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
        T adder = adderSupplier.get();
        long start = System.nanoTime();
        List<Thread> ts = new ArrayList<>();
        // 4 个线程,每人累加 50 万
        for (int i = 0; i < 40; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    action.accept(adder);
                }
            }));
        }
        ts.forEach(t -> t.start());
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(adder + " cost:" + (end - start) / 1000_000);
    }
}

138.【JUC并发编程- 03】_第25张图片

(2).为什么性能高呢?

性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。相当于不共享一个累加单元,每一个线程都有一个自己的累加单元!!!

(4).源码之 LongAdder

LongAdder 是并发大师 @author Doug Lea (大哥李)的作品,设计的非常精巧

LongAdder 类有几个关键域

        // 累加单元数组, 懒惰初始化
        transient volatile Cell[] cells;
        // 基础值, 如果没有竞争, 则用 cas 累加这个域
        transient volatile long base;
        // 在 cells 创建或扩容时, 置为 1, 表示加锁
        transient volatile int cellsBusy;
(5).cas 锁源码

CAS锁的源码实践,但是实际开发中我们不用用下面这样的代码进行开发

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author Jsxs
 * @Date 2023/10/15 11:45
 * @PackageName:com.jsxs.Test
 * @ClassName: LockCas
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j(topic = "c.test")
public class LockCas {

    // 0的时候表示没有加锁,1的时候表示已经加锁
    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);
    }

    public static void main(String[] args) {
        LockCas lock = new LockCas();
        // 1.线程1执行完毕解锁之后,线程2才能继续执行
        new Thread(() -> {
            log.debug("begin...");
            lock.lock();
            try {
                log.debug("lock...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                lock.unlock();
            }
        }).start();

        new Thread(() -> {
            log.debug("begin...");
            lock.lock();
            try {
                log.debug("lock...");
            } finally {
                lock.unlock();
            }
        }).start();
    }
}
(6).原理之伪共享

其中 Cell 即为累加单元

@sun.misc.Contended   //⭐注解: 防止缓存行伪共享
static final class Cell {
    volatile long value;

    Cell(long x) {
        value = x;
    }

    // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
    final boolean cas(long prev, long next) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
    }
    // 省略不重要代码
}

得从缓存说起,缓存与内存的速度比较

138.【JUC并发编程- 03】_第26张图片
为什么设置那么多的缓存呢?
138.【JUC并发编程- 03】_第27张图片

因为 CPU 与 内存速度差异很大,需要靠预读数据至缓存来提升效率。

而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)

缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中。 也就是相当于取两份。

CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效。

138.【JUC并发编程- 03】_第28张图片
因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象。这样问题来了:

  • Core-0 要修改 Cell[0]
  • Core-1 要修改 Cell[1]

无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效

@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效。

138.【JUC并发编程- 03】_第29张图片

(7).add源码
    public void add(long x) {
        // as 为累加单元数组
        // b 为基础值
        // x 为累加值
        Cell[] as;
        long b, v;
        int m;
        Cell a;
        // 进入 if 的两个条件
        // 1. as 有值, 表示已经发生过竞争, 进入 if
        // 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            // uncontended 表示 cell 没有竞争
            boolean uncontended = true;
            if (
                // as 还没有创建
                    as == null || (m = as.length - 1) < 0 ||
                            // 当前线程对应的 cell 还没有
                            (a = as[getProbe() & m]) == null ||
                            // cas 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell )
                            !(uncontended = a.cas(v = a.value, v + x))
            ) {
                // 进入 cell 数组创建、cell 创建的流程
                longAccumulate(x, null, uncontended);
            }
        }
    }

138.【JUC并发编程- 03】_第30张图片

8.UnSafe

(1).概述

Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得。

138.【JUC并发编程- 03】_第31张图片

(2).获取UnSafe实列
package com.jsxs.Test;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * @Author Jsxs
 * @Date 2023/10/18 21:00
 * @PackageName:com.jsxs.Test
 * @ClassName: Test14
 * @Description: TODO
 * @Version 1.0
 */
public class Test14 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        // 1.通过反射指定属性名获取具体属性
        Field unsafe = Unsafe.class.getDeclaredField("theUnsafe");
        // 2.因为是私有的成员变量,所以我们需要先设置Accessible
        unsafe.setAccessible(true);
        // 3.返回我们的真正的UnSafe对象
        Unsafe o = (Unsafe)unsafe.get(null);
        System.out.println(o);
    }
}

138.【JUC并发编程- 03】_第32张图片

(3).UnSafe的Case操作
package com.jsxs.Test;

import lombok.Data;
import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * @Author Jsxs
 * @Date 2023/10/18 21:00
 * @PackageName:com.jsxs.Test
 * @ClassName: Test14
 * @Description: TODO
 * @Version 1.0
 */
public class Test14 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        // 1.通过反射指定属性名获取具体属性
        Field o = Unsafe.class.getDeclaredField("theUnsafe");
        // 2.因为是私有的成员变量,所以我们需要先设置Accessible
        o.setAccessible(true);
        // 3.返回我们的真正的UnSafe对象
        Unsafe unsafe = (Unsafe)o.get(null);
        System.out.println(o);

        // 1.获取欲的偏移地址 :  获取Student1中id属性的偏移地址
        long idOffset = unsafe.objectFieldOffset(Student1.class.getDeclaredField("id"));
        long nameOffset = unsafe.objectFieldOffset(Student1.class.getDeclaredField("name"));

        Student1 student1 = new Student1();

        // 2.执行 cas 操作
        unsafe.compareAndSwapInt(student1,idOffset,0,1);  // 第一个: 对象, 第二个: id的偏移地址,第三个: 旧值,第四个: 新值
        unsafe.compareAndSwapObject(student1,nameOffset,null,"张三");  // 第一个: 对象, 第二个: id的偏移地址,第三个: 旧值,第四个: 新值
        
        // 3.查看我们是否成功!! 
        System.out.println(student1.id+" "+ student1.name);

    }
}

@Data
class Student1 {
    volatile int id;
    volatile String name;
}

138.【JUC并发编程- 03】_第33张图片

(4).模拟原子整数

使用自定义的 AtomicData 实现之前线程安全的原子整数 Account 实现。

1.Account接口

package com.jsxs.Test;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author Jsxs
 * @Date 2023/10/11 14:58
 * @PackageName:com.jsxs.Test
 * @ClassName: Account
 * @Description: TODO
 * @Version 1.0
 */
public 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(() -> {
                // 每次取10块钱
                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");
    }
}

2.实现

package com.jsxs.Test;

import com.jsxs.utils.UnSafeUtils;
import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * @Author Jsxs
 * @Date 2023/10/18 21:32
 * @PackageName:com.jsxs.Test
 * @ClassName: Test15
 * @Description: TODO
 * @Version 1.0
 */
public class Test15 {
    public static void main(String[] args) {
        Account.demo(new MyAtomicInteger(10000));
    }
}


class MyAtomicInteger implements Account{

    volatile int value;
     static final long valueOffset;
    static Unsafe unsafe;


    static {
        try {
            // 1.通过反射机制进行获取UnSafe ⭐
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);

            // 2.获取我们偏移域
            valueOffset=unsafe.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));

        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
    }

    public int getValue() {
        return value;
    }

    public void decrement(int amount){
        while (true){
            // 1.旧值
            int prev = this.value;
            // 2.新值
            int next=prev-amount;
            // 3.利用 UnSafe 进行设置  ⭐⭐
            if (unsafe.compareAndSwapInt(this,valueOffset,prev,next)){
                break;
            }

        }
    }

    @Override
    public Integer getBalance() {
        return getValue();
    }

    public MyAtomicInteger(int value) {
        this.value = value;
    }

    @Override
    public void withdraw(Integer amount) {
        decrement(amount);
    }

}

测试成功
138.【JUC并发编程- 03】_第34张图片

(七)、共享模型之不可变

  • 不可变类的使用
  • 不可变类设计
  • 无状态类设计

1.日期转换的问题

(1).问题提出

下面的代码在运行时,由于 SimpleDateFormat 不是线程安全的。

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.text.SimpleDateFormat;

/**
 * @Author Jsxs
 * @Date 2023/10/19 20:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test16
 * @Description: TODO
 * @Version 1.0
 */

@Slf4j(topic = "test16")
public class Test16 {
    public static void main(String[] args) {
        // 1.进行我们的序列化操作
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        // 2.开启十个线程进行解析
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    log.debug("{}", sdf.parse("1951-04-21"));
                } catch (Exception e) {
                    log.error("{}", e);
                }
            }).start();
        }
    }
}

有很大几率出现 java.lang.NumberFormatException (数字类型错误) 或者出现不正确的日期解析结果,例如:

138.【JUC并发编程- 03】_第35张图片

(2).思路 - 同步锁

这样虽能解决问题,但带来的是性能上的损失,并不算很好:

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.text.SimpleDateFormat;

/**
 * @Author Jsxs
 * @Date 2023/10/19 20:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test16
 * @Description: TODO
 * @Version 1.0
 */

@Slf4j(topic = "c.test16")
public class Test16 {
    public static void main(String[] args) {
        // 1.进行我们的序列化操作
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        // 2.开启十个线程进行解析
        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();
        }
    }
}

138.【JUC并发编程- 03】_第36张图片

(3).思路 - 不可变

如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!这样的对象在Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:

package com.jsxs.Test;

import lombok.extern.slf4j.Slf4j;

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

/**
 * @Author Jsxs
 * @Date 2023/10/19 20:17
 * @PackageName:com.jsxs.Test
 * @ClassName: Test16
 * @Description: TODO
 * @Version 1.0
 */

@Slf4j(topic = "c.test16")
public class Test16 {
    public static void main(String[] args) {

        // 1. 利用 线程安全的方法
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                LocalDate date = dtf.parse("2018-10-01", LocalDate::from);
                log.debug("{}", date);
            }).start();
        }
    }
}

138.【JUC并发编程- 03】_第37张图片

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

    // ...

}
(1).final 的使用

发现该类、类中所有属性都是 final

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

但有同学会说,使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是如何实现的,就以 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 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 final char[] value 做出了修改:

    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;
            }
            
        }
        
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset + count);
    }

结果发现也没有,构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】

3.享元模式

(1). 简介

定义 英文名称:Flyweight pattern. 当需要重用数量有限的同一类对象时。

(2).体现
  1. 包装类

在JDK中 BooleanByteShortIntegerLongCharacter 等包装类提供了 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);
    }
  • Byte, Short, Long 缓存的范围都是 -128~127
  • Character 缓存的范围是 0~127
  • Integer的默认范围是 -128~127
    • 最小值不能变
    • 但最大值可以通过调整虚拟机参数 -Djava.lang.Integer.IntegerCache.high 来改变
  • Boolean 缓存了 TRUE 和 FALSE
(3).DIY

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

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];
                    }
                }
            }
            // 如果没有空闲连接,当前线程进入等待
            synchronized (this) {
                try {
                    log.debug("wait...");
                    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);
                synchronized (this) {
                    log.debug("free {}", conn);
                    this.notifyAll();
                }
                break;
            }
        }
    }

}

class MockConnection implements Connection {
    // 实现略
}

4.finnal原理

(1).设置 final 变量的原理

理解了 volatile 原理,再对比 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 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况。 因为finnal不能进行修改的特点。

(2).获取final 变量的原理

Monitor 原理

5.无状态

在 web 阶段学习时,设计 Controller 时为了保证其线程安全,都会有这样的建议,不要为 Controller 设置成员变量,这种没有任何成员变量的类是线程安全的

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

你可能感兴趣的:(java,redis,开发语言)