一文吃透 Java 并发三大核心问题:可见性、原子性、有序性


Java并发理论基础

一、多线程的便利

  1. 更多的处理器核心
  2. 更快的响应速度

二、Java多线程并发不安全

指的是多个线程同时访问共享资源时,如果没有正确同步,可能会出现数据错误、程序异常或逻辑混乱的情况

并发不安全的核心问题:共享资源 + 缺乏同步

例如:

public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

如果多个线程同时调用increment()方法,预期结果是每次加1,但并发场景下会出现 加了多次结果却没变或者变少的 情况。

这是因为:

count++ 实际上分为三步:
1. 读取 count 的值
2. +1
3. 写回 count

多个线程可能都在几乎同时读取到相同的值,执行 +1 后写回,这就会造成“丢失更新”,即所谓的 线程不安全


如何避免并发不安全

  1. 加锁同步synchronizedReentrantLock

  2. 使用原子类:如 AtomicInteger

  3. 使用并发容器ConcurrentHashMapCopyOnWriteArrayList


三、Java多线程并发出现问题的根源

1. 可见性问题 —— 线程之间看不到彼此的最新数据

可见性:具有可见性是指一个线程对共享变量的修改,另外一个线程能够立刻看到

现象:

一个线程对共享变量的修改,对另一个线程不可见。

举例:
volatile boolean running = true;

Thread t1 = new Thread(() -> {
    while (running) {
        // do something
    }
});

如果没有volatile,另一个线程把running = false;改,但t1可能永远看不到这个修改

根源分析:
  • Java 的内存模型(JMM)中,每个线程有自己的工作内存(类似缓存),变量修改不会立即同步到主内存
  • 导致一个线程的变更,其他线程看不到

2. 原子性问题 —— 操作是不可分割的

原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

现象:

i++ 这样的操作在多线程下会出现“丢失更新”问题

举例:
count++; // 实际包含3步操作
1. 读取 count 的值
2. +1
3. 写回 count

多个线程同时读写一个变量时,操作被中断或交叉执行,结果就会出错

根源分析:
  • Java 中的普通变量操作不是原子性的
  • 虽然某些语句只有一行,但背后可能包含多步,如读 => 改 => 写
  • 在没有同步机制时,同时操作同一个共享变量,且操作过程没有正确同步,最终导致彼此的数据读错、写乱、更新丢失

3. 有序性问题 —— 指令可能被重排

有序性:即程序执行的顺序按照代码的先后顺序执行

现象:

变量可能在尚未完成初始化时就被另一个线程访问。

举例:
Room room = new Room();

这里的 room 是一个 引用变量,它并不直接存储对象本身,而是存储“指向堆内存中实际对象的地址”。

可以理解成:

  • room 是遥控器(引用)
  • new Room() 创建的是电视(对象)
什么叫“先把引用赋值”?

JVM 在执行 room = new Room(); 时,正常流程是:

1. 在堆内存中分配空间
2. 执行构造方法(初始化对象)
3. 将地址赋值给变量 room(即引用)

但为了性能优化,JVM 可能会把“步骤3”提前到“步骤2”之前

1. 分配内存
2. 将地址赋值给变量 room(引用赋值)
3. 执行构造方法(对象还没初始化 )

引用变量先获得了堆内存地址(即对象的引用),但这块内存中对应的对象还没有被完全初始化。

根源分析:

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序


四、Java 解决并发问题的方式

synchronized(内置锁)

  • 保证原子性可见性有序性
  • 对象或类作为锁对象,自动加锁解锁
1. 修饰同步方法(锁住当前对象)

代码示例:

public class SyncInstanceMethodDemo {
    public synchronized void syncMethod() {
        System.out.println(Thread.currentThread().getName() + " 进入实例同步方法");
        try { Thread.sleep(500); } catch (InterruptedException ignored) {}
        System.out.println(Thread.currentThread().getName() + " 退出实例同步方法");
    }

    public static void main(String[] args) {
        SyncInstanceMethodDemo demo = new SyncInstanceMethodDemo();
        new Thread(() -> demo.syncMethod(), "T1").start();
        new Thread(() -> demo.syncMethod(), "T2").start();
    }
}

运行结果:

T1 进入实例同步方法
T1 退出实例同步方法
T2 进入实例同步方法
T2 退出实例同步方法
  • 锁对象是当前实例对象(this)。

  • 多个线程调用同一个实例的同步方法时会被串行执行。


2. 修饰同步代码块

代码示例:

public class SyncBlockDemo {
    private final Object lock = new Object();

    public void syncBlock() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " 进入同步代码块");
            try { Thread.sleep(500); } catch (InterruptedException ignored) {}
            System.out.println(Thread.currentThread().getName() + " 退出同步代码块");
        }
    }

    public static void main(String[] args) {
        SyncBlockDemo demo = new SyncBlockDemo();
        new Thread(() -> demo.syncBlock(), "T3").start();
        new Thread(() -> demo.syncBlock(), "T4").start();
    }
}

运行结果:

T3 进入同步代码块
T3 退出同步代码块
T4 进入同步代码块
T4 退出同步代码块

解释运行过程:

  1. 主线程中创建了一个 SyncBlockDemo 对象,只有 一个 lock 对象
  2. 启动了两个线程:T3 和 T4,都调用 demo.syncBlock()
  3. 两个线程几乎同时执行,在执行 synchronized (lock) 时会尝试获取 lock 的锁
  4. 哪个线程先抢到,哪个线程就会先执行临界区的内容(打印+睡眠500ms)。
  5. 另一个线程会进入阻塞状态,直到锁释放为止。

3. 修饰静态同步方法

代码示例:

public class StaticSyncMethodDemo {
    public static synchronized void staticSyncMethod() {
        System.out.println(Thread.currentThread().getName() + " 进入静态同步方法");
        try { Thread.sleep(500); } catch (InterruptedException ignored) {}
        System.out.println(Thread.currentThread().getName() + " 退出静态同步方法");
    }

    public static void main(String[] args) {
        new Thread(() -> StaticSyncMethodDemo.staticSyncMethod(), "T5").start();
        new Thread(() -> StaticSyncMethodDemo.staticSyncMethod(), "T6").start();
    }
}

运行结果:

T5 进入静态同步方法
T5 退出静态同步方法
T6 进入静态同步方法
T6 退出静态同步方法

说明:

  • 静态同步方法锁住类的 Class对象,两个线程顺序执行。

  • 静态同步方法锁的是“类级别的锁”,也就是 JVM 为当前类创建的唯一 Class 对象,如 Example.class,不论你 new 多少个对象,这个锁都是一样的


volatile 关键字

  • 保证可见性禁止指令重排序
  • 不保证原子性(适用于状态标志)
可见性的实现

代码示例:

import java.util.concurrent.TimeUnit;

public class VolatileExample1 {

    private static volatile boolean stop = false;

    public static void main(String[] args) {
        // Thread-A
        new Thread(() -> {
            while (!stop) {
                // 模拟一些工作
            }
            System.out.println("3: " + Thread.currentThread().getName() + " 停止了");
        }, "Thread A").start();

        // Thread-main
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println("1: 主线程等待一秒...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("2: 将 stop 变量设置为 true");
        stop = true;
    }
}

运行结果:

1: 主线程等待一秒...
2: 将 stop 变量设置为 true
3: Thread A 停止了
有序性实现

代码示例:

public class VolatileOrderExample {

    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;          // 写共享变量a
        flag = true;    // 写 volatile变量
    }

    public void reader() {
        if (flag) {     // 读 volatile变量
            System.out.println(a); // 有序性保证下,这里一定会输出1
        }
    }

    public static void main(String[] args) {
        VolatileOrderExample example = new VolatileOrderExample();

        Thread t1 = new Thread(example::writer);
        Thread t2 = new Thread(example::reader);

        t1.start();
        t2.start();
    }
}
有序性体现在哪里?
  • a = 1flag = true 之前执行;
  • flag = truevolatile 写;
  • 在另一个线程中读取 flagvolatile 读)为 true 后,根据 happens-before 原则,a = 1 的写入对该线程可见
如果没有 volatile

可能会出现 flag == true,但 a == 0 的情况(由于指令重排序或可见性问题)。


final 关键字(不可变性)

final 是 Java 的一个修饰符,用于 修饰类、方法和变量,表示不可更改的含义:

使用位置表示含义
不能被继承
方法不能被重写(可以重载)
变量值不能被修改(常量)

修饰类(final class)
作用:

final 修饰的类不能被继承。

示例:
final class Animal {
    public void run() {
        System.out.println("Animal is running.");
    }
}

// 错误示例:
// class Dog extends Animal {} // 报错:不能继承 final 类

修饰方法(final method)
作用:

final 修饰的方法不能被子类重写(Override)。

示例:
class Parent {
    public final void sayHello() {
        System.out.println("Hello from parent.");
    }
}

class Child extends Parent {
    // 报错:不能重写 final 方法
    // public void sayHello() {}
}

修饰变量
作用:

变量值一旦赋值之后不能再更改,即表示常量。

分类:
类型示例特点
final 局部变量方法内声明的 final 变量赋值后不可改,常用于 lambda
final 成员变量类中的 final 字段一般和构造器搭配赋值
final 静态变量static final 修饰全局常量,命名全大写 MAX_VALUE
示例:
public class Demo {
    final int age = 18;
    static final String SCHOOL_NAME = "OpenAI Academy";

    public void test() {
        final int x = 100;
        // x = 200; // 报错:不能再赋值
    }
}
注意:
  • final 成员变量 必须在定义时或构造函数中初始化
  • final 引用类型变量:引用不能变,但对象内容可以变
final List<String> list = new ArrayList<>();
list.add("hello"); // 合法
// list = new ArrayList<>(); // 报错

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值