十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java多线程编程:从基础到实战

Java多线程编程:从基础到实战 1. Java多线程基础概念解析第一次接触Java多线程时很多人会被各种术语和概念绕晕。其实理解多线程并不复杂我们可以从生活中的例子入手。想象你正在一家快餐店点餐收银员负责接单主线程后厨有多个厨师同时制作不同菜品工作线程而传菜员则负责把做好的食物送到顾客手中线程间通信。这种分工协作的模式就是多线程在现实中的完美体现。在Java中每个程序启动时都会创建一个主线程main线程就像快餐店必须至少有一个收银员。但要想提高效率我们必须学会创建和管理更多的工作线程。与单线程相比多线程程序有三个显著特点并发执行多个线程可以同时运行实际是CPU快速切换资源共享所有线程共享进程的内存空间需要同步当多个线程访问共享资源时必须协调顺序特别注意虽然多核CPU可以真正并行执行线程但在单核CPU上多线程是通过时间片轮转实现的伪并行。这种区别对编程逻辑没有影响但会影响性能优化策略。2. 线程创建与启动的三种方式2.1 继承Thread类这是最直观的创建方式适合简单的线程任务。假设我们要模拟一个下载任务class DownloadThread extends Thread { private String url; public DownloadThread(String url) { this.url url; } Override public void run() { System.out.println(开始下载 url); // 模拟下载耗时 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(url 下载完成); } } // 使用示例 public static void main(String[] args) { new DownloadThread(https://example.com/file1.zip).start(); new DownloadThread(https://example.com/file2.zip).start(); }这种方式虽然简单但有个明显缺点Java是单继承的继承了Thread就不能继承其他类。在实际项目中更推荐下面这种实现方式。2.2 实现Runnable接口class DownloadTask implements Runnable { private String url; public DownloadTask(String url) { this.url url; } Override public void run() { System.out.println(开始下载 url); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(url 下载完成); } } // 使用示例 public static void main(String[] args) { new Thread(new DownloadTask(https://example.com/file1.zip)).start(); new Thread(new DownloadTask(https://example.com/file2.zip)).start(); }这种方式更灵活因为实现接口后还可以继承其他类。同时它也更符合面向对象的设计原则——线程执行的是一个任务而不是线程本身。2.3 使用Callable和Future带返回值当需要获取线程执行结果时前两种方式就力不从心了。Java 5.0引入了Callable接口class DownloadCallable implements CallableString { private String url; public DownloadCallable(String url) { this.url url; } Override public String call() throws Exception { System.out.println(开始下载 url); Thread.sleep(2000); return url 下载完成文件大小3.5MB; } } // 使用示例 public static void main(String[] args) throws Exception { ExecutorService executor Executors.newSingleThreadExecutor(); FutureString future executor.submit(new DownloadCallable(https://example.com/file1.zip)); System.out.println(主线程可以做其他事情...); // 获取异步执行结果会阻塞直到任务完成 String result future.get(); System.out.println(result); executor.shutdown(); }经验之谈在实际项目中推荐使用线程池(ExecutorService)来管理线程而不是直接new Thread()。这样可以避免频繁创建销毁线程的开销也能更好地控制并发数量。3. 线程生命周期与状态转换理解线程的各种状态对调试多线程程序至关重要。Java线程的生命周期包括以下状态NEW新建线程被创建但尚未启动RUNNABLE可运行在JVM中执行可能正在运行或等待CPU时间片BLOCKED阻塞等待获取监视器锁synchronizedWAITING等待无限期等待其他线程执行特定操作TIMED_WAITING定时等待有限时间的等待TERMINATED终止线程执行完毕状态转换图示NEW ↓ start() ↓ RUNNABLE ←──┐ │ ↑ │ │ │ │ blocked | │ │ │ │ ↓ │ │ BLOCKED │ │ │ ↓ │ WAITING ←───┤ │ │ ↓ │ TIMED_WAITING │ │ │ ↓ │ TERMINATED │ ↑ │ └─────────┘通过代码可以查看线程状态Thread thread new Thread(() - { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(thread.getState()); // NEW thread.start(); System.out.println(thread.getState()); // RUNNABLE Thread.sleep(500); System.out.println(thread.getState()); // TIMED_WAITING Thread.sleep(1000); System.out.println(thread.getState()); // TERMINATED4. 线程同步与锁机制当多个线程访问共享资源时就会出现竞态条件Race Condition。比如下面的银行账户操作class BankAccount { private int balance 1000; public void withdraw(int amount) { if (balance amount) { try { Thread.sleep(10); // 模拟处理时间 } catch (InterruptedException e) { e.printStackTrace(); } balance - amount; System.out.println(取款成功余额 balance); } else { System.out.println(余额不足); } } } // 测试代码 public static void main(String[] args) { BankAccount account new BankAccount(); // 两个线程同时取款 new Thread(() - account.withdraw(800)).start(); new Thread(() - account.withdraw(800)).start(); }运行这段代码你可能会看到两个线程都成功取出了800元导致最终余额变成-600这显然是不合理的。这就是典型的线程安全问题。4.1 synchronized关键字Java中最基本的同步机制是synchronized关键字它可以修饰方法或代码块// 同步方法 public synchronized void withdraw(int amount) { // 方法体不变 } // 或者同步代码块 public void withdraw(int amount) { synchronized(this) { // 方法体不变 } }synchronized保证了同一时刻只有一个线程能执行该方法或代码块。它的实现原理是基于对象内部的一个称为监视器锁(monitor)的机制。4.2 ReentrantLockJava 5.0引入了更灵活的ReentrantLockclass BankAccount { private final Lock lock new ReentrantLock(); private int balance 1000; public void withdraw(int amount) { lock.lock(); try { if (balance amount) { Thread.sleep(10); balance - amount; System.out.println(取款成功余额 balance); } else { System.out.println(余额不足); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }相比synchronizedReentrantLock提供了更多功能可中断的锁获取超时获取锁公平锁多个条件变量避坑指南使用ReentrantLock时必须将unlock()放在finally块中否则如果同步代码抛出异常锁可能永远不会释放导致死锁。5. 线程间通信多个线程协作完成任务时经常需要相互通知和等待。Java提供了wait()、notify()和notifyAll()方法来实现这一点。5.1 生产者-消费者模式这是一个经典的多线程协作模型class MessageQueue { private QueueString queue new LinkedList(); private int capacity; public MessageQueue(int capacity) { this.capacity capacity; } public synchronized void put(String message) throws InterruptedException { while (queue.size() capacity) { wait(); // 队列满时等待 } queue.add(message); notifyAll(); // 通知消费者有新消息 } public synchronized String take() throws InterruptedException { while (queue.isEmpty()) { wait(); // 队列空时等待 } String message queue.remove(); notifyAll(); // 通知生产者有空位 return message; } } // 生产者线程 class Producer implements Runnable { private MessageQueue queue; public Producer(MessageQueue queue) { this.queue queue; } Override public void run() { try { for (int i 0; i 10; i) { String msg 消息- i; queue.put(msg); System.out.println(生产 msg); Thread.sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); } } } // 消费者线程 class Consumer implements Runnable { private MessageQueue queue; public Consumer(MessageQueue queue) { this.queue queue; } Override public void run() { try { for (int i 0; i 10; i) { String msg queue.take(); System.out.println(消费 msg); Thread.sleep(200); } } catch (InterruptedException e) { e.printStackTrace(); } } } // 测试代码 public static void main(String[] args) { MessageQueue queue new MessageQueue(3); new Thread(new Producer(queue)).start(); new Thread(new Consumer(queue)).start(); }5.2 Condition对象使用ReentrantLock时可以通过Condition实现更精细的线程通信class MessageQueue { private final Lock lock new ReentrantLock(); private final Condition notFull lock.newCondition(); private final Condition notEmpty lock.newCondition(); // 其他代码不变... public void put(String message) throws InterruptedException { lock.lock(); try { while (queue.size() capacity) { notFull.await(); } queue.add(message); notEmpty.signalAll(); } finally { lock.unlock(); } } public String take() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); } String message queue.remove(); notFull.signalAll(); return message; } finally { lock.unlock(); } } }Condition的await()、signal()和signalAll()分别对应Object的wait()、notify()和notifyAll()但可以创建多个Condition实现更精确的线程唤醒。6. 线程池最佳实践直接创建线程有两个主要问题创建和销毁线程开销大无限制创建线程可能导致系统资源耗尽Java通过Executor框架提供了线程池解决方案6.1 四种常用线程池// 1. 固定大小线程池 ExecutorService fixedPool Executors.newFixedThreadPool(5); // 2. 单线程池保证任务顺序执行 ExecutorService singleThread Executors.newSingleThreadExecutor(); // 3. 可缓存线程池自动扩容 ExecutorService cachedPool Executors.newCachedThreadPool(); // 4. 定时任务线程池 ScheduledExecutorService scheduledPool Executors.newScheduledThreadPool(3);6.2 自定义线程池对于生产环境建议通过ThreadPoolExecutor构造函数创建线程池这样可以更精细地控制参数ThreadPoolExecutor executor new ThreadPoolExecutor( 5, // 核心线程数 10, // 最大线程数 60, // 空闲线程存活时间(秒) TimeUnit.SECONDS, // 时间单位 new ArrayBlockingQueue(100), // 工作队列 new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 );6.3 线程池参数调优经验核心线程数CPU密集型任务建议设为CPU核心数1IO密集型任务可以设更多工作队列根据任务特点选择ArrayBlockingQueue固定大小有助于防止资源耗尽LinkedBlockingQueue无界队列可能堆积大量任务SynchronousQueue不存储元素每个插入操作必须等待另一个线程的移除操作拒绝策略AbortPolicy默认策略直接抛出RejectedExecutionExceptionCallerRunsPolicy由调用线程执行该任务DiscardPolicy直接丢弃任务DiscardOldestPolicy丢弃队列中最老的任务然后重试实战技巧阿里巴巴Java开发手册建议不要使用Executors创建线程池而是通过ThreadPoolExecutor构造函数创建这样可以更明确线程池的运行规则避免资源耗尽的风险。7. 常见多线程问题与解决方案7.1 死锁Deadlock死锁是指两个或多个线程互相持有对方需要的资源导致所有线程都无法继续执行。典型的四个必要条件互斥条件占有且等待不可抢占循环等待示例代码Object lock1 new Object(); Object lock2 new Object(); new Thread(() - { synchronized (lock1) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println(Thread1 got both locks); } } }).start(); new Thread(() - { synchronized (lock2) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { System.out.println(Thread2 got both locks); } } }).start();解决方案避免嵌套锁按固定顺序获取锁使用tryLock()设置超时7.2 线程饥饿Starvation某些线程因为优先级太低或总是获取不到锁而长期得不到执行。解决方法合理设置线程优先级使用公平锁但会降低吞吐量避免长时间持有锁7.3 活锁Livelock线程没有阻塞但因为不断重试相同的操作而无法继续执行。比如两个线程互相礼让资源。解决方法引入随机性设置最大重试次数7.4 上下文切换开销线程切换需要保存和恢复执行上下文频繁切换会降低性能。优化建议减少锁的持有时间减小锁的粒度使用无锁数据结构如ConcurrentHashMap合理设置线程池大小8. Java内存模型与volatile关键字8.1 Java内存模型(JMM)JMM定义了线程如何与内存交互主要解决以下问题原子性哪些操作是不可分割的可见性一个线程的修改何时对其他线程可见有序性指令重排序的影响8.2 volatile关键字volatile解决了可见性和有序性问题class SharedData { private volatile boolean flag false; public void setFlag() { flag true; } public void doWork() { while (!flag) { // 等待flag变为true } System.out.println(Flag is now true); } }volatile的特点保证变量的修改对所有线程立即可见禁止指令重排序优化不保证原子性比如count这种复合操作使用场景volatile适合作为状态标志如开关控制或者用于发布不可变对象。9. 原子类与CAS操作Java提供了一系列原子类如AtomicInteger来解决复合操作的原子性问题AtomicInteger counter new AtomicInteger(0); // 线程安全的自增 counter.incrementAndGet(); // CAS操作示例 int oldValue, newValue; do { oldValue counter.get(); newValue oldValue 1; } while (!counter.compareAndSet(oldValue, newValue));原子类的实现原理是CASCompare-And-SwapCPU指令它包含三个操作数内存位置(V)期望的原值(A)新值(B)CAS的优点是无需加锁减少了线程切换开销缺点是可能遇到ABA问题可以通过AtomicStampedReference解决。10. ThreadLocal原理与应用ThreadLocal为每个线程提供独立的变量副本class UserContext { private static final ThreadLocalUser currentUser new ThreadLocal(); public static void set(User user) { currentUser.set(user); } public static User get() { return currentUser.get(); } public static void remove() { currentUser.remove(); } } // 使用示例 UserContext.set(user); // 在当前线程设置用户 // ... 其他代码 ... User user UserContext.get(); // 获取当前线程的用户 UserContext.remove(); // 使用后清理ThreadLocal的典型应用场景保存用户会话信息数据库连接管理避免参数传递内存泄漏警告ThreadLocal使用不当可能导致内存泄漏。线程池中的线程会复用如果不调用remove()之前设置的值可能会一直存在。最佳实践是总是在try-finally块中使用ThreadLocaltry { UserContext.set(user); // 执行业务逻辑 } finally { UserContext.remove(); }
返回列表