Java基础快速入门:多线程原子性与并发安全

本文纲要

  1. 概述
  2. 原子性概念与问题引入
    2.1 什么是原子性
    2.2 多线程操作共享数据的问题
    2.3 volatile不能保证原子性
  3. 使用synchronized实现原子性
  4. AtomicInteger原子类
    4.1 AtomicInteger简介与构造
    4.2 常用方法演示
    4.3 使用AtomicInteger解决冰淇淋问题
    4.4 内存解析:CAS算法与自旋
    4.5 源码解析
  5. 悲观锁与乐观锁

概述

在现代Java开发中,多线程并发是非常常见的场景,而原子性是保证数据安全的核心基石之一。本文通过一个“送冰淇淋”的生动案例,循序渐进地剖析原子性的概念、问题、解决方案,并深入解析volatilesynchronizedAtomicIntegerCAS算法的原理。内容贴近实际开发,适合Java初学者一次性掌握并发安全的基础。

原子性概念与问题引入

1 ) 什么是原子性

用一个小故事来理解:你送给心仪的小女孩一个冰淇淋,这个动作包含两个步骤:你送出与她接收。如果只有“送”没有“收”,或者只有“收”没有“送”,这件事就不完整。

原子性就是指:在一次或多次操作中,所有的操作要么全部执行并且不受任何干扰,要么全部不执行,这些步骤是一个不可分割的整体。

再比如,小贾给小皮汇款1000元,包含“小贾扣款”和“小皮到账”两步。如果第一步成功第二步失败,钱就消失了;如果第一步失败第二步成功,钱就凭空产生。所以原子性要求这多个操作要么同时成功,要么同时失败。

2 ) 多线程操作共享数据的问题

在Java多线程中,若多个线程同时操作共享变量,就可能破坏原子性。我们用代码来模拟送冰淇淋的场景:100个线程,每个线程送出100个冰淇淋,理论上总共是10000个。

项目结构:

threadatom/
└── src/
    └── com/
        └── wb/
            └── threadatom/
                ├── AtomDemo.java 
                └── MyAtomThread.java 

MyAtomThread.java

package com.wb.threadatom;
 
public class MyAtomThread implements Runnable {
    private volatile int count = 0; // 送冰淇淋的数量 
 
    @Override 
    public void run() {
        for (int i = 0; i < 100; i++) {
            // 1.从共享数据中读取数据到本线程栈中 
            // 2.修改本线程栈中变量副本的值 
            // 3.把本线程栈中变量副本的值赋值给共享数据 
            count++;
            System.out.println("已经送了" + count + "个冰淇淋");
        }
    }
}

AtomDemo.java

package com.wb.threadatom;
 
public class AtomDemo {
    public static void main(String[] args) {
        MyAtomThread atom = new MyAtomThread();
        for (int i = 0; i < 100; i++) {
            new Thread(atom).start();
        }
    }
}

运行多次,会发现总数量经常是9999、9997等,并非10000。为什么?因为count++操作在内存中并非原子操作,它实际包含三步:

  1. 从共享内存读取count的值到线程本地栈;
  2. 在线程栈内对副本加1;
  3. 将新值写回共享内存。

在多线程环境下,CPU时间片可能在任何一步被切换。例如:线程A读取count=100,本地副本变成101;此时线程B抢到CPU,读到的count仍为100,也自增为101。两个线程各自加1,最终写回去的还是101,而不是102。这就是count++不具备原子性导致的数据错误。

3 ) volatile不能保证原子性

或许你会想:在count前加上volatile关键字能否解决?volatile能保证变量的可见性(每次读取都是主存中的最新值),但它不能保证操作的原子性。即便线程B在读取时能看到线程A修改后的值,如果线程A的写操作还未完成,线程B读到的仍然是旧值。以下内存图可以说明:

共享内存 count=100线程B线程A共享内存 count=100线程B线程A最终值为101,而不是102读count=100副本count=101 (还未写回)读count=100 (仍是旧值)副本count=101写回101写回101

因此,volatile 只能保证读到最新值,但阻止不了多个线程同时读写造成的覆盖问题。要解决原子性,必须引入锁或原子类。

使用synchronized实现原子性

synchronized同步代码块可以保证同一时间只有一个线程执行临界区代码,从而保证原子性。

1 ) 项目结构:

threadatom2/
└── src/
    └── com/
        └── wb/
            └── threadatom2/
                ├── AtomDemo.java 
                └── MyAtomThread.java 

MyAtomThread.java

package com.wb.threadatom2;
 
public class MyAtomThread implements Runnable {
    private volatile int count = 0; // 送冰淇淋的数量 
    private Object lock = new Object();
 
    @Override 
    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized (lock) {
                count++;
                System.out.println("已经送了" + count + "个冰淇淋");
            }
        }
    }
}

AtomDemo.java

package com.wb.threadatom2;
 
public class AtomDemo {
    public static void main(String[] args) {
        MyAtomThread atom = new MyAtomThread();
        for (int i = 0; i < 100; i++) {
            new Thread(atom).start();
        }
    }
}

我们只创建了一个MyAtomThread对象并传递给100个线程,因此锁对象lock是唯一的。无论运行多少次,输出总数始终为10000。原因:线程进入synchronized块前必须先获取锁,其他线程即使抢到CPU也无法进入,只有当前线程执行完count++并释放锁后,下一个线程才能继续操作。这保证了count++的原子性。

不过,synchronized每次都需要加锁、判断和释放锁,性能开销较大。有没有更轻量的方式呢?

AtomicInteger原子类

从JDK 1.5开始,java.util.concurrent.atomic包提供了一系列原子类,可以高效、线程安全地更新变量,无需显式加锁。其中最常用的就是AtomicInteger

1 ) AtomicInteger简介与构造

AtomicInteger提供了两个构造方法:

  • AtomicInteger():创建初始值为0的原子整数。
  • AtomicInteger(int initialValue):创建指定初始值的原子整数。

项目结构:

threadatom3/
└── src/
    └── com/
        └── wb/
            └── threadatom3/
                ├── MyAtomIntergerDemo1.java 
                └── MyAtomIntergerDemo2.java 

MyAtomIntergerDemo1.java

package com.wb.threadatom3;
 
import java.util.concurrent.atomic.AtomicInteger;
 
public class MyAtomIntergerDemo1 {
    public static void main(String[] args) {
        // 无参构造,默认值为0 
        AtomicInteger ac = new AtomicInteger();
        System.out.println(ac);   // 输出0 
 
        // 带初始值的构造 
        AtomicInteger ac2 = new AtomicInteger(10);
        System.out.println(ac2);  // 输出10 
    }
}

2 ) 常用方法演示

AtomicInteger 的核心方法及作用如下:

方法说明
int get()获取当前值
int getAndIncrement()以原子方式将当前值加1,返回自增前的值
int incrementAndGet()以原子方式将当前值加1,返回自增后的值
int addAndGet(int delta)以原子方式将当前值加上delta,返回相加后的值
int getAndSet(int newValue)以原子方式设置为newValue,并返回旧值

我们将这些方法逐个演示(原代码中多个注释块分别为独立示例,这里逐一展示):

MyAtomIntergerDemo2.java

package com.wb.threadatom3;
 
import java.util.concurrent.atomic.AtomicInteger;
 
public class MyAtomIntergerDemo2 {
    public static void main(String[] args) {
        // --- get() 演示 --- 
        AtomicInteger ac1 = new AtomicInteger(10);
        System.out.println(ac1.get());   // 输出10 
 
        // --- getAndIncrement() 演示 --- 
        AtomicInteger ac2 = new AtomicInteger(10);
        int andIncrement = ac2.getAndIncrement();
        System.out.println(andIncrement); // 输出10(自增前的值)
        System.out.println(ac2.get());    // 输出11(当前值)
 
        // --- incrementAndGet() 演示 --- 
        AtomicInteger ac3 = new AtomicInteger(10);
        int i1 = ac3.incrementAndGet();
        System.out.println(i1);          // 输出11(自增后的值)
        System.out.println(ac3.get());   // 输出11 
 
        // --- addAndGet() 演示 --- 
        AtomicInteger ac4 = new AtomicInteger(10);
        int i2 = ac4.addAndGet(20);
        System.out.println(i2);          // 输出30 
        System.out.println(ac4.get());   // 输出30 
 
        // --- getAndSet() 演示 --- 
        AtomicInteger ac5 = new AtomicInteger(100);
        int andSet = ac5.getAndSet(20);
        System.out.println(andSet);      // 输出100(旧值)
        System.out.println(ac5.get());   // 输出20 
    }
}

3 ) 使用AtomicInteger解决冰淇淋问题

AtomicInteger替换原有的volatile int count,并调用incrementAndGet(),无需任何锁即可保证线程安全。

项目结构:

threadatom4/
└── src/
    └── com/
        └── wb/
            └── threadatom4/
                ├── AtomDemo.java 
                └── MyAtomThread.java 

MyAtomThread.java

package com.wb.threadatom4;
 
import java.util.concurrent.atomic.AtomicInteger;
 
public class MyAtomThread implements Runnable {
    // 使用原子类替代 volatile int count 
    AtomicInteger ac = new AtomicInteger(0);
 
    @Override 
    public void run() {
        for (int i = 0; i < 100; i++) {
            // 原子自增,并获取自增后的值 
            int count = ac.incrementAndGet();
            System.out.println("已经送了" + count + "个冰淇淋");
        }
    }
}

AtomDemo.java

package com.wb.threadatom4;
 
public class AtomDemo {
    public static void main(String[] args) {
        MyAtomThread atom = new MyAtomThread();
        for (int i = 0; i < 100; i++) {
            new Thread(atom).start();
        }
    }
}

多次运行,结果始终为10000,高效且线程安全。incrementAndGet()在底层利用了CAS(Compare-And-Swap)算法,避免了重量级锁。

4 ) 内存解析:CAS算法与自旋

CAS全称Compare-And-Swap,是CPU级别的原子指令,包含三个操作数:

  • 内存值V:共享变量的当前值;
  • 旧的预期值A:线程从内存中读取时记录的值;
  • 要修改的值B:希望将V更新成的值。

工作流程:比较V与A,若相等则说明没有其他线程修改过,将V更新为B;若不相等,则说明已被其他线程修改,本次修改失败,重新读取最新的V作为A,再次计算B并重试。这个重试过程称为自旋

以下用内存图演示两个线程通过CAS完成自增的过程:

共享内存 V=100线程B线程A共享内存 V=100线程B线程A读取V=100,记录为旧值A=100计算B=101读取V=100,记录旧值A=100计算B=101CAS(100,101) → V==A? true更新成功,V=101CAS(100,101) → V(101) != A(100) → false更新失败自旋:重新读取V=101,新A=101再计算B=102CAS(101,102) → V==A? true更新成功,V=102

最终值变为102,两次自增均成功。CAS通过不断自旋保证最终一致性,且没有加锁的开销。

5 ) 源码解析

我们看一下AtomicIntegerincrementAndGet()源码,验证上面的逻辑。

incrementAndGet()方法:

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

它调用了Unsafe类的getAndAddInt,然后对返回值加1,表示自增后的结果。参数this是当前的AtomicInteger对象,valueOffset是内部变量value的内存偏移量,1表示加1。

getAndAddInt方法(简化版):

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);     // 读取内存最新值作为旧值v 
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}
  • getIntVolatile(o, offset):从主存获取对象o在偏移量offset处的当前值,记为旧值v。
  • compareAndSwapInt(o, offset, v, v + delta):比较当前内存值是否仍等于v。如果相等,则更新为v+delta并返回true,循环结束;如果不相等,返回false,循环继续(自旋),重新读取最新值。

所以整个过程与前面内存图完全吻合:读取旧值 → 计算新值 → CAS比较并交换 → 失败则自旋重试compareAndSwapIntnative方法,直接调用CPU指令,保证了“比较+交换”的原子性。

悲观锁与乐观锁

使用synchronized和CAS都能保证共享数据的安全,但两者的设计哲学截然不同:

特性synchronized(悲观锁)CAS(乐观锁)
基本思想每次操作前认为一定有其他线程会修改,因此直接加锁,阻塞其他线程假设没有其他线程修改,不加锁,只在更新时检查是否被修改过
实现方式JVM内置锁,线程阻塞/唤醒CPU原子指令,自旋重试
是否阻塞线程是,未获得锁的线程进入阻塞状态否,失败后通过循环重试,不阻塞
性能开销上下文切换开销较大,适合写操作多的场景自旋消耗CPU,适合读多写少、冲突较少的场景
示例synchronized(lock){ ... }AtomicInteger.incrementAndGet()

二者在实际开发中各有适用领域。对于简单计数、标记等操作,优先考虑原子类和CAS;对于复杂业务逻辑,可能需要显式锁来保证整体一致性。理解它们的区别,能帮助你在并发编程时做出更合适的选择。

总结

我们从原子性的基本概念出发,经历了volatile的局限、synchronized的保障、AtomicInteger的高效解决方案,并深入CAS原理与源码,最后对比了悲观锁与乐观锁。

这些知识是Java并发编程的基石,希望能帮助你打下扎实的基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值