MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java中的Atomic类与原子操作

2021-03-257.2k 阅读

Java中的Atomic类基础介绍

在Java的多线程编程领域中,数据的一致性和线程安全是至关重要的问题。传统的变量在多线程环境下进行读写操作时,很容易出现数据竞争和不一致的情况。为了解决这类问题,Java提供了Atomic类库,它包含了一系列能够进行原子操作的类,这些原子操作在多线程环境下能够保证数据的一致性和线程安全。

Atomic类的概述

Atomic类位于java.util.concurrent.atomic包下,它提供了对基本数据类型(如intlongboolean)以及引用类型的原子操作支持。所谓原子操作,是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何线程上下文切换。这使得Atomic类在多线程环境下能够安全地进行数据操作,避免了常见的并发问题。

常用Atomic类介绍

  1. AtomicInteger:用于对整数进行原子操作。它提供了诸如getsetincrementAndGetdecrementAndGet等方法,这些方法在多线程环境下能够保证操作的原子性。例如,在一个多线程程序中,多个线程同时对一个AtomicInteger对象进行自增操作,不会出现数据不一致的情况。
  2. AtomicLong:与AtomicInteger类似,只不过它操作的是长整型数据。适用于需要对长整型进行原子操作的场景,比如记录系统中的时间戳等。
  3. AtomicBoolean:用于对布尔值进行原子操作。提供了getset方法,在多线程环境下能够保证布尔值的读写操作是原子的,避免了由于并发读写导致的不一致问题。
  4. AtomicReference:用于对引用类型进行原子操作。这在需要原子地更新对象引用时非常有用,比如在实现无锁数据结构时,可能需要原子地替换某个节点的引用。

AtomicInteger的深入分析与示例

AtomicInteger的基本方法

  1. get()方法:获取AtomicInteger当前的值。该方法是原子性的,多个线程同时调用get方法时,不会出现数据不一致的情况。
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(10);
        int value = atomicInteger.get();
        System.out.println("Current value: " + value);
    }
}
  1. set(int newValue)方法:设置AtomicInteger的值为指定的新值。同样,这个操作是原子性的,能够保证在多线程环境下设置值的一致性。
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerSetExample {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(10);
        atomicInteger.set(20);
        int value = atomicInteger.get();
        System.out.println("New value: " + value);
    }
}
  1. incrementAndGet()方法:将AtomicInteger的值原子地自增1,并返回自增后的值。在多线程环境下,多个线程同时调用这个方法,不会出现丢失自增操作的情况。
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerIncrementExample {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(10);
        int incrementedValue = atomicInteger.incrementAndGet();
        System.out.println("Incremented value: " + incrementedValue);
    }
}
  1. decrementAndGet()方法:将AtomicInteger的值原子地自减1,并返回自减后的值。类似于incrementAndGet,它在多线程环境下能保证操作的原子性。
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDecrementExample {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(10);
        int decrementedValue = atomicInteger.decrementAndGet();
        System.out.println("Decremented value: " + decrementedValue);
    }
}
  1. getAndIncrement()方法:先返回AtomicInteger当前的值,然后再将其原子地自增1。这个方法与incrementAndGet的区别在于返回值的时机不同。
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerGetAndIncrementExample {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(10);
        int currentValue = atomicInteger.getAndIncrement();
        int newCurrentValue = atomicInteger.get();
        System.out.println("Current value before increment: " + currentValue);
        System.out.println("Current value after increment: " + newCurrentValue);
    }
}
  1. getAndDecrement()方法:先返回AtomicInteger当前的值,然后再将其原子地自减1。
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerGetAndDecrementExample {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(10);
        int currentValue = atomicInteger.getAndDecrement();
        int newCurrentValue = atomicInteger.get();
        System.out.println("Current value before decrement: " + currentValue);
        System.out.println("Current value after decrement: " + newCurrentValue);
    }
}
  1. addAndGet(int delta)方法:将AtomicInteger的值原子地增加指定的delta值,并返回增加后的值。
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerAddAndGetExample {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(10);
        int addedValue = atomicInteger.addAndGet(5);
        System.out.println("Added value: " + addedValue);
    }
}
  1. getAndAdd(int delta)方法:先返回AtomicInteger当前的值,然后再将其原子地增加指定的delta值。
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerGetAndAddExample {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(10);
        int currentValue = atomicInteger.getAndAdd(5);
        int newCurrentValue = atomicInteger.get();
        System.out.println("Current value before add: " + currentValue);
        System.out.println("Current value after add: " + newCurrentValue);
    }
}

多线程环境下AtomicInteger的应用示例

下面通过一个多线程自增的示例来展示AtomicInteger在多线程环境下的线程安全性。

import java.util.concurrent.atomic.AtomicInteger;

class IncrementThread extends Thread {
    private AtomicInteger atomicInteger;

    public IncrementThread(AtomicInteger atomicInteger) {
        this.atomicInteger = atomicInteger;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            atomicInteger.incrementAndGet();
        }
    }
}

public class AtomicIntegerMultiThreadExample {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        IncrementThread[] threads = new IncrementThread[10];

        for (int i = 0; i < 10; i++) {
            threads[i] = new IncrementThread(atomicInteger);
            threads[i].start();
        }

        for (IncrementThread thread : threads) {
            thread.join();
        }

        System.out.println("Final value: " + atomicInteger.get());
    }
}

在上述代码中,创建了10个线程,每个线程对AtomicInteger对象进行1000次自增操作。如果使用普通的int变量进行自增,由于多线程竞争,最终结果可能会小于10000。但使用AtomicInteger,能够保证最终结果为10000,因为其自增操作是原子的。

AtomicLong的特性与应用

AtomicLong的方法与AtomicInteger类似

AtomicLong的方法与AtomicInteger非常相似,只是操作的数据类型为long。它也提供了getsetincrementAndGetdecrementAndGetaddAndGet等原子操作方法。

import java.util.concurrent.atomic.AtomicLong;

public class AtomicLongExample {
    public static void main(String[] args) {
        AtomicLong atomicLong = new AtomicLong(10L);
        long value = atomicLong.get();
        System.out.println("Current value: " + value);

        atomicLong.set(20L);
        long newValue = atomicLong.get();
        System.out.println("New value: " + newValue);

        long incrementedValue = atomicLong.incrementAndGet();
        System.out.println("Incremented value: " + incrementedValue);

        long decrementedValue = atomicLong.decrementAndGet();
        System.out.println("Decremented value: " + decrementedValue);

        long addedValue = atomicLong.addAndGet(5L);
        System.out.println("Added value: " + addedValue);
    }
}

AtomicLong在时间戳相关场景中的应用

在一些需要记录时间戳的场景中,AtomicLong非常有用。例如,在分布式系统中,可能需要为每个事件分配一个唯一的时间戳,并且这个操作需要在多线程环境下保证原子性。

import java.util.concurrent.atomic.AtomicLong;

public class TimestampGenerator {
    private static AtomicLong timestamp = new AtomicLong(System.currentTimeMillis());

    public static long generateTimestamp() {
        return timestamp.incrementAndGet();
    }
}

public class TimestampApp {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            long ts = TimestampGenerator.generateTimestamp();
            System.out.println("Generated timestamp: " + ts);
        }
    }
}

在上述代码中,TimestampGenerator类使用AtomicLong来生成唯一的时间戳。每次调用generateTimestamp方法时,AtomicLong的值原子地自增,确保生成的时间戳是唯一的,即使在多线程环境下也能保证这一点。

AtomicBoolean的功能与使用场景

AtomicBoolean的基本方法

AtomicBoolean提供了getset方法来获取和设置布尔值,并且这两个操作都是原子性的。

import java.util.concurrent.atomic.AtomicBoolean;

public class AtomicBooleanExample {
    public static void main(String[] args) {
        AtomicBoolean atomicBoolean = new AtomicBoolean(true);
        boolean value = atomicBoolean.get();
        System.out.println("Current value: " + value);

        atomicBoolean.set(false);
        boolean newValue = atomicBoolean.get();
        System.out.println("New value: " + newValue);
    }
}

AtomicBoolean在多线程控制中的应用

在多线程编程中,AtomicBoolean常用于控制线程的执行逻辑。例如,在一个生产者 - 消费者模型中,可以使用AtomicBoolean来表示缓冲区是否已满或为空。

import java.util.concurrent.atomic.AtomicBoolean;

class Producer implements Runnable {
    private AtomicBoolean bufferFull;

    public Producer(AtomicBoolean bufferFull) {
        this.bufferFull = bufferFull;
    }

    @Override
    public void run() {
        while (true) {
            if (!bufferFull.get()) {
                // 生产数据
                System.out.println("Producing data...");
                bufferFull.set(true);
            }
        }
    }
}

class Consumer implements Runnable {
    private AtomicBoolean bufferFull;

    public Consumer(AtomicBoolean bufferFull) {
        this.bufferFull = bufferFull;
    }

    @Override
    public void run() {
        while (true) {
            if (bufferFull.get()) {
                // 消费数据
                System.out.println("Consuming data...");
                bufferFull.set(false);
            }
        }
    }
}

public class AtomicBooleanProducerConsumerExample {
    public static void main(String[] args) {
        AtomicBoolean bufferFull = new AtomicBoolean(false);
        Thread producerThread = new Thread(new Producer(bufferFull));
        Thread consumerThread = new Thread(new Consumer(bufferFull));

        producerThread.start();
        consumerThread.start();
    }
}

在上述代码中,AtomicBoolean对象bufferFull用于控制生产者和消费者线程的行为。生产者线程在缓冲区不满时生产数据并设置bufferFulltrue,消费者线程在缓冲区满时消费数据并设置bufferFullfalse。由于AtomicBoolean的操作是原子的,避免了多线程竞争导致的不一致问题。

AtomicReference及其在无锁数据结构中的应用

AtomicReference的基本用法

AtomicReference用于对引用类型进行原子操作。它提供了getset方法来获取和设置引用,并且这两个操作是原子的。

import java.util.concurrent.atomic.AtomicReference;

class MyObject {
    private String data;

    public MyObject(String data) {
        this.data = data;
    }

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

public class AtomicReferenceExample {
    public static void main(String[] args) {
        AtomicReference<MyObject> atomicReference = new AtomicReference<>(new MyObject("Initial data"));
        MyObject value = atomicReference.get();
        System.out.println("Current value: " + value);

        MyObject newObject = new MyObject("New data");
        atomicReference.set(newObject);
        MyObject updatedValue = atomicReference.get();
        System.out.println("Updated value: " + updatedValue);
    }
}

AtomicReference在无锁链表中的应用

无锁数据结构是一种在多线程环境下不使用锁机制就能保证线程安全的数据结构。AtomicReference在实现无锁链表时起着关键作用。下面是一个简单的无锁链表示例:

import java.util.concurrent.atomic.AtomicReference;

class Node {
    int value;
    AtomicReference<Node> next;

    public Node(int value) {
        this.value = value;
        this.next = new AtomicReference<>(null);
    }
}

public class LockFreeLinkedList {
    private AtomicReference<Node> head = new AtomicReference<>(null);

    public void add(int value) {
        Node newNode = new Node(value);
        while (true) {
            Node currentHead = head.get();
            newNode.next.set(currentHead);
            if (head.compareAndSet(currentHead, newNode)) {
                break;
            }
        }
    }

    public void printList() {
        Node current = head.get();
        while (current != null) {
            System.out.print(current.value + " ");
            current = current.next.get();
        }
        System.out.println();
    }

    public static void main(String[] args) {
        LockFreeLinkedList list = new LockFreeLinkedList();
        list.add(1);
        list.add(2);
        list.add(3);
        list.printList();
    }
}

在上述代码中,AtomicReference用于原子地更新链表的头节点。add方法使用compareAndSet方法来尝试更新头节点,如果更新失败则重试,从而实现了无锁的链表插入操作。这种方式避免了使用锁带来的性能开销,提高了多线程环境下的并发性能。

原子操作的实现原理

硬件层面的支持

现代处理器提供了一些特殊的指令,如比较并交换(Compare - And - Swap,简称CAS)指令,来支持原子操作。CAS指令可以在一条指令中完成比较和赋值操作,从而保证操作的原子性。当一个线程执行CAS指令时,它会将内存中的值与一个预期值进行比较,如果相等则将内存中的值更新为新值,否则不进行任何操作。这种操作在硬件层面保证了原子性,Java的Atomic类正是利用了这些硬件特性来实现原子操作。

Java中Atomic类对CAS的使用

AtomicInteger为例,其incrementAndGet方法的实现大致如下:

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

在这个方法中,首先获取当前值current,然后计算新值next。接着使用compareAndSet方法尝试将当前值更新为新值。如果更新成功则返回新值,否则继续循环重试。compareAndSet方法最终会调用底层的CAS指令来实现原子操作。这种基于CAS的实现方式避免了使用锁,减少了线程上下文切换的开销,提高了并发性能。

CAS的ABA问题

虽然CAS在实现原子操作方面非常有效,但它也存在一个问题,即ABA问题。假设有一个线程A读取了一个值A,然后另一个线程B将这个值先改为B,再改回A。此时线程A再执行CAS操作时,会认为值没有改变而成功更新,但实际上这个值已经被修改过了。在某些场景下,这种情况可能会导致逻辑错误。

为了解决ABA问题,Java提供了AtomicStampedReferenceAtomicMarkableReference类。AtomicStampedReference通过引入一个时间戳(stamp)来记录值的版本,每次值发生变化时,时间戳也会相应变化。这样在进行CAS操作时,不仅会比较值,还会比较时间戳,从而避免了ABA问题。AtomicMarkableReference则是通过一个布尔标记来记录值是否被修改过,同样可以解决ABA问题。

Atomic类与锁机制的比较

性能方面

  1. 低竞争场景:在低竞争场景下,Atomic类的性能通常优于锁机制。因为Atomic类基于CAS操作,不需要获取锁,避免了线程上下文切换的开销。而锁机制在每次获取锁和释放锁时都需要进行线程上下文切换,这在低竞争场景下会带来不必要的性能损耗。
  2. 高竞争场景:在高竞争场景下,锁机制可能会表现得更好。因为Atomic类的CAS操作在竞争激烈时会频繁失败,导致线程需要不断重试,这会消耗大量的CPU资源。而锁机制可以通过排队等待的方式,避免过多的无效重试,在高竞争场景下能够提供更稳定的性能。

代码复杂度方面

  1. Atomic类:使用Atomic类实现多线程安全的代码相对简洁,只需要调用Atomic类提供的原子操作方法即可。例如,实现一个多线程自增操作,使用AtomicInteger只需要一行代码atomicInteger.incrementAndGet()
  2. 锁机制:使用锁机制实现相同的功能需要更多的代码。不仅需要定义锁对象,还需要在合适的位置进行加锁和解锁操作,并且要注意避免死锁等问题。例如,使用synchronized关键字实现多线程自增操作,代码如下:
class Counter {
    private int value;

    public synchronized void increment() {
        value++;
    }

    public synchronized int getValue() {
        return value;
    }
}

相比之下,使用锁机制的代码复杂度更高。

适用场景方面

  1. Atomic类:适用于对性能要求较高,且操作较为简单的场景,如简单的计数器、标志位控制等。例如,在一个高并发的Web服务器中,使用AtomicInteger来统计请求数量,能够高效地实现线程安全的计数。
  2. 锁机制:适用于对数据一致性要求非常严格,且操作较为复杂的场景。例如,在一个银行转账的操作中,需要保证多个账户余额的一致性,此时使用锁机制能够更好地控制并发访问,确保数据的准确性。

扩展Atomic类的功能

自定义原子操作

在某些情况下,Atomic类提供的标准原子操作可能无法满足特定的需求,这时可以通过继承Atomic类并实现自定义的原子操作方法来扩展其功能。例如,假设需要一个原子地将一个整数乘以2的操作,可以如下实现:

import java.util.concurrent.atomic.AtomicInteger;

class CustomAtomicInteger extends AtomicInteger {
    public CustomAtomicInteger(int initialValue) {
        super(initialValue);
    }

    public int multiplyAndGet(int factor) {
        for (;;) {
            int current = get();
            int next = current * factor;
            if (compareAndSet(current, next))
                return next;
        }
    }
}

public class CustomAtomicIntegerExample {
    public static void main(String[] args) {
        CustomAtomicInteger customAtomicInteger = new CustomAtomicInteger(5);
        int multipliedValue = customAtomicInteger.multiplyAndGet(2);
        System.out.println("Multiplied value: " + multipliedValue);
    }
}

在上述代码中,CustomAtomicInteger继承自AtomicInteger,并添加了一个multiplyAndGet方法,实现了原子地将整数乘以指定因子的操作。

使用Atomic类实现更复杂的数据结构

可以使用Atomic类来构建更复杂的线程安全数据结构。例如,实现一个线程安全的栈,可以使用AtomicReference来管理栈顶元素的引用。

import java.util.concurrent.atomic.AtomicReference;

class StackNode {
    int value;
    AtomicReference<StackNode> next;

    public StackNode(int value) {
        this.value = value;
        this.next = new AtomicReference<>(null);
    }
}

public class ThreadSafeStack {
    private AtomicReference<StackNode> top = new AtomicReference<>(null);

    public void push(int value) {
        StackNode newNode = new StackNode(value);
        while (true) {
            StackNode currentTop = top.get();
            newNode.next.set(currentTop);
            if (top.compareAndSet(currentTop, newNode)) {
                break;
            }
        }
    }

    public Integer pop() {
        while (true) {
            StackNode currentTop = top.get();
            if (currentTop == null) {
                return null;
            }
            StackNode newTop = currentTop.next.get();
            if (top.compareAndSet(currentTop, newTop)) {
                return currentTop.value;
            }
        }
    }

    public static void main(String[] args) {
        ThreadSafeStack stack = new ThreadSafeStack();
        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());
    }
}

在上述代码中,通过AtomicReference实现了一个线程安全的栈。pushpop方法使用CAS操作来保证对栈顶元素的原子更新,从而确保了多线程环境下栈操作的线程安全性。

总结Atomic类在Java多线程编程中的地位

Atomic类在Java多线程编程中扮演着重要的角色,它为开发人员提供了一种高效、简洁的方式来实现线程安全的操作。通过利用硬件层面的CAS指令,Atomic类在低竞争场景下能够提供出色的性能,避免了锁机制带来的线程上下文切换开销。同时,Atomic类的丰富功能和灵活扩展性,使其适用于各种不同的多线程场景,无论是简单的计数器,还是复杂的无锁数据结构。然而,在使用Atomic类时,也需要注意其适用场景,特别是在高竞争场景下,需要综合考虑性能和代码复杂度等因素,选择合适的并发控制策略。总之,深入理解和掌握Atomic类的原理和使用方法,对于编写高效、可靠的多线程Java程序至关重要。