Skip to content

线程安全性

线程安全性(Thread Safety)是指在多线程编程环境下,当多个线程同时访问某个类的实例或者共享资源时,这个类能够确保其内部状态的一致性,并且不会发生数据不一致、竞态条件或死锁等问题。

一个线程安全的类意味着即使在其实例上并发执行多个操作,也能表现出预期的行为,并且这些操作之间是互斥的,即保证了原子性和可见性。

关键性指标

原子性、可见性、有序性

  1. 原子性(Atomicity)

    • 指单个操作或者一系列操作要么全部完成,要么都不做。例如,对于一个计数器的操作,增加一的操作应当是不可分割的,不允许在两个线程间交错执行导致计数值错误。
  2. 可见性(Visibility)

    • 当一个线程修改了共享变量的值后,其他线程能够立即看到该变化。Java内存模型通过volatile关键字和同步机制来保证可见性。
  3. 有序性(Ordering / Sequential Consistency)

    • 确保对共享变量的所有操作都按照程序中的顺序进行,防止指令重排序带来的影响。Java中通过happens-before原则以及synchronizedvolatile关键字等手段来实现。

实现方式

  • 互斥控制:使用synchronized关键字、ReentrantLock等工具来实现对临界区代码块的互斥访问。
  • 不可变对象:创建不可变对象可以天然地实现线程安全,因为一旦创建后它的状态就不会改变。
  • 线程局部存储:如ThreadLocal,它为每个线程提供独立的数据副本,避免了线程间的共享冲突。
  • 原子类:Java.util.concurrent.atomic包提供的原子变量类,如AtomicInteger等,它们提供了原子级别的更新操作。
  • volatile变量:可以确保任何线程对其的读取总是从主内存获取最新值,同时写入也会立即刷新到主内存,使得该变量在多线程环境下的可见性得到保障,但并不能防止并发修改。

要设计线程安全的类或方法,开发者需要仔细考虑如何协调不同线程之间的交互,确保无论在任何并发场景下,都能正确无误地管理共享资源的状态。

实战代码

以下案例可使用并发模拟代码

1、synchronized实现线程安全

java
private static int count = 0;
private synchronized void add() {
    count++;
    // 这里可以添加具体的业务操作
}

2、使用Lock实现线程安全

基于ReentrantLock

java
private Lock lock = new ReentrantLock();
private void add() {
    lock.lock();
    try{
        count++;
        // 这里可以添加具体的业务操作
    }finally {
        lock.unlock();
    }
}

更多可查看Reentrantlock

基于StampedLock

java
public static int count = 0;

private final static StampedLock lock = new StampedLock();

private void add() {
    long stamp = lock.writeLock();
    try {
        count++;
    } finally {
        lock.unlock(stamp);
    }
}

详见StampedLock

3、AtomicInteger实现线程安全

java
public static AtomicInteger count = new AtomicInteger(0);
private void add() {
    count.incrementAndGet();
    // 这里可以添加具体的业务操作
}

incrementAndGet源码分析

package java.util.concurrent.atomic.AtomicInteger

java
// private static final Unsafe unsafe = Unsafe.getUnsafe();

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}


public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

public native int getIntVolatile(Object o, long offset);

public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

使用Unsafe类实现线程安全的递增操作,Unsafe类提供了一些底层的、非公开的方法来直接操作内存,包括对对象字段进行原子更新。

getAndAddInt使用循环CAS(Compare-and-Swap)机制来实现原子性更新

  • getIntVolatile(o, offset):首先通过JNI调用native方法获取内存中指定对象和偏移量对应的当前整数值,这是一个volatile读操作,确保了数据的可见性。
  • 在循环体内,不断尝试执行compareAndSwapInt操作,如果当前值等于预期值(也就是上一步刚读取到的v),则将内存中的值原子性地更新为v + delta(这里是自增1的操作)。
  • 当CAS操作成功时,循环结束,返回原来的旧值v。

compareAndSwapInt是一个native方法,通过CPU提供的CAS指令实现原子性的比较并交换操作,只有当内存中的值没有被其他线程修改过时,才会更新内存,并返回true;否则不做任何修改,继续循环尝试直到成功为止。这样就确保了在多线程环境下对该整数变量进行递增操作时的线程安全性。

4、AtomicLong实现线程安全

java
public static AtomicLong count = new AtomicLong(0);
private void add() {
    count.incrementAndGet();
    // 这里可以添加具体的业务操作
}

使用AtomicLong类实现一个线程安全的计数器

5、LongAdder实现线程安全

java
public static LongAdder count = new LongAdder();
private void add() {
    count.increment();
    // 这里可以添加具体的业务操作
}

LongAdder内部通过一种称为“分裂数组”的机制分散了更新操作,以减少竞争和提高吞吐量。相较于AtomicLong高并发场景下性能更优。

LongAdder与AtomicLong都是Java并发包中用于实现原子计数器的类,它们在实现线程安全方面具有相似的目的,但内部机制和适用场景有所不同:

AtomicLong

  • 特点:基于CAS(Compare and Swap)操作来保证原子性更新。所有对AtomicLong实例的操作如incrementAndGet、getAndIncrement等,都是原子性的。
  • 性能:在低并发场景下,由于其简单的单点更新特性,性能表现良好。
  • 局限:高并发场景下,大量的并发线程争抢同一个变量时,可能会造成严重的锁竞争,从而影响性能。

LongAdder

  • 特点:通过内部的Cell数组结构分散了更新压力,每个Cell可以独立进行CAS操作,减少多线程间的冲突。当一个Cell的更新失败时,会尝试另一个Cell或者创建新的Cell。
  • 性能:在高并发场景下,LongAdder的性能优于AtomicLong,因为它能够将更新操作分散到多个内存位置,降低了锁竞争。
  • 额外功能:提供sum()方法来获取累加后的总值,同时,在计算最终结果时,也会合并所有Cell中的数据。

应用场景建议:

  • AtomicLong:适用于并发量较小或需要单一共享变量的高性能原子操作,例如作为序列号生成器或者维护全局唯一标识符(GUID)的计数器。
  • LongAdder:特别适合于高并发环境下的计数任务,比如统计请求次数、事件发生次数等,这些场景下需要频繁地对数值进行递增操作,并且期望在大量并发访问下保持良好的性能表现。此外,如果性能是关键考量因素,而不需要实时获取精确的累计值,LongAdder也是一个很好的选择。在非高峰时段或统计周期结束时再调用sum()方法获得准确的统计结果即可。

对于低并发场景两者都可以使用,但从可扩展性和高并发性能优化的角度来看,LongAdder在处理高并发计数需求时更胜一筹。

6、AtomicReference的用法

java
private static AtomicReference<Integer> count = new AtomicReference<>(0);

public static void main(String[] args) {
    count.compareAndSet(0, 1); // 1
    count.compareAndSet(0, 2); // no
    count.compareAndSet(1, 3); // 3
    count.compareAndSet(2, 4); // no
    count.compareAndSet(3, 5); // 5
    log.info("count:{}", count.get());
}

通过CAS操作来尝试将当前值替换为新的预期值,只有当当前值等于预期值时才会更新。

  1. count.compareAndSet(0, 1); 成功执行,因为当前值确实是0,所以将其设置为1。
  2. count.compareAndSet(0, 2); 失败执行,此时count已经被更新为1,不再等于0。
  3. count.compareAndSet(1, 3); 成功执行,因为当前值是1,符合预期值,所以将其设置为3。
  4. count.compareAndSet(2, 4); 失败执行,原因同上一步,当前值已经是3,并非2。
  5. count.compareAndSet(3, 5); 成功执行,由于当前值是3,该操作可以成功地将count更新为5。

输出结果为5

7、AtomicIntegerFieldUpdater

java
private static AtomicIntegerFieldUpdater<AtomicDemo> updater =
            AtomicIntegerFieldUpdater.newUpdater(AtomicDemo.class, "count");

@Getter
public volatile int count = 100;

public static void main(String[] args) {

    AtomicDemo demo = new AtomicDemo();

    if (updater.compareAndSet(demo, 100, 120)) {
        log.info("update success 1, {}", demo.getCount());
    }

    if (updater.compareAndSet(demo, 100, 120)) {
        log.info("update success 2, {}", demo.getCount());
    } else {
        log.info("update failed, {}", demo.getCount());
    }
}

输出

19:57:55.991 [main] INFO cn.diyai.mul_thread.atomic.AtomicExample5 - update success 1, 120 19:57:55.994 [main] INFO cn.diyai.mul_thread.atomic.AtomicExample5 - update failed, 120

第一次尝试使用updater.compareAndSet(demo, 100, 120)demo对象的count字段从100原子性地更新为120,由于此时count字段的值确实是100,所以这个操作会成功,并输出"update success 1, 120"。

第二次尝试同样的更新操作,但由于第一次更新后count已经变为120,不再等于预期值100,所以这次操作会失败,并输出"update failed, 120"。

AtomicIntegerFieldUpdater提供了一种基于反射机制的、灵活的方式来实现非静态、非volatile int字段的原子更新,尤其适用于无法直接修改源代码以添加volatile关键字或改为使用AtomicInteger的情况。

第二次更新操作因为不符合预期值而失败,展示了其原子性和条件更新的特点。

8、AtomicBoolean实现只执行一次

java
private static AtomicBoolean isHappened = new AtomicBoolean(false);
private void execOne() {
    if (isHappened.compareAndSet(false, true)) {
        log.info("execute");
    }
    // 这里可以添加具体的业务操作
}

使用AtomicBoolean类来实现一个线程安全的标志位操作。

调用isHappened.compareAndSet(false, true),这是一个原子操作,它会检查当前isHappened的值是否为false,如果是,则将其设置为true并返回true表示更新成功;否则(即已为true时),不会做任何改变并返回false表示更新失败。

在多线程环境下,如果需要确保某个操作仅被执行一次,可以使用这种方式。这是因为当第一个线程将isHappened设置为true后,其他线程的compareAndSet操作都会因条件不满足而不再执行业务操作部分的代码。

CAS

CAS(Compare-And-Swap)算法是一种无锁同步技术,保证数据变量的原子性,是硬件对于并发操作的支持,包含了三个操作数:

  1. 内存值 V:这是当前存储在内存中的值。
  2. 预估值 A:这是期望当前内存值应该为的值。
  3. 更新值 B:如果内存值V等于预估值A,则将内存值V更新为这个新值B。

执行CAS操作时,处理器会检查内存位置的当前值是否与预期值A相等。

如果是,则将该位置的值更新为新值B;

如果不是,则不进行任何操作,并通常会返回当前内存的实际值。

整个过程是一个原子操作,意味着不会被其他线程中断。

通过这种方式,多个线程可以并发地尝试修改同一变量,但只有一个线程能在某一时刻成功更新该变量,从而避免了竞态条件和数据不一致的问题。

Java中的一些并发工具类如AtomicIntegerAtomicLongAtomicReference以及Unsafe类提供的相关方法,都依赖于CAS来实现高效的无锁并发控制。