volatile 保证了「不重排序」「内存可见性」,但是不保证原子性。要保证原子性,可以对程序加锁。加锁的问题是,需要一定量的编码,也有可能造成死锁问题。此时 Java 中的原子类就可以派上用场了,原子类提供了一种线程安全的方式来进行共享变量的操作,它能够确保在多线程环境下,变量的操作是原子性的。
Java 中的原子类包括 AtomicBoolean、AtomicInteger、AtomicLong 等,它们都提供了原子性的读写操作,能够保证多线程环境下变量的操作是线程安全的。
原子类主要是通过 CAS(Compare and Swap)操作和 volatile 关键字来实现的。
CAS 操作
CAS 操作是一种基于乐观锁的无锁算法,它可以在不使用锁的情况下实现并发控制。CAS 操作需要有三个操作数:
当且仅当 V 的值等于 A 时,CAS 会通过原子方式用新值 B 来更新 V 的值,否则不会进行任何操作。
在整个 CAS 操作过程中,Atomic 类使用了 volatile 关键字来保证多线程之间的可见性,即当一个线程修改了 V 的值后,其他线程可以立即看到该变化。
AtomicBoolean
由于 AtomicBoolean 的操作是原子性的,多个线程可以同时进行对 running 的读写操作而不会导致竞争条件的问题。
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 实现了对共享变量的原子更新操作。
总结
常见面试问题
1、简述原子类的使用场景
原子类适合在多线程环境下进行原子操作的场景。当多个线程需要对共享变量进行读取、写入、递增、递减等操作时,原子类可以提供一种线程安全的、高效的解决方案。使用原子类可以避免在多线程环境下出现数据竞争、死锁等问题,并且不需要使用显式的同步机制,从而提高程序的并发性能。
以下是一些适合使用原子类的场景:
总之,使用原子类可以避免多线程环境下的数据竞争和死锁等问题,并且不需要使用显式的同步机制,从而提高程序的并发性能。原子类适用于对共享变量进行简单的读取、写入、递增、递减等操作的场景,但是对于复杂的操作,需要使用更高级别的同步机制来保证程序的正确性。
2、原子类有什么缺点?
AQS 是java.util.concurrent.locks.AbstractQueuedSynchronizer 的简称,是并发包的抽象类,用来实现各种锁、各种同步工具,例如 ReentrantLock、Semaphore、CountDownLatch 。同时也提供了一些高级同步功能,如条件变量(Condition)等。
因此我们有必要了解 AQS,以便更好地使用这些并发工具。
AQS 中有一个队列和一个变量,具体来说,它们是:
AQS 的核心思想
AQS 的核心思想是使用一个先进先出的双向队列(即 FIFO 队列)来管理获取锁但是未成功的线程。当一个线程请求锁时:
以 ReentrantLock 为例,当一个线程请求锁时,如果锁没有被占用,那么该线程可以获取锁并继续执行;如果锁已经被其他线程占用,那么该线程就会被阻塞,并且被加入到等待队列中。当持有锁的线程释放锁时,AQS 会从等待队列中唤醒一个线程,让其获取锁并继续执行。
AQS 的实现主要依赖于两个核心方法:tryAcquire() 和 tryRelease()。在 ReentrantLock 中,tryAcquire() 方法用于尝试获取锁,tryRelease() 方法用于释放锁。
AQS 操作的核心流程
AQS 的操作流程可以概括为以下几个步骤:
以上就是 AQS 的核心操作流程,其中最关键的是等待队列的维护和线程的唤醒机制。通过等待队列和唤醒机制,AQS 可以实现高效的线程同步,避免线程轮询和忙等待的性能问题。
独占和共享两种同步方式
支持条件变量
Condition 是 AQS 中提供的一种条件变量实现,它提供了 await 和 signal 方法来支持线程的等待和唤醒操作,通常和 Lock 对象一起使用。
AQS 内部使用一个条件队列来保存等待条件的线程,当条件不满足时,线程会被加入到条件队列中等待。当条件满足时,会从条件队列中选择一个线程唤醒。
实现自定义同步器
可以通过继承 AQS 类来实现自定义同步器,具体实现需要重写 AQS 的一些方法,如 tryAcquire、tryRelease 等,根据具体需求实现相应的逻辑。
哪些类是 AQS 的实现类
ReentrantLock
ReentrantLock 是基于 AQS 实现的可重入锁,它可以替代 synchronized 关键字来进行同步操作。在项目中,如果需要进行多线程访问共享资源的情况,可以使用 ReentrantLock 来实现同步操作。
Semaphore
Semaphore 也是基于 AQS 实现的,它可以控制同时访问某个资源的线程数量。在项目中,如果需要控制某个共享资源的并发访问数量,可以使用 Semaphore 来实现。
CountDownLatch
CountDownLatch 也是基于 AQS 实现的,它可以实现一个或多个线程等待其他线程完成后再继续执行的功能。在项目中,如果需要实现某个线程等待其他线程完成某个任务后再进行下一步操作,可以使用 CountDownLatch 来实现。
总结
阅读量:2031
点赞量:0
收藏量:0