Java并发基础:原子变量在多线程同步中的专业应用!| 多线程篇(七)

java 复制代码
环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

一、前言

还是原来的配方,还是儿时的味道,老样子!在开启本章节内容之前,先来回顾一波上期《Java并发工具类:构建高效多线程应用的关键!| 多线程篇(六)》内容,通过上期的教学内容,我们主要学习Java并发工具类(CountDownLatchCyclicBarrierSemaphorePhaser),通过这些工具类,我们可以提高程序性能,优化多线程程序的性能,减少线程间的不必要等待,提高资源利用率,还能实现复杂的同步逻辑,轻松实现复杂的线程间协作,如多阶段数据处理、循环任务执行等。核心内容包含如下:

  1. 并发工具类的基本概念:了解每种工具类的用途和工作机制。
  2. 并发工具类的使用场景:掌握何时何地应该使用这些工具类。
  3. 并发工具类的源码解析:深入理解它们的内部实现原理。
  4. 并发工具类的案例分析:通过实际案例,展示这些工具类的应用。
  5. 并发工具类的优点与局限:评估它们在不同场景下的适用性和潜在问题。

现在,让我们带着对并发工具类的深刻理解,学习Java并发编程的另一个核心知识点------原子变量。所谓的原子变量,它其实是Java并发API中的另一个核心组件,它们对于实现线程安全的计数器、累加器以及提供不可变对象的原子更新至关重要。正如并发工具类帮助我们解决了线程间的协调问题,原子变量则专注于在多线程环境下保证单个变量的原子操作。原子变量的使用场景广泛,从简单的原子计数器到复杂的不可变对象的线程安全更新,它们都扮演着不可或缺的角色。

所以,在接下来的内容中,我们将一起学习以下关于原子变量的主题:

  1. 原子变量的基本概念:了解原子变量的工作原理和它们如何保证操作的原子性。
  2. 原子变量的使用场景:探讨在何种情况下使用原子变量比传统同步机制更加合适。
  3. 原子变量的源码解析 :深入分析AtomicIntegerAtomicLongAtomicReference等类的内部实现。
  4. 原子变量的案例分析:通过实际的编程案例,展示原子变量在多线程程序中的应用。
  5. 原子变量的优点与局限:评估原子变量在不同场景下的性能表现和潜在的使用限制。

随着我们对原子变量的深入了解,你将会发现它们是实现高效并发程序的有力工具。现在,让我们开始新的学习篇章,一起探索原子变量的强大,并掌握它们在Java并发编程中的使用技巧吧。

二、摘要

本文将详细介绍Java中的原子变量,它们是用于多线程环境下保证数据原子性的工具。通过实际案例和源码分析,我们将了解如何使用这些原子类以及它们在实际开发中的应用。

三、正文

0.变量-概述

在学习该知识点前,我先问问大家,对原子变量了解多少?又能够清晰地讲解出其概念呢?可能部分同学抱着学习的心态来,会认真思索,但是这些如果你不会,那么请务必认真听。

所谓原子变量,它们是一组特殊的变量,设计用来在多个线程同时操作数据时,保证操作的安全性和准确性。就像使用普通刀叉吃饭,如果很多人同时使用,可能会发生抢食或食物掉落的情况。而原子变量则像是为每个人提供了一把专属的餐具,确保每个人都能有序、安全地吃到饭。

  • AtomicInteger:用于原子地操作整数类型的变量。
  • AtomicLong:用于原子地操作长整数类型的变量。
  • AtomicReference:用于原子地操作对象引用。

从专业角度上来说,原子变量是基于java.util.concurrent.atomic包中的类实现的,它们通过volatile关键字和CAS(Compare-And-Swap)算法来确保复合操作的原子性和内存可见性。这些变量适用于高并发场景下的基础数据类型和对象引用的线程安全操作,有效避免了传统同步机制可能带来的性能瓶颈和复杂的编程模型。

在设计并发程序时,原子变量提供了一种无锁的编程范式,允许开发者以一种声明式的方式处理并发问题,从而编写出既简洁又高效的多线程代码。然而,它们也有局限性,比如在处理复杂的复合操作时可能不如锁直观,且存在ABA问题等并发挑战。因此,合理选择并发控制手段,根据具体场景权衡使用原子变量还是其他同步机制,是每个并发编程者需要掌握的技能。

在多线程编程中,数据的原子性是一个常见问题。为了保证线程安全,我们需要确保一个操作或者多个操作在多个线程间是不可分割的。本章将深入探讨这些原子变量,包括AtomicIntegerAtomicLongAtomicReference

1.原子操作-实现原理

原子变量的核心是CAS(Compare-And-Swap)操作。CAS操作是一种无锁算法,它通过比较和交换的方式来确保变量的更新操作是原子的。具体而言,CAS操作涉及三个操作数:

  1. 内存地址:存储变量的内存位置。
  2. 期望值:期望变量当前的值。
  3. 新值:要更新的新值。

CAS操作检查内存地址处的当前值是否与期望值相等,如果相等,则将其更新为新值;如果不相等,则说明其他线程已经修改了该值,CAS操作失败,线程可以选择重试或执行其他操作。

java 复制代码
public class CASExample {
    private final AtomicInteger atomicInteger = new AtomicInteger(0);

    public void increment() {
        int expectedValue;
        int newValue;
        do {
            expectedValue = atomicInteger.get();
            newValue = expectedValue + 1;
        } while (!atomicInteger.compareAndSet(expectedValue, newValue));
    }
}

在这个示例中,compareAndSet()方法使用CAS操作来确保atomicInteger的自增操作是线程安全的。

2.常用的原子变量类型

原子变量是Java并发编程中的基础构件,它们利用底层硬件的原子指令来保证操作的原子性。以下是Java中常见的几种原子变量:

  • AtomicInteger:原子更新整型值。
  • AtomicLong:原子更新长整型值。
  • AtomicReference:原子更新引用类型。

如上述所讲,对于AtomicIntegerAtomicLongAtomicReference,这仨是Java并发API中提供的几个原子类,它们分别用于对基本数据类型intlong和引用类型进行原子操作。下面我将对这三个类进行通俗易懂且专业性的概括一波,希望能够大家理解学习。

1.AtomicInteger

AtomicIntegerAtomicLong分别是原子性地操作intlong类型变量的类。它们提供了一组常用的原子操作方法,如incrementAndGet()decrementAndGet()addAndGet()等。

  • 描述AtomicInteger是一个使用原子操作来更新整数值的类。它适用于需要频繁进行自增(increment)或自减(decrement)操作的场景,如计数器或统计指标。
  • 特点 :通过原子的方式保证操作的安全性,即使多个线程同时对同一个AtomicInteger实例进行操作,也能保证最终结果的正确性。

2.AtomicLong

  • 描述AtomicLong类似于AtomicInteger,但它是针对long类型数据的。在需要对长整型数值进行原子操作时使用,比如处理大数值的计数或累加。
  • 特点 :由于long类型的数据在多线程操作中更容易受到CPU乱序执行的影响,AtomicLong确保了对这些大数值的操作同样具有原子性。

3. AtomicBoolean

AtomicBoolean是用于操作boolean类型变量的原子类,常用于实现简单的状态管理或标志位。

java 复制代码
public class AtomicBooleanExample {
    private final AtomicBoolean isReady = new AtomicBoolean(false);

    public void setReady() {
        isReady.set(true);
    }

    public boolean isReady() {
        return isReady.get();
    }

    public static void main(String[] args) {
        AtomicBooleanExample example = new AtomicBooleanExample();
        example.setReady();
        System.out.println("Is ready: " + example.isReady());
    }
}

4. AtomicReference

AtomicReference用于操作对象引用的原子类。它可以确保在多线程环境中安全地更新对象引用,常用于实现非阻塞的数据结构。

java 复制代码
public class AtomicReferenceExample {
    private final AtomicReference<String> atomicReference = new AtomicReference<>("initial value");

    public void updateValue(String newValue) {
        atomicReference.set(newValue);
    }

    public String getValue() {
        return atomicReference.get();
    }

    public static void main(String[] args) {
        AtomicReferenceExample example = new AtomicReferenceExample();
        example.updateValue("updated value");
        System.out.println("Value: " + example.getValue());
    }
}

3.原子变量的专业应用场景

3.1高效计数器

在高并发环境中,计数器的操作可能会成为性能瓶颈。使用AtomicInteger可以实现一个高效的线程安全计数器。

java 复制代码
public class Counter {
    private final AtomicInteger counter = new AtomicInteger(0);

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

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

这种方式比使用synchronized关键字或显式锁的计数器要高效得多,特别是在需要频繁更新计数器的场景中。

3.2非阻塞算法

非阻塞算法是一类不需要锁的并发算法,使用AtomicReference可以实现许多非阻塞数据结构,如无锁队列、无锁栈等。

java 复制代码
import java.util.concurrent.atomic.AtomicReference;

public class LockFreeStack<T> {
    private static class Node<T> {
        final T value;
        Node<T> next;

        Node(T value) {
            this.value = value;
        }
    }

    private final AtomicReference<Node<T>> top = new AtomicReference<>();

    public void push(T value) {
        Node<T> newNode = new Node<>(value);
        Node<T> oldTop;
        do {
            oldTop = top.get();
            newNode.next = oldTop;
        } while (!top.compareAndSet(oldTop, newNode));
    }

    public T pop() {
        Node<T> oldTop;
        Node<T> newTop;
        do {
            oldTop = top.get();
            if (oldTop == null) {
                return null; // 栈为空
            }
            newTop = oldTop.next;
        } while (!top.compareAndSet(oldTop, newTop));
        return oldTop.value;
    }
}

在这个示例中,LockFreeStack类实现了一个无锁栈,使用AtomicReference来保证栈顶指针的原子性更新。

3.3并发控制标志

AtomicBoolean可以用于实现简单的并发控制标志,如实现一个只运行一次的任务(类似于java.util.concurrent中的AtomicBoolean实现的runOnce模式)。

java 复制代码
public class OneTimeTask {
    private final AtomicBoolean hasRun = new AtomicBoolean(false);

    public void run() {
        if (hasRun.compareAndSet(false, true)) {
            // 仅第一次调用时执行任务
            System.out.println("Task is running.");
        } else {
            System.out.println("Task has already run.");
        }
    }

    public static void main(String[] args) {
        OneTimeTask task = new OneTimeTask();
        task.run();  // 输出:Task is running.
        task.run();  // 输出:Task has already run.
    }
}

4.原子变量-使用场景

接着我们来聊聊它们的一些使用场景,对于原子变量,它适用于需要频繁更新和读取的场景,尤其是在高并发的情况下。相比于使用synchronized关键字或显式锁,原子变量通常能提供更好的性能。以下是一些典型的使用场景:

  • 计数器和累加器,如统计事件次数、访问量等。
  • 多线程环境中的资源限制,如限制同时访问某个资源的线程数。
  • 需要原子更新对象引用的场景,如在某些设计模式中。

经典的可能就如上这些场景,还有很多其他场景也能够被使用,这都需要大家在学习完后,能够吃透它,方才能使用时得心应手。

5.原子变量-源码解析

这里,我们来分析一下它的源码,基于原子变量的实现,大多基于java.util.concurrent.atomic包中的AbstractQueuedSynchronizer(AQS)或者通过循环CAS操作来保证原子性。以下是AtomicInteger的一个简单源码解析示例:

java 复制代码
public final int get() {
    return value;
}

public final void set(int newValue) {
    Unsafe unsafe = UNSAFE;
    unsafe.putOrderedInt(this, valueOffset, newValue);
}

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

在这个示例中,Unsafe类被用来执行底层的原子操作,getAndAddInt方法用于原子地增加值,putOrderedInt确保新值的设置对其他线程可见。如下是我对其源码的详细解读,大家请看:

上段代码是AtomicInteger类中几个关键方法的示例,展示了如何使用底层的Unsafe类来实现原子操作。下面我将对这些方法进行详细解释:

  1. get() 方法

    • 功能:返回当前AtomicInteger的值。
    • 实现:直接返回内部的value成员变量。由于value被声明为volatile,因此get()方法保证了内存的可见性,即直接返回最值的值。
  2. set(int newValue) 方法

    • 功能:设置AtomicInteger的值为newValue
    • 实现:使用Unsafe类的putOrderedInt方法来原子地设置value的值。putOrderedInt确保了该操作的原子性,并且对其他线程立即可见。valueOffsetvalue在内存中的偏移量。
  3. incrementAndGet() 方法

    • 功能:原子地将当前值增加1,并返回增加后的值。
    • 实现:使用Unsafe类的getAndAddInt方法原子地将value增加1。getAndAddInt方法返回增加前的原始值,因此通过加1操作得到增加后的值并返回。

5.1核心概念剖析

  • UnsafeUnsafe是Java中用于执行低级别、高性能的原始操作的工具类。它提供了硬件级别的原子操作,如CAS(Compare-And-Swap)。
  • volatile关键字volatile关键字在Java中用于声明一个变量的值在多个线程间共享,并且它的修改对所有线程立即可见。
  • 原子操作:原子操作是指在多线程环境中,一个操作或者一系列操作要么完全执行,要么完全不执行,不会出现中间状态,从而避免数据不一致的问题。

5.2为什么使用Unsafe?

这里请大家思考一个问题,为何要使用Unsafe?其实很简单。Unsafe提供了一种方式来执行那些在Java中通常需要同步控制的原始操作,但它不使用锁。这使得AtomicInteger和其他原子类在执行操作时更加高效,因为它们避免了锁的开销。然而,使用Unsafe的代码通常更难以理解和维护,并且可能依赖于特定的硬件架构。

总的来说,上段AtomicInteger的部分源码,它展示了如何利用Java的并发工具和底层硬件特性来实现高效的线程安全操作,还是很值得学习跟借鉴的。

6.案例演示

6.1案例代码

这里,我们来假设一个多线程累加的场景,我们可以使用AtomicInteger来实现一个安全的累加器:

java 复制代码
package com.secf.service.port.day7;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

/**
 * 原子变量使用示例
 *
 * @Author bug菌
 * @Source 公众号:猿圈奇妙屋
 * @Date 2024年7月2日10:11:33
 */
public class Counter {
    private AtomicInteger count = new AtomicInteger(0);

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

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

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        IntStream.range(0, 10).forEach(i -> {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            }).start();
        });

        Thread.sleep(1000); // 等待所有线程完成
        System.out.println("Total count: " + counter.getCount());
    }
}

在这个例子中,AtomicInteger确保了即使在多个线程同时递增的情况下,计数也是准确的。

6.2执行结果演示

根据如上的案例,作者在本地进行测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加其他的测试数据或测试方法,以便于进行熟练学习以此加深知识点的理解。

6.3代码解析

上述案例代码演示了AtomicInteger在多线程环境中如何使用,以确保对共享计数器的原子操作。下面是对代码的逐行分析:

  1. 包声明package com.secf.service.port.day7; 定义了类的包名,表明这个类属于com.secf.service.port.day7包。

  2. 导入语句import java.util.concurrent.atomic.AtomicInteger;import java.util.stream.IntStream; 分别导入了AtomicInteger类和Java 8的流操作工具。

  3. 类定义public class Counter { 定义了一个名为Counter的公共类。

  4. 成员变量private AtomicInteger count = new AtomicInteger(0);Counter类中定义了一个AtomicInteger类型的私有成员变量count,并初始化为0。

  5. increment()方法public void increment() { count.incrementAndGet(); } 定义了一个方法increment,用于原子地递增count的值。

  6. getCount()方法public int getCount() { return count.get(); } 定义了一个方法getCount,用于返回count的当前值。

  7. main()方法

    • public static void main(String[] args) throws InterruptedException { 定义了程序的入口点。
    • Counter counter = new Counter(); 创建了Counter类的一个实例。
    • IntStream.range(0, 10).forEach(i -> { ... }); 创建了10个线程,每个线程执行一个循环,循环体中包含1000次对increment方法的调用。
    • new Thread(() -> { ... }).start(); 为每个迭代创建并启动一个新线程。
    • Thread.sleep(1000); 主线程休眠1秒,以等待所有子线程完成它们的任务。
    • System.out.println("Total count: " + counter.getCount()); 打印最终的计数值。
  8. 线程安全 :通过使用AtomicInteger,即使多个线程同时对count进行递增操作,也能保证操作的原子性,从而确保最终的计数值是准确的。

  9. 等待所有线程完成main方法中的Thread.sleep(1000);是一个简化的等待策略,用于等待所有子线程完成。在实际应用中,可能需要更可靠的线程同步机制。

我分享这个案例主要是想演示如何利用AtomicInteger来实现一个简单的多线程安全计数器。通过这种方式,我们可以避免使用传统的同步机制(如synchronized关键字),同时确保程序的线程安全性和性能。

7.原子变量-优点与局限

7.1优点

  • 性能高 :原子变量通常比使用synchronized或显式锁有更高的性能。
  • 易于使用:原子变量的API简单直观,易于使用。
  • 减少锁的开销:避免了使用锁带来的上下文切换开销。

7.2局限

  • 操作的局限性:原子变量仅适用于单一变量的原子操作。如果操作涉及多个变量的原子性,使用原子变量将非常困难或不可能实现。在这种情况下,锁机制是更好的选择。
  • ABA问题 :在某些场景中,CAS操作可能会遭遇ABA问题,即一个变量被线程A读取为值X,然后被线程B改为Y,再被线程A改回X。虽然对CAS操作的比较结果没有影响,但这可能会导致逻辑错误。为了解决ABA问题,可以使用AtomicStampedReference或类似的解决方案。

通过深入理解原子变量的工作原理和使用场景,我们可以在适当的场合使用它们来提高程序的性能和响应速度。同时,我们也需要注意它们的局限性,并根据具体情况选择合适的并发控制手段。

7.3原子变量与锁机制的比较

7.3.1性能对比

  • 原子变量的优势:原子变量基于无锁机制,实现了较高的并发性,尤其适合高频率的读写操作,如计数器、自旋锁等场景。由于不涉及线程阻塞和上下文切换,原子变量的操作通常比使用锁的操作更高效。
  • 锁的优势:锁机制适合复杂的临界区保护,特别是在需要多个步骤的原子操作时,锁提供了更强的同步保障。对于需要控制多个共享资源的场景,锁的使用更加灵活。

7.3.2适用场景

  • 原子变量:适用于单一变量的简单更新操作,如计数器、自增操作、状态标志等。在高并发的环境中,原子变量通常可以提供更好的性能。
  • 锁机制:适用于复杂的同步需求,如多个变量的操作、涉及多个步骤的事务性操作等。锁提供的互斥性和条件等待机制使其适合处理复杂的多线程场景。

8.测试用例

8.1测定代码

以下是main函数中的测试用例示例:

java 复制代码
/**
 * 原子变量使用示例
 *
 * @Author bug菌
 * @Source 公众号:猿圈奇妙屋
 * @Date 2024年7月2日10:11:33
 */
public class AtomicTest {
    public static void main(String[] args) {
        AtomicInteger count = new AtomicInteger(0);
        // 模拟多线程递增
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    count.incrementAndGet();
                }
            }).start();
        }

        // 等待所有线程完成
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println("Final count is: " + count.get());
    }
}

8.2执行结果演示

根据如上的案例,作者在本地进行测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加其他的测试数据或测试方法,以便于进行熟练学习以此加深知识点的理解。

8.3代码解析

接着我将对上述代码进行详细的一个逐句解读,希望能够帮助到同学们,能以更快的速度对其知识点掌握学习,这也是我写此文的初衷,授人以鱼不如授人以渔,只有将其原理摸透,日后应对场景使用,才能得心应手,所以如果有基础的同学,可以略过如下代码分析步骤,然而没基础的同学,还是需要加强对代码的理解,方便你深入理解并掌握其常规使用。

上述测试代码是一个多线程环境下使用AtomicInteger的测试示例,用于展示如何在多个线程间安全地进行计数操作。下面是对代码的详细解释:

  1. 类定义public class AtomicTest { 定义了一个名为AtomicTest的公共类。

  2. main方法public static void main(String[] args) { 程序的入口点。

  3. 创建AtomicIntegerAtomicInteger count = new AtomicInteger(0); 创建了一个初始值为0的AtomicInteger对象count

  4. 多线程模拟

    • for (int i = 0; i < 10; i++) { ... } 循环10次,每次创建一个新的线程。
    • new Thread(() -> { ... }).start(); 使用lambda表达式创建并启动线程。线程的任务是在内部循环中递增count的值,每次循环调用incrementAndGet()方法。
  5. 等待线程完成

    • while (Thread.activeCount() > 2) { Thread.yield(); } 使用一个循环来等待除了主线程和当前活跃线程之外的其他线程完成。Thread.activeCount()返回当前活跃的线程数量,这里假设除了主线程外,最多有10个线程是活跃的(每个线程执行完毕后,计数器会减1)。Thread.yield();让出当前线程的执行权,以便其他线程可以执行。
  6. 输出最终结果System.out.println("Final count is: " + count.get()); 打印count的最终值。由于countAtomicInteger类型,即使在多线程环境下,每次递增操作也是安全的,因此最终输出的值应该是100(10个线程,每个线程递增10次)。

  7. 线程同步的简化 :代码中的线程同步策略(while循环和Thread.yield())是一种简化的实现,用于等待所有线程完成。在实际应用中,可能需要更可靠的线程同步机制,例如使用CountDownLatch或其他同步辅助工具。

这段代码演示了AtomicInteger如何在没有使用synchronized关键字的情况下,安全地在多个线程间进行递增操作。这是原子变量在并发编程中的一个典型应用,展示了它们在提高性能和简化编程模型方面的优势。

四、小结

原子变量的学习不仅是对Java并发编程知识的补充,更是对编程思维的一次锻炼。它们教会我们在面对复杂的并发问题时,如何选择合适的工具和方法来实现高效、安全的编程。随着技术的不断进步,原子变量及其背后的原理将继续在并发编程领域发挥重要作用。

五、总结

原子变量作为Java并发编程中的重要工具,为我们提供了一种高效且简洁的方式来处理多线程环境下的数据同步问题。通过本章的学习,我们深入理解了原子变量的基本概念、工作原理以及它们在实际编程中的应用。

... ...

ok,以上就是我这期的全部内容啦,若想学习更多,你可以持续关注我,我会把这个多线程篇系统性的更新,保证每篇都是实打实的项目实战经验所撰。只要你每天学习一个奇淫小知识,日积月累下去,你一定能成为别人眼中的大佬的!功不唐捐,久久为功!

「学无止境,探索无界」,期待在技术的道路上与你再次相遇。咱们下期拜拜~~

六、往期推荐

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

相关推荐
许野平22 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
duration~37 分钟前
Maven随笔
java·maven
zmgst41 分钟前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
跃ZHD1 小时前
前后端分离,Jackson,Long精度丢失
java
blammmp1 小时前
Java:数据结构-枚举
java·开发语言·数据结构
暗黑起源喵1 小时前
设计模式-工厂设计模式
java·开发语言·设计模式
WaaTong2 小时前
Java反射
java·开发语言·反射
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
九圣残炎2 小时前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
wclass-zhengge2 小时前
Netty篇(入门编程)
java·linux·服务器