在并发编程中,多线程是提升程序效率的重要手段,但也伴随着线程安全等问题。本文将从线程状态、线程安全风险及解决方案等方面,带你初识多线程的核心基础知识。

一、线程的状态

线程在生命周期中会经历多种状态,Java 中线程的状态主要分为以下几种,其中与"等待"相关的状态需要重点区分:

  • NEW:线程对象已创建,但尚未调用 start() 方法(未启动)。
  • RUNNABLE:线程正在 Java 虚拟机中执行,或等待操作系统调度(如等待 CPU 时间片),处于可运行状态。
  • BLOCKED:表示线程正在等待获取锁(如进入synchronized代码块时未获取到锁)。
  • WAITING:表示线程在无限期等待其他线程的通知(如调用wait()join()等方法后),需其他线程主动唤醒(如调用notify())。
  • TIMED_WAITING:表示线程在有限时间内等待唤醒(如调用sleep(1000)wait(1000)等方法),超时后会自动唤醒。
  • TERMINATED:线程已执行完毕(run()方法退出),生命周期结束

示例:观察线程状态转换

public class ThreadStateDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                // 进入TIMED_WAITING状态(超时等待)
                Thread.sleep(1000);
                // 进入WAITING状态(无限等待)
                synchronized (ThreadStateDemo.class) {
                    ThreadStateDemo.class.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 新建状态
        System.out.println("线程启动前状态:" + t.getState()); // NEW
        t.start();
        Thread.sleep(100);
        System.out.println("线程休眠时状态:" + t.getState()); // TIMED_WAITING
        Thread.sleep(1000);
        System.out.println("线程等待时状态:" + t.getState()); // WAITING
    }
}

二、多线程带来的风险——线程安全(重点)

多线程并发执行时,若不加以控制,可能导致线程安全问题。我们先从现象入手,逐步分析本质。

1. 观察线程不安全

最经典的例子是多线程同时修改共享变量:

public class ThreadUnsafeDemo {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        // 两个线程同时对count自增10000次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("最终结果:" + count); // 预期20000,实际可能小于20000
    }
}

现象:最终结果往往小于20000。原因是count++并非原子操作,多线程并发时会出现"丢失更新"。

2. 线程安全的概念

线程安全是指:在多线程环境下,无论线程调度方式如何,程序都能保证正确的执行结果(与单线程执行结果一致)。

3. 线程不安全的原因

a. 线程调度是随机的

操作系统对线程的调度是抢占式的,执行顺序不可预测。这是线程安全问题的根本原因,使得多线程执行路径存在无数种可能。

b. 修改共享数据

多个线程同时读写同一份共享数据(如上述例子中的count),若缺乏同步控制,会导致数据不一致。

c. 原子性问题

原子性是指:一个操作或一组操作,要么全部执行且执行过程不被中断,要么全不执行。
例如count++可拆分为3步:

  1. 读取count的当前值;
  2. 对值进行+1运算;
  3. 将结果写回count
    多线程并发时,步骤可能被穿插执行,导致结果错误。

解决原子性问题:可使用java.util.concurrent.atomic包下的原子类(如AtomicInteger),其内部通过CAS(Compare-And-Swap)机制保证原子操作:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count.incrementAndGet(); // 原子自增
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count.incrementAndGet();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("最终结果:" + count); // 稳定输出20000
    }
}
d. 指令重排序

编译器或CPU为优化性能,可能会调整指令的执行顺序。单线程下不影响结果,但多线程下可能导致逻辑错误。
例如:

// 线程1
int a = 1; // 步骤1
boolean flag = true; // 步骤2

// 线程2
if (flag) {
    System.out.println(a); // 可能输出0(因步骤1和步骤2被重排序)
}
e. 可见性问题

CPU缓存导致线程对共享变量的修改,其他线程可能无法立即感知(即"不可见")。
例如:

public class VisibilityDemo {
    private static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!flag) {
                // 循环等待flag变为true
            }
            System.out.println("线程退出");
        });
        t.start();
        Thread.sleep(1000);
        flag = true; // 主线程修改flag
    }
}

现象:线程可能永远不退出,因为flag的修改未被线程t感知(线程t一直读取缓存中的旧值)。

4. 解决方法

(1)synchronized关键字

synchronized是Java中最常用的同步机制,核心特性:

  • 互斥性:同一时间只有一个线程能执行synchronized修饰的代码块(或方法),其他线程需等待锁释放。
  • 可重入性:同一线程可多次获取同一把锁(避免自身死锁)。

使用方式

// 1. 修饰代码块(锁对象自定义)
synchronized (lockObject) {
    // 临界区代码
}

// 2. 修饰实例方法(锁对象为this)
public synchronized void method() {
    // 临界区代码
}

// 3. 修饰静态方法(锁对象为类对象,如ClassName.class)
public static synchronized void staticMethod() {
    // 临界区代码
}

用synchronized解决线程不安全问题

public class SynchronizedDemo {
    private static int count = 0;
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (lock) { // 加锁保证原子性
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (lock) {
                    count++;
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("最终结果:" + count); // 稳定输出20000
    }
}
(2)volatile关键字

volatile主要解决可见性禁止指令重排序问题,但不保证原子性

  • 可见性:一个线程对volatile变量的修改,会立即被其他线程感知(强制刷新CPU缓存)。
  • 禁止重排序volatile变量的读写操作前后的指令不会被重排序。

用volatile解决可见性问题

public class VolatileDemo {
    private static volatile boolean flag = false; // 用volatile修饰

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!flag) {
                // 循环等待
            }
            System.out.println("线程退出"); // 能正常退出
        });
        t.start();
        Thread.sleep(1000);
        flag = true; // 修改后立即被线程t感知
    }
}

注意volatilesynchronized的区别:

  • synchronized保证原子性、可见性、有序性(通过互斥)。
  • volatile仅保证可见性和有序性,不保证原子性。

5. Java标准库中的线程安全类

Java标准库中部分类天生线程安全,主要分为两类:

  1. 通过加锁实现线程安全

    • ConcurrentHashMap:高效的并发哈希表(重点),采用分段锁机制,支持高并发读写。
    • Hashtable:全表加锁,并发效率低(已被ConcurrentHashMap替代)。
    • VectorStack等(加锁的集合类,效率较低)。
  2. 不可变对象(无修改操作)

    • String:字符串一旦创建不可修改,多线程读取安全。
    • IntegerLong等包装类(值不可变)。

总结

本文介绍了多线程的核心基础知识:

  • 线程状态中BLOCKEDWAITINGTIMED_WAITING的区别。
  • 线程安全的本质是多线程对共享数据的并发修改,根源在于线程调度的随机性,辅以原子性、可见性、有序性问题。
  • 解决线程安全的关键手段:synchronized(保证原子性、可见性、有序性)和volatile(保证可见性、有序性)。
  • 标准库中线程安全类的特点及使用场景。

多线程编程的核心是"控制并发",理解线程安全的本质和解决方案,是编写可靠并发程序的基础。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐