JUC-下-灵析社区

菜鸟码转

五、 原子类

volatile 保证了「不重排序」「内存可见性」,但是不保证原子性。要保证原子性,可以对程序加锁。加锁的问题是,需要一定量的编码,也有可能造成死锁问题。此时 Java 中的原子类就可以派上用场了,原子类提供了一种线程安全的方式来进行共享变量的操作,它能够确保在多线程环境下,变量的操作是原子性的。

Java 中的原子类包括 AtomicBoolean、AtomicInteger、AtomicLong 等,它们都提供了原子性的读写操作,能够保证多线程环境下变量的操作是线程安全的。

原子类主要是通过 CAS(Compare and Swap)操作和 volatile 关键字来实现的。

CAS 操作

CAS 操作是一种基于乐观锁的无锁算法,它可以在不使用锁的情况下实现并发控制。CAS 操作需要有三个操作数:

  • 内存地址 V;
  • 旧的预期值 A;
  • 新的值 B。

当且仅当 V 的值等于 A 时,CAS 会通过原子方式用新值 B 来更新 V 的值,否则不会进行任何操作。

在整个 CAS 操作过程中,Atomic 类使用了 volatile 关键字来保证多线程之间的可见性,即当一个线程修改了 V 的值后,其他线程可以立即看到该变化。

AtomicBoolean

  • AtomicBoolean 是 Java 中的一个原子性布尔值类,它提供了一种线程安全的方式来对布尔值进行读写操作;
  • 在 AtomicBoolean 的值发生变化的时候,不允许插入其它操作;
  • AtomicBoolean 可以被用于多线程环境下的状态标记、控制开关等场景。由于其操作是原子性的,多个线程可以同时进行读写操作而不会导致数据不一致或竞争条件的问题。

由于 AtomicBoolean 的操作是原子性的,多个线程可以同时进行对 running 的读写操作而不会导致竞争条件的问题。

AtomicInteger 和 AtomicLong

  • AtomicInteger 和 AtomicLong 是 Java 中的原子性整数和长整数类。它们提供了一种线程安全的方式来对整数和长整数进行读写操作;
  • AtomicInteger 和 AtomicLong 可以被用于多线程环境下的计数器、序号生成器、状态标记等场景。由于其操作是原子性的,多个线程可以同时进行读写操作而不会导致数据不一致或竞争条件的问题。

下面是一个使用 AtomicInteger 的示例:

import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
  
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public void decrement() {
        count.decrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
  
}

这个示例展示了一个线程安全的计数器类。它的 increment() 和 decrement() 方法分别对计数器进行加一和减一操作。由于 AtomicInteger 的操作是原子性的,多个线程可以同时进行对 count 的读写操作而不会导致竞争条件的问题。因此,这个示例中的 Counter 类是线程安全的。

下面是一个使用 AtomicLong 的示例:

import java.util.concurrent.atomic.AtomicLong;

public class IdGenerator {
  
    private AtomicLong id = new AtomicLong(0);

    public long nextId() {
        return id.incrementAndGet();
    }
  
}

这个示例展示了一个线程安全的序号生成器类。它的 nextId() 方法会返回一个递增的长整数,由于 AtomicLong 的操作是原子性的,多个线程可以同时进行对 id 的读写操作而不会导致竞争条件的问题。因此,这个示例中的 IdGenerator 类是线程安全的。

总之,AtomicInteger 和 AtomicLong 是在多线程环境下对整数和长整数进行原子性读写操作的工具类,它们可以帮助开发者实现线程安全的计数器、序号生成器等功能。

AtomicReference

AtomicReference 是 Java 中的一个原子类,用于提供原子性的引用更新操作。它是线程安全的,可以保证在多线程环境中对共享变量的操作是原子的。在 Java 并发编程中,AtomicReference 经常用于实现无锁的并发算法和数据结构,例如 CAS (Compare and Swap) 算法。

以下是 AtomicReference 的用法示例:

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceExample {
    
    public static void main(String[] args) {
        AtomicReference<String> atomicReference = new AtomicReference<>("initialValue");
        
        // 获取当前值
        String currentValue = atomicReference.get();
        System.out.println("Current value: " + currentValue);
        
        // 尝试更新值,如果当前值等于期望值则更新成功
        boolean updated = atomicReference.compareAndSet("initialValue", "newValue");
        System.out.println("Update successful: " + updated);
        System.out.println("Current value: " + atomicReference.get());
        
        // 尝试更新值,期望值不匹配则更新失败
        updated = atomicReference.compareAndSet("initialValue", "newValue");
        System.out.println("Update successful: " + updated);
        System.out.println("Current value: " + atomicReference.get());
        
        // 设置新值
        atomicReference.set("newInitialValue");
        System.out.println("Current value: " + atomicReference.get());
    }
}

在上面的示例中,首先创建了一个 AtomicReference 对象并初始化为「initialValue」。然后通过 get() 方法获取当前值,并使用 compareAndSet() 方法尝试更新值,如果当前值等于期望值则更新成功,否则更新失败。接着再次使用 compareAndSet() 方法尝试更新值,此时期望值不匹配,因此更新失败。最后使用 set() 方法设置新的值。

输出结果为:

Current value: initialValue
Update successful: true
Current value: newValue
Update successful: false
Current value: newValue
Current value: newInitialValue

可以看到,通过 AtomicReference 实现了对共享变量的原子更新操作。

总结

  • CAS 操作是一种乐观锁机制,它通过比较当前值和期望值来判断是否存在竞态条件,并在判断通过时原子地更新值;
  • AtomicInteger、AtomicLong、AtomicBoolean:提供了原子性的操作。
  • AtomicReference、AtomicStampedReference、AtomicMarkableReference:提供了对对象的原子性更新操作。

常见面试问题

1、简述原子类的使用场景

原子类适合在多线程环境下进行原子操作的场景。当多个线程需要对共享变量进行读取、写入、递增、递减等操作时,原子类可以提供一种线程安全的、高效的解决方案。使用原子类可以避免在多线程环境下出现数据竞争、死锁等问题,并且不需要使用显式的同步机制,从而提高程序的并发性能。

以下是一些适合使用原子类的场景:

  • 计数器和累加器:在多线程环境下,如果需要对计数器或者累加器进行递增或递减操作,可以使用 AtomicInteger 或 AtomicLong 等原子类来实现,避免了数据竞争的问题;
  • 缓存行填充:在多线程环境下,缓存行的大小通常是 64 字节,如果共享变量之间的距离小于缓存行的大小,会导致伪共享(false sharing)问题。可以使用 AtomicLongArray 等原子类来解决这个问题;
  • 布尔标志:在多线程环境下,如果需要对布尔标志进行读取和写入操作,可以使用 AtomicBoolean 来实现,避免了数据竞争的问题;

总之,使用原子类可以避免多线程环境下的数据竞争和死锁等问题,并且不需要使用显式的同步机制,从而提高程序的并发性能。原子类适用于对共享变量进行简单的读取、写入、递增、递减等操作的场景,但是对于复杂的操作,需要使用更高级别的同步机制来保证程序的正确性。

2、原子类有什么缺点?

  • 原子类的主要缺点是性能较低。虽然原子类可以在多线程环境下保证数据的原子性,但是这种保证是有代价的。原子类的实现通常依赖于 CAS(Compare and Swap)操作,而 CAS 操作需要保证内存的可见性、有序性和原子性,因此需要使用同步机制来实现。这种同步机制可能会导致上下文切换和缓存不一致等问题,从而影响程序的性能;
  • 当多个原子类之间存在依赖关系时,仍然需要使用同步机制来保证程序的正确性;
  • 原子类只能保证单个操作的原子性,而不能保证多个操作之间的原子性。如果需要进行多个操作的原子操作,需要使用更高级别的同步机制,例如使用锁或者使用并发容器。

六、 AQS(队列同步器)

AQS 是java.util.concurrent.locks.AbstractQueuedSynchronizer 的简称,是并发包的抽象类,用来实现各种锁、各种同步工具,例如 ReentrantLock、Semaphore、CountDownLatch 。同时也提供了一些高级同步功能,如条件变量(Condition)等。

因此我们有必要了解 AQS,以便更好地使用这些并发工具。

AQS 中有一个队列和一个变量,具体来说,它们是:

  • 一个等待线程的队列,用于实现线程的排队执行;
  • 一个被 volatile 修饰的 state 变量,表示同步状态。线程需要先获取这个同步状态变量的锁才能执行临界区代码,当一个线程占用了这个锁后,其他线程就需要等待,直到该线程释放了锁,其他线程才有机会获取到锁并执行临界区代码。

AQS 的核心思想

AQS 的核心思想是使用一个先进先出的双向队列(即 FIFO 队列)来管理获取锁但是未成功的线程。当一个线程请求锁时:

  • 如果锁没有被占用,那么该线程可以获取锁并继续执行;
  • 如果锁已经被其他线程占用,那么该线程就会被阻塞,并且被加入到等待队列中。在锁被释放时,AQS 会从等待队列中唤醒一个线程继续执行。

以 ReentrantLock 为例,当一个线程请求锁时,如果锁没有被占用,那么该线程可以获取锁并继续执行;如果锁已经被其他线程占用,那么该线程就会被阻塞,并且被加入到等待队列中。当持有锁的线程释放锁时,AQS 会从等待队列中唤醒一个线程,让其获取锁并继续执行。

AQS 的实现主要依赖于两个核心方法:tryAcquire() 和 tryRelease()。在 ReentrantLock 中,tryAcquire() 方法用于尝试获取锁,tryRelease() 方法用于释放锁。

  • 如果 tryAcquire() 返回 true,则表示当前线程已经成功获取了锁;
  • 如果返回 false,则表示当前线程未能获取锁,需要将当前线程加入到等待队列中。当持有锁的线程调用 tryRelease() 方法释放锁时,AQS 会唤醒等待队列中的一个线程,让其尝试获取锁。

AQS 操作的核心流程

AQS 的操作流程可以概括为以下几个步骤:

  • 通过 acquire() 方法获取锁:当一个线程尝试获取锁时,它会调用 AQS 的 acquire() 方法。 acquire() 方法首先会调用tryAcquire() 方法尝试获取锁,如果 tryAcquire() 方法返回 false,则会将当前线程加入到等待队列中,然后阻塞线程;
  • 加入等待队列:如果 tryAcquire() 方法返回 false,则会将当前线程加入到等待队列中,等待锁的释放。在加入等待队列时,AQS 会将当前线程封装成一个 Node 对象,并将该对象添加到等待队列的尾部;
  • 释放锁:当一个线程释放锁时,它会调用 AQS 的 release() 方法。release() 方法首先会调用tryRelease() 方法释放锁,然后会遍历等待队列,找到第一个可以被唤醒的线程,并将其唤醒;
  • 唤醒等待的线程:AQS 内部维护了一个等待队列,用于保存等待锁的线程。当锁释放时,AQS 会遍历等待队列,找到第一个可以被唤醒的线程,并将其唤醒。被唤醒的线程会尝试重新获取锁,如果成功则继续执行,否则会再次阻塞等待;
  • 释放等待节点:当一个线程被唤醒后,它会从等待队列中移除自己的等待节点,这样其他线程就不会继续唤醒它。

以上就是 AQS 的核心操作流程,其中最关键的是等待队列的维护和线程的唤醒机制。通过等待队列和唤醒机制,AQS 可以实现高效的线程同步,避免线程轮询和忙等待的性能问题。

独占和共享两种同步方式

  • 独占方式是指同一时刻只能有一个线程持有锁,ReentrantLock 就是一种支持独占方式的锁;
  • 共享方式则允许多个线程同时持有锁, Semaphore 是一种支持共享方式的锁。

支持条件变量

Condition 是 AQS 中提供的一种条件变量实现,它提供了 await 和 signal 方法来支持线程的等待和唤醒操作,通常和 Lock 对象一起使用。

AQS 内部使用一个条件队列来保存等待条件的线程,当条件不满足时,线程会被加入到条件队列中等待。当条件满足时,会从条件队列中选择一个线程唤醒。

实现自定义同步器

可以通过继承 AQS 类来实现自定义同步器,具体实现需要重写 AQS 的一些方法,如 tryAcquire、tryRelease 等,根据具体需求实现相应的逻辑。

哪些类是 AQS 的实现类

ReentrantLock

ReentrantLock 是基于 AQS 实现的可重入锁,它可以替代 synchronized 关键字来进行同步操作。在项目中,如果需要进行多线程访问共享资源的情况,可以使用 ReentrantLock 来实现同步操作。

Semaphore

Semaphore 也是基于 AQS 实现的,它可以控制同时访问某个资源的线程数量。在项目中,如果需要控制某个共享资源的并发访问数量,可以使用 Semaphore 来实现。

CountDownLatch

CountDownLatch 也是基于 AQS 实现的,它可以实现一个或多个线程等待其他线程完成后再继续执行的功能。在项目中,如果需要实现某个线程等待其他线程完成某个任务后再进行下一步操作,可以使用 CountDownLatch 来实现。

总结

  • AQS 提供了一种可靠且灵活的同步机制,使得开发者能够轻松地实现线程同步和互斥,从而避免多线程竞争导致的数据不一致或者死锁等问题;
  • 在项目中使用 AQS 主要是为了实现多线程同步、协作等功能,可以使用基于 AQS 实现的工具类来简化代码实现。但是,需要注意 AQS 的使用需要慎重,因为过度使用 AQS 可能会导致代码难以维护、出现死锁等问题。

阅读量:2031

点赞量:0

收藏量:0