跳转至

Java 并发面试问题全解析

简介

在 Java 开发领域,并发编程是一个至关重要且颇具挑战性的主题。理解 Java 并发相关的概念、掌握其使用方法以及熟悉常见和最佳实践,不仅有助于编写出高效、可靠的多线程程序,也是在面试中脱颖而出的关键。本文将围绕 Java 并发面试问题展开深入探讨,帮助读者全面掌握这一技术领域的要点。

目录

  1. 基础概念
    • 线程与进程
    • 并发与并行
    • 线程安全
  2. 使用方法
    • 创建线程的方式
    • 线程的生命周期与状态转换
    • 线程同步机制
  3. 常见实践
    • 生产者 - 消费者模型
    • 线程池的使用
  4. 最佳实践
    • 避免死锁
    • 性能优化
  5. 小结
  6. 参考资料

基础概念

线程与进程

进程是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间和系统资源。

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

并发与并行

并发是指在同一时间段内,多个任务都在执行,但不一定是同时执行。操作系统通过快速切换线程,让用户感觉多个任务在同时进行。

并行则是指在同一时刻,多个任务真正地同时执行,这需要多核 CPU 的支持。

线程安全

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的。

使用方法

创建线程的方式

  1. 继承 Thread 类
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running.");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
  1. 实现 Runnable 接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable is running.");
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}
  1. 实现 Callable 接口(有返回值)
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Callable result";
    }
}

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        String result = futureTask.get();
        System.out.println(result);
    }
}

线程的生命周期与状态转换

线程有以下几种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、等待(Waiting)、计时等待(Timed Waiting)和终止(Terminated)。状态转换图展示了线程在不同操作下如何在这些状态之间转换。

线程同步机制

  1. synchronized 关键字
class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}
  1. ReentrantLock
import java.util.concurrent.locks.ReentrantLock;

class Counter {
    private int count = 0;
    private ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

常见实践

生产者 - 消费者模型

import java.util.LinkedList;
import java.util.Queue;

class Producer implements Runnable {
    private final Queue<Integer> queue;
    private final int capacity;

    public Producer(Queue<Integer> queue, int capacity) {
        this.queue = queue;
        this.capacity = capacity;
    }

    @Override
    public void run() {
        int value = 0;
        while (true) {
            synchronized (queue) {
                while (queue.size() == capacity) {
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                queue.add(value++);
                queue.notify();
            }
        }
    }
}

class Consumer implements Runnable {
    private final Queue<Integer> queue;

    public Consumer(Queue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (queue) {
                while (queue.isEmpty()) {
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                int value = queue.poll();
                System.out.println("Consumed: " + value);
                queue.notify();
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedList<>();
        int capacity = 5;

        Thread producerThread = new Thread(new Producer(queue, capacity));
        Thread consumerThread = new Thread(new Consumer(queue));

        producerThread.start();
        consumerThread.start();
    }
}

线程池的使用

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

class Task implements Runnable {
    private final int taskId;

    public Task(int taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {
        System.out.println("Task " + taskId + " is running.");
    }
}

public class Main {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 5; i++) {
            Task task = new Task(i);
            executorService.submit(task);
        }

        executorService.shutdown();
    }
}

最佳实践

避免死锁

  • 确保线程获取锁的顺序一致。
  • 尽量减少锁的持有时间。
  • 使用定时锁(如 ReentrantLock 的 tryLock 方法),避免无限期等待。

性能优化

  • 减少不必要的同步,只在共享资源访问处进行同步。
  • 使用线程局部变量(ThreadLocal),减少线程间的竞争。
  • 合理使用并发集合类(如 ConcurrentHashMap),提高并发性能。

小结

本文全面探讨了 Java 并发面试中常见的问题,从基础概念到使用方法,再到常见实践和最佳实践。通过理解和掌握这些内容,读者能够更好地应对面试挑战,并在实际开发中编写出高质量的并发程序。

参考资料

  • 《Effective Java》
  • 《Java Concurrency in Practice》
  • Oracle Java 官方文档