上学期学的,现在基本忘记完了,很遗憾

Java 内存模型

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。 JMM 体现在以下几个方面 原子性 - 保证指令不会受到线程上下文切换的影响 可见性 - 保证指令不会受 cpu 缓存的影响 有序性 - 保证指令不会受 cpu 指令并行优化的影响

可见性

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

static boolean run = true;
public static void main(String[] args) throws InterruptedException {
 Thread t = new Thread(()->{
 while(run){
 // ....
 }
 });
 t.start();
 sleep(1);
 run = false; // 线程t不会如预想的停下来
}
为什么呢?分析一下:
1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
2. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,
减少对主存中 run 的访问,提高效率
    3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量
的值,结果永远是旧值
​

解决方法

volatile(易变关键字) 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存

可见性 vs 原子性

前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可 见, 不能保证原子性,仅用在一个写线程,多个读线程的情况

synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低

有序性

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; 
j = ...; 

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU 执行指令的原理来理解一下吧

public class Singleton {
    // 未加volatile,可能出现指令重排
    private static Singleton instance;
​
    // 私有构造方法,防止外部new
    private Singleton() {}
​
    // 多线程下的获取实例方法(未加锁,仅用于演示指令重排)
    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查:判断是否为null
            synchronized (Singleton.class) { // 加锁
                if (instance == null) { // 第二次检查:双重校验
                    // new对象时可能发生指令重排:1.分配内存 → 3.引用指向内存 → 2.初始化对象
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
​
    // 测试方法:模拟对象初始化后的属性(若未初始化会报错)
    private String name = "初始化完成的单例对象";
    public String getName() {
        return name;
    }
​
    // 主线程:启动多个线程获取单例
    public static void main(String[] args) {
        // 启动10个线程并发获取单例
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Singleton singleton = Singleton.getInstance();
                // 若拿到半初始化对象,name可能为null(未初始化)
                if (singleton.getName() == null) {
                    System.out.println("线程" + Thread.currentThread().getId() + 
                                       "拿到了半初始化的对象!指令重排发生");
                } else {
                    System.out.println("线程" + Thread.currentThread().getId() + 
                                       "拿到正常对象:" + singleton.getName());
                }
            }).start();
        }
    }
}

当线程 A 执行 instance = new Singleton() 时,若发生指令重排(1→3→2),instance 已经不为 null(步骤 3 完成),但对象的 name 属性还没初始化(步骤 2 未完成)。

此时线程 B 执行第一次检查 if (instance == null),会直接返回这个 “半初始化” 的 instance,调用 getName() 就会拿到 null,触发异常。

这种情况是概率性的(依赖 CPU / 编译器优化),但在高并发场景下很容易复现。

volatile 会在 instance 的写操作前后添加内存屏障

  • 写屏障:禁止将 new 对象的初始化指令(步骤 2)重排到引用赋值(步骤 3)之后。

  • 读屏障:禁止其他线程读取 instance 时,提前执行后续指令。

最终保证:只有当对象完全初始化后,instance 才会指向该内存地址,其他线程永远拿不到半初始化的对象。

happens-before

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛 开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

  1. 锁解锁 - 加锁规则:解锁前的写对后续加锁的读可见

public class LockHappensBefore {
    static int x = 0;
    static Object m = new Object();
​
    public static void main(String[] args) throws InterruptedException {
        // 线程t1:加锁修改x,解锁后写操作对t2可见
        Thread t1 = new Thread(() -> {
            synchronized (m) {
                x = 10; // 解锁前的写操作
            } // 解锁m
        }, "t1");
​
        // 线程t2:加锁读取x,能看到t1的写结果
        Thread t2 = new Thread(() -> {
            synchronized (m) {
                System.out.println("t2读取x:" + x); // 必然输出10,而非默认值0
            }
        }, "t2");
​
        t1.start();
        t1.join(); // 确保t1先执行完解锁
        t2.start();
    }
}
  1. volatile 变量规则:volatile 写对后续 volatile 读可见

线程对 volatile 变量的写操作,对后续其他线程对该变量的读操作可见(同时禁止指令重排)。

public class VolatileHappensBefore {
    volatile static int x = 0; // 必须加volatile
​
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            x = 10; // volatile写操作
        }, "t1");
​
        Thread t2 = new Thread(() -> {
            System.out.println("t2读取volatile x:" + x); // 大概率读到10(若t1先执行)
        }, "t2");
​
        t1.start();
        t2.start();
    }
}

volatile 禁止了指令重排,且保证写操作立即刷新到主内存,读操作直接从主内存读取。

对比非 volatile 变量:如果 x 不加 volatile,t2 可能读到 0(CPU 缓存未同步),加了则能保证可见性(但不保证执行顺序,若 t2 先执行仍会读 0)。

3.线程启动规则:start () 前的写对线程内的读可见

主线程在调用线程 start() 之前对变量的写操作,对该线程启动后的读操作可见。

public class StartHappensBefore {
    static int x = 0;
​
    public static void main(String[] args) {
        x = 10; // start()前的写操作
        Thread t2 = new Thread(() -> {
            System.out.println("t2读取x:" + x); // 必然输出10,而非0
        }, "t2");
        t2.start(); // 启动线程
    }
}
  1. 线程终止规则:线程结束前的写对 join () 后的读可见

规则含义:线程结束前对变量的写操作,对其他线程通过 join() 等待其结束后的读操作可见。

public class JoinHappensBefore {
    static int x = 0;
​
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            x = 10; // 线程结束前的写操作
        }, "t1");
        t1.start();
        t1.join(); // 等待t1结束
        System.out.println("主线程读取x:" + x); // 必然输出10
    }
}
  1. 线程中断规则:interrupt () 前的写对检测中断后的读可见

规则含义:线程 t1 打断 t2(调用 t2.interrupt())前对变量的写,对其他线程检测到 t2 被打断后的读可见。

public class InterruptHappensBefore {
    static int x = 0;
    static Thread t2;
​
    public static void main(String[] args) throws InterruptedException {
        t2 = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                // 循环等待被打断
            }
            System.out.println("t2被打断后读取x:" + x); // 必然输出10
        }, "t2");
        t2.start();
​
        // 主线程修改x,然后打断t2
        x = 10; // interrupt()前的写操作
        t2.interrupt(); // 打断t2
        t2.join();
    }
}
  1. 默认值规则:默认值的写对所有线程的读可见

变量的默认值(int=0、boolean=false、引用 = null)的 “写操作”(JVM 初始化),对所有线程的读可见。

public class DefaultValueHappensBefore {
    static int x; // 默认值0
    static boolean flag; // 默认值false
    static Object obj; // 默认值null
​
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(x); // 必然输出0
            System.out.println(flag); // 必然输出false
            System.out.println(obj); // 必然输出null
        }).start();
    }
}
  1. 传递性规则:hb 关系可传递

规则含义:如果 A hb B,B hb C,那么 A hb C。结合 volatile 防指令重排,可保证复杂场景的可见性。

public class TransitivityHappensBefore {
    int a = 0;
    volatile boolean flag = false;
​
    // 线程1
    public void writer() {
        a = 1; // 操作A:写普通变量
        flag = true; // 操作B:写volatile变量(A hb B)
    }
​
    // 线程2
    public void reader() {
        if (flag) { // 操作C:读volatile变量(B hb C)
            int b = a; // 操作D:读普通变量(C hb D)
            System.out.println(b); // 必然输出1(A hb D,传递性)
        }
    }
​
    public static void main(String[] args) throws InterruptedException {
        TransitivityHappensBefore demo = new TransitivityHappensBefore();
        Thread t1 = new Thread(demo::writer);
        Thread t2 = new Thread(demo::reader);
        t1.start();
        t2.start();
    }
}
  1. ConcurrencyTest 示例(指令重排 + volatile)

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;
​
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {
    int num = 0;
    volatile boolean ready = false;
​
    @Actor
    public void actor1(I_Result r) {
        if (ready) {
            r.r1 = num + num; // 若指令重排,可能读到num=0,结果为0
        } else {
            r.r1 = 1; // ready=false时输出1(正常)
        }
    }
​
    @Actor
    public void actor2(I_Result r) {
        num = 2; // 操作1:写num
        ready = true; // 操作2:写volatile变量
    }
}

本章重点讲解了 JMM 中的 可见性 - 由 JVM 缓存优化引起 有序性 - 由 JVM 指令重排序优化引起 happens-before 规则 原理方面 CPU 指令并行 volatile 模式方面 两阶段终止模式的 volatile 改进 同步模式之 balking

共享模型之无锁

CAS 与 volatile API 原子整数 原子引用 原子数组 字段更新器 原子累加器 Unsafe

  • 原理方面 LongAdder 源码 伪共享

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

while (true) {
 int prev = balance.get();
 int next = prev - amount;
 if (balance.compareAndSet(prev, next)) {
 break;
 }
 }

CAS 与 volatile

public void withdraw(Integer amount) {
 while(true) {
 // 需要不断尝试,直到成功为止
 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 的说法),它必须是原子操作。

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

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

volatile

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。 注意 volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原 子性) CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

为什么无锁效率高

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

CAS 的特点

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

原子整数

J.U.C 并发包提供了: AtomicBoolean AtomicInteger AtomicLong

原子引用

AtomicReference AtomicMarkableReference AtomicStampedReference

AAB问题
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();
 sleep(1);
 // 尝试改为 C
 log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
}
private static void other() {
 new Thread(() -> {
 log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
 }, "t1").start();
 sleep(0.5);
 new Thread(() -> {
 log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
 }, "t2").start();
}
​
11:29:52.325 c.Test36 [main] - main start... 
11:29:52.379 c.Test36 [t1] - change A->B true 
11:29:52.879 c.Test36 [t2] - change B->A true 
11:29:53.880 c.Test36 [main] - change A->C true 

AtomicStampedReference

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();
 sleep(1);
 // 尝试改为 C
 log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
}
private static void other() {
 new Thread(() -> {
 log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", 
 ref.getStamp(), ref.getStamp() + 1));
 log.debug("更新版本为 {}", ref.getStamp());
 }, "t1").start();
 sleep(0.5);
 new Thread(() -> {
 log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", 
 ref.getStamp(), ref.getStamp() + 1));
 log.debug("更新版本为 {}", ref.getStamp());
 }, "t2").start();
}

15:41:34.891 c.Test36 [main] - main start... 15:41:34.894 c.Test36 [main] - 版本 0 15:41:34.956 c.Test36 [t1] - change A->B true 15:41:34.956 c.Test36 [t1] - 更新版本为 1 15:41:35.457 c.Test36 [t2] - change B->A true 15:41:35.457 c.Test36 [t2] - 更新版本为 2 15:41:36.457 c.Test36 [main] - change A->C false

AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A -> C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。 但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了 AtomicMarkableReference

原子数组
字段更新器

AtomicReferenceFieldUpdater // 域 字段 AtomicIntegerFieldUpdater AtomicLongFieldUpdater

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

Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type

public class Test5 {
 private volatile int field;
 public static void main(String[] args) {
 AtomicIntegerFieldUpdater fieldUpdater =
     AtomicIntegerFieldUpdater.newUpdater(Test5.class, "field");
 Test5 test5 = new Test5();
 fieldUpdater.compareAndSet(test5, 0, 10);
 // 修改成功 field = 10
 System.out.println(test5.field);
 // 修改成功 field = 20
 fieldUpdater.compareAndSet(test5, 10, 20);
 System.out.println(test5.field);
 // 修改失败 field = 20
 fieldUpdater.compareAndSet(test5, 10, 30);
 System.out.println(test5.field);
 }
}

10 20 20

原子累加器
cas锁

Unsafe、

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

public class UnsafeAccessor {
 static Unsafe unsafe;
 static {
 try { 
 Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
 theUnsafe.setAccessible(true);
 unsafe = (Unsafe) theUnsafe.get(null);
 } catch (NoSuchFieldException | IllegalAccessException e) {
 throw new Error(e);
 }
 }
 static Unsafe getUnsafe() {
 return unsafe;
 }
}
@Data
class Student {
 volatile int id;
 volatile String name;
}
Unsafe unsafe = UnsafeAccessor.getUnsafe();
Field id = Student.class.getDeclaredField("id");
Field name = Student.class.getDeclaredField("name");
// 获得成员变量的偏移量
long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id);
long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name);
Student student = new Student();
// 使用 cas 方法替换成员变量的值
UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 true
UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
System.out.println(student);
Student(id=20, name=张三) 

共享模型之不可变

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

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();
}
19:10:40.859 [Thread-2] c.TestDateParse - {} 
java.lang.NumberFormatException: For input string: "" 
 at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) 
 at java.lang.Long.parseLong(Long.java:601) 
 at java.lang.Long.parseLong(Long.java:631) 
 at java.text.DigitList.getLong(DigitList.java:195) 
 at java.text.DecimalFormat.parse(DecimalFormat.java:2084) 
 at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162) 
 at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) 
 at java.text.DateFormat.parse(DateFormat.java:364) 
 at cn.itcast.n7.TestDateParse.lambda$test1$0(TestDateParse.java:18) 
 at java.lang.Thread.run(Thread.java:748) 
19:10:40.859 [Thread-1] c.TestDateParse - {} 
java.lang.NumberFormatException: empty String 
 at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842) 
 at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110) 
 at java.lang.Double.parseDouble(Double.java:538) 
 at java.text.DigitList.getDouble(DigitList.java:169) 
 at java.text.DecimalFormat.parse(DecimalFormat.java:2089) 
 at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162) 
 at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) 
 at java.text.DateFormat.parse(DateFormat.java:364) 
 at cn.itcast.n7.TestDateParse.lambda$test1$0(TestDateParse.java:18) 
 at java.lang.Thread.run(Thread.java:748) 
19:10:40.857 [Thread-8] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 
19:10:40.857 [Thread-9] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 
19:10:40.857 [Thread-6] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 
19:10:40.857 [Thread-4] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 
19:10:40.857 [Thread-5] c.TestDateParse - Mon Apr 21 00:00:00 CST 178960645 
19:10:40.857 [Thread-0] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 
19:10:40.857 [Thread-7] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 
19:10:40.857 [Thread-3] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 

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

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();
}
不可变设计
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
 
 // ...
 
}

发现该类、类中所有属性都是 final 的 属性用 final 修饰保证了该属性是只读的,不能修改 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

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)】

无状态

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

共享模型之工具

上学期学的基本忘完了

没事,用到再学

这段代码实现了一个极简但完整的线程池,核心模仿 JDK ThreadPoolExecutor 的设计思路,包含任务队列(阻塞队列)工作线程(Worker)拒绝策略三大核心模块,支持核心线程数限制、任务超时获取、自定义拒绝策略等关键功能,完整覆盖了线程池的核心逻辑

任务提交时,若核心线程数未满,直接创建 Worker 执行任务;

核心线程数满时,将任务加入阻塞队列;

队列满时,触发自定义拒绝策略;

Worker 线程执行完当前任务后,循环从队列获取任务(带超时),超时则销毁 Worker。

组件 / 类 / 接口 核心作用 关键方法 / 属性 核心设计细节
RejectPolicy<T> 接口 定义拒绝策略规范,支持自定义队列满时的任务处理逻辑 reject(BlockingQueue<T> queue, T task):队列满时执行的拒绝逻辑 函数式接口(@FunctionalInterface),支持 Lambda 表达式快速定义策略
BlockingQueue<T> 自定义阻塞队列,作为线程池的任务缓冲区,支持阻塞 / 超时的添加 / 获取任务 1. put():阻塞添加(队列满则等待)2. take():阻塞获取(队列空则等待)3. poll(timeout, unit):超时获取4. offer(timeout, unit):超时添加5. tryPut():尝试添加,满则触发拒绝策略 1. 用 ReentrantLock 保证线程安全2. 用 Condition 实现生产者 / 消费者等待唤醒3. 队列基于 ArrayDeque 实现
ThreadPool 线程池核心类,管理工作线程和任务队列,对外提供任务提交入口 1. execute(Runnable task):提交任务2. 构造方法:初始化核心参数(核心线程数、超时时间、队列容量、拒绝策略) 1. workers 集合管理所有工作线程2. 核心逻辑:核心线程数未满则创建 Worker,否则入队3. 线程安全:操作 workers 时加同步锁
Worker 内部类 工作线程,负责执行任务,执行完后循环从队列获取任务 run():核心执行逻辑- 先执行初始化任务- 循环调用 poll() 获取任务(带超时)- 超时则移除自身,销毁线程 1. 继承 Thread 类,本身是独立线程2. 任务执行完后置空,避免内存泄漏3. 超时无任务则自动销毁,节省资源
拒绝策略示例(main 方法) 演示 5 种常见拒绝策略,覆盖 JDK 默认策略的核心场景 1. 死等(queue.put(task))2. 超时等待(queue.offer(...))3. 放弃任务4. 抛出异常5. 调用者自行执行 策略通过 Lambda 传入线程池,灵活扩展

ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
 int maximumPoolSize,
 long keepAliveTime,
 TimeUnit unit,
 BlockingQueue<Runnable> workQueue,
 ThreadFactory threadFactory,
RejectedExecutionHandler handler)

corePoolSize 核心线程数目 (最多保留的线程数) maximumPoolSize 最大线程数目 keepAliveTime 生存时间 - 针对救急线程 unit 时间单位 - 针对救急线程 workQueue 阻塞队列 threadFactory 线程工厂 - 可以为线程创建时起个好名字 handler 拒绝策略

线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排 队,直到有空闲的线程。 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线 程来救急。 如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它 著名框架也提供了实现 AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略 北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090CallerRunsPolicy 让调用者运行任务 DiscardPolicy 放弃本次任务 DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之 Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方 便定位问题 Netty 的实现,是创建一个新线程来执行任务 ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略 PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略 当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime 和 unit 来控制

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐