1. 进程与线程的基本概念

1.1 什么是进程?

进程(Process)是操作系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间、系统资源和执行环境。

进程的特点:

  • 独立性:进程之间相互隔离,一个进程崩溃不会影响其他进程
  • 资源开销大:创建、销毁和切换进程需要较大的系统开销
  • 通信复杂:进程间通信(IPC)需要通过管道、消息队列、共享内存等机制

1.2 什么是线程?

线程(Thread)是进程内的一个执行单元,是 CPU 调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和系统资源。

线程的特点:

  • 轻量级:创建、销毁和切换线程的开销远小于进程
  • 资源共享:同一进程内的线程共享内存、文件句柄等资源
  • 通信简单:线程间可以直接通过共享内存进行通信

1.3 进程 vs 线程对比

特性 进程 线程
资源开销
创建速度
通信方式 复杂(IPC) 简单(共享内存)
独立性 强(相互隔离) 弱(共享资源)
崩溃影响 不影响其他进程 可能影响同进程其他线程

2. Java 中的线程创建:继承 Thread 类

2.1 继承 Thread 类的基本方式

在 Java 中,创建线程最简单的方式是继承 Thread 类并重写 run() 方法。

/**
 * 通过继承 Thread 类创建线程
 */
public class MyThread extends Thread {
    private String threadName;
    
    public MyThread(String name) {
        this.threadName = name;
    }
    
    @Override
    public void run() {
        System.out.println("线程 " + threadName + " 开始执行");
        try {
            // 模拟线程执行任务
            for (int i = 1; i <= 5; i++) {
                System.out.println(threadName + ": 执行第 " + i + " 次");
                Thread.sleep(500); // 休眠500毫秒
            }
        } catch (InterruptedException e) {
            System.out.println(threadName + " 被中断");
        }
        System.out.println("线程 " + threadName + " 执行结束");
    }
    
    public static void main(String[] args) {
        // 创建线程实例
        MyThread thread1 = new MyThread("线程A");
        MyThread thread2 = new MyThread("线程B");
        
        // 启动线程(注意:不是调用 run() 方法)
        thread1.start();
        thread2.start();
        
        // 等待线程执行完成
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("所有线程执行完毕");
    }
}

2.2 start() 与 run() 的区别

这是一个常见的面试点,需要特别注意:

public class StartVsRunDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("当前线程: " + Thread.currentThread().getName());
        });
        
        // 错误方式:直接调用 run() 方法
        System.out.println("=== 调用 run() 方法 ===");
        thread.run(); // 在主线程中执行
        
        // 正确方式:调用 start() 方法
        System.out.println("\n=== 调用 start() 方法 ===");
        thread.start(); // 创建新线程执行
        
        // 输出结果:
        // === 调用 run() 方法 ===
        // 当前线程: main
        // === 调用 start() 方法 ===
        // 当前线程: Thread-0
    }
}

关键区别:

  • run():只是普通方法调用,不会创建新线程
  • start():会创建新线程,并在新线程中执行 run() 方法

2.3 线程的生命周期

Java 线程有以下几种状态:

public class ThreadStateDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(1000);
                synchronized (ThreadStateDemo.class) {
                    ThreadStateDemo.class.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        System.out.println("创建后状态: " + thread.getState()); // NEW
        
        thread.start();
        System.out.println("启动后状态: " + thread.getState()); // RUNNABLE
        
        Thread.sleep(100);
        System.out.println("休眠中状态: " + thread.getState()); // TIMED_WAITING
        
        Thread.sleep(2000);
        synchronized (ThreadStateDemo.class) {
            ThreadStateDemo.class.notify();
        }
        
        thread.join();
        System.out.println("结束后状态: " + thread.getState()); // TERMINATED
    }
}

3. 线程安全的核心问题

3.1 什么是线程安全问题?

当多个线程同时访问共享资源,且至少有一个线程在修改资源时,如果不采取同步措施,就可能导致数据不一致的问题。

/**
 * 线程不安全示例:银行账户取款
 */
public class UnsafeBankAccount {
    private int balance = 1000; // 初始余额1000元
    
    // 取款方法(线程不安全)
    public void withdraw(int amount) {
        if (balance >= amount) {
            // 模拟耗时操作
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + 
                             " 取款 " + amount + " 元,余额: " + balance);
        } else {
            System.out.println("余额不足");
        }
    }
    
    public static void main(String[] args) {
        UnsafeBankAccount account = new UnsafeBankAccount();
        
        // 创建10个线程同时取款
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                account.withdraw(100);
            }, "线程" + i).start();
        }
    }
}

可能输出(线程不安全):

线程0 取款 100 元,余额: 900
线程1 取款 100 元,余额: 900  // 余额重复,数据不一致!
线程2 取款 100 元,余额: 800
...

3.2 线程安全问题的根源

  1. 原子性问题:一个操作被中途打断
  2. 可见性问题:一个线程的修改对其他线程不可见
  3. 有序性问题:指令重排序导致执行顺序与预期不符

4. 实现线程安全的常用方法

4.1 使用 synchronized 关键字

4.1.1 同步方法
public class SafeBankAccount {
    private int balance = 1000;
    
    // 同步方法
    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + 
                             " 取款 " + amount + " 元,余额: " + balance);
        }
    }
}
4.1.2 同步代码块
public class SafeBankAccount2 {
    private int balance = 1000;
    private final Object lock = new Object(); // 专用锁对象
    
    public void withdraw(int amount) {
        // 同步代码块
        synchronized (lock) {
            if (balance >= amount) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance -= amount;
                System.out.println(Thread.currentThread().getName() + 
                                 " 取款 " + amount + " 元,余额: " + balance);
            }
        }
    }
}

4.2 使用 Lock 接口

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockBankAccount {
    private int balance = 1000;
    private final Lock lock = new ReentrantLock();
    
    public void withdraw(int amount) {
        lock.lock(); // 获取锁
        try {
            if (balance >= amount) {
                Thread.sleep(10);
                balance -= amount;
                System.out.println(Thread.currentThread().getName() + 
                                 " 取款 " + amount + " 元,余额: " + balance);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 必须在finally中释放锁
        }
    }
}

4.3 使用原子类(Atomic Classes)

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);
    
    // 线程安全的自增
    public void increment() {
        count.incrementAndGet();
    }
    
    // 线程安全的获取值
    public int getCount() {
        return count.get();
    }
    
    // CAS(Compare And Swap)操作示例
    public boolean compareAndSet(int expect, int update) {
        return count.compareAndSet(expect, update);
    }
}

4.4 使用 volatile 关键字

public class VolatileExample {
    private volatile boolean flag = false;
    
    public void writer() {
        flag = true; // 写操作
    }
    
    public void reader() {
        if (flag) { // 读操作
            System.out.println("flag is true");
        }
    }
}

volatile 的特点:

  • 保证可见性:一个线程的修改对其他线程立即可见
  • 禁止指令重排序
  • 不保证原子性(不能替代 synchronized)

5. 线程安全的集合类

5.1 ConcurrentHashMap

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentMapDemo {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        
        // 线程安全的putIfAbsent
        map.putIfAbsent("key1", 1);
        
        // 线程安全的compute
        map.compute("key2", (k, v) -> v == null ? 1 : v + 1);
        
        // 遍历(弱一致性)
        map.forEach((k, v) -> System.out.println(k + ": " + v));
    }
}

5.2 CopyOnWriteArrayList

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteDemo {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        
        // 写时复制:修改时创建新数组
        list.add("item1");
        list.add("item2");
        
        // 遍历时使用快照,不会抛出ConcurrentModificationException
        for (String item : list) {
            System.out.println(item);
            list.add("newItem"); // 不会影响当前遍历
        }
    }
}

6. 线程池与线程安全

6.1 使用线程池管理线程

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建固定大小的线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);
        
        // 提交任务
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("任务 " + taskId + " 由线程 " + 
                                 Thread.currentThread().getName() + " 执行");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        
        // 优雅关闭
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
    }
}

6.2 ThreadLocal 线程局部变量

public class ThreadLocalDemo {
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    
    public static void main(String[] args) {
        // 每个线程有自己独立的副本
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                int value = threadLocal.get();
                threadLocal.set(value + 1);
                System.out.println(Thread.currentThread().getName() + 
                                 ": " + threadLocal.get());
                // 使用完后必须清理,防止内存泄漏
                threadLocal.remove();
            }).start();
        }
    }
}

7. 最佳实践与常见陷阱

7.1 线程安全最佳实践

  1. 尽量使用不可变对象

    public final class ImmutablePerson {
        private final String name;
        private final int age;
        
        public ImmutablePerson(String name, int age) {
            this.name = name;
            this.age = age;
        }
        
        // 只有getter,没有setter
        public String getName() { return name; }
        public int getAge() { return age; }
    }
    
  2. 缩小同步范围

    // 不好:同步整个方法
    public synchronized void process() {
        // 前置非同步操作
        // ... 
        // 核心同步操作
        // ...
        // 后置非同步操作
        // ...
    }
    
    // 好:只同步必要部分
    public void process() {
        // 前置非同步操作
        // ...
        synchronized (this) {
            // 核心同步操作
            // ...
        }
        // 后置非同步操作
        // ...
    }
    
  3. 使用线程安全的集合类

  4. 避免死锁

    // 死锁示例
    public class DeadlockDemo {
        private final Object lock1 = new Object();
        private final Object lock2 = new Object();
        
        public void method1() {
            synchronized (lock1) {
                synchronized (lock2) {
                    // 操作
                }
            }
        }
        
        public void method2() {
            synchronized (lock2) {
                synchronized (lock1) {  // 可能死锁!
                    // 操作
                }
            }
        }
    }
    

7.2 常见陷阱

  1. 误用 String 作为锁

    // 错误:字符串常量池可能导致意外锁共享
    synchronized ("lock") {
        // ...
    }
    
    // 正确:使用专用对象
    private final Object lock = new Object();
    synchronized (lock) {
        // ...
    }
    
  2. 忘记在 finally 中释放锁

  3. 过度同步影响性能

  4. 误以为 volatile 能保证原子性

8. 总结

通过本文的学习,你应该掌握:

  1. 进程与线程的区别:进程是资源分配单位,线程是执行单位
  2. Thread 类的使用:继承 Thread 类创建线程,理解 start() 与 run() 的区别
  3. 线程安全的核心:理解原子性、可见性、有序性问题
  4. 同步机制:synchronized、Lock、volatile、原子类的使用场景
  5. 线程安全集合:ConcurrentHashMap、CopyOnWriteArrayList 等
  6. 最佳实践:缩小同步范围、使用不可变对象、避免死锁

在实际开发中,应根据具体场景选择合适的线程安全方案。对于简单的计数器,可以使用原子类;对于复杂的业务逻辑,可能需要结合 synchronized 或 Lock 使用。记住:没有银弹,只有最适合当前场景的解决方案。

开始:多线程编程需求

选择线程创建方式

继承Thread类

实现Runnable接口

实现Callable接口

重写run()方法

启动线程(start())

是否存在共享资源?

直接运行
无需同步

分析线程安全问题

原子性问题?

可见性问题?

有序性问题?

使用原子类/同步

使用volatile/同步

使用volatile/同步

性能要求高?

使用Lock/并发集合

使用synchronized

测试与优化

完成:线程安全程序

希望这篇详细的讲解能帮助你深入理解 Java 中的进程、线程和线程安全。在实际编码中,多思考、多测试,才能写出既正确又高效的多线程程序。

更多推荐