10道考察Java线程基础知识的高频编程题

90 阅读9分钟

以下是10道考察Java线程基础知识的高频编程题,涵盖线程创建、同步机制、线程通信、线程安全等核心考点,附解题代码和解析:

1. 线程的两种创建方式及区别

题目:分别使用“继承Thread类”和“实现Runnable接口”创建线程,打印当前线程名称,并说明两种方式的核心区别。

答案

// 方式1:继承Thread类
class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行逻辑
        System.out.println("Thread方式:" + Thread.currentThread().getName());
    }
}

// 方式2:实现Runnable接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行逻辑
        System.out.println("Runnable方式:" + Thread.currentThread().getName());
    }
}

public class ThreadCreation {
    public static void main(String[] args) {
        // 启动Thread子类线程
        new MyThread().start();
        
        // 启动Runnable实现类线程(需传入Thread)
        new Thread(new MyRunnable()).start();
    }
}

解析

  • 核心区别:
    1. 继承Thread:单继承限制(Java类只能单继承),线程逻辑与线程对象绑定。
    2. 实现Runnable:无继承限制,可多线程共享Runnable实例的资源(如共享变量),更灵活。

2. 使用synchronized解决线程安全问题

题目:多个线程同时对一个计数器进行累加操作,会出现线程安全问题。请用synchronized关键字修复以下代码,确保计数正确。

// 问题代码(线程不安全)
class Counter {
    private int count = 0;
    public void increment() { count++; } // 非原子操作,多线程下会出错
    public int getCount() { return count; }
}

答案

class SafeCounter {
    private int count = 0;
    
    // 方法级同步:锁对象为this
    public synchronized void increment() {
        count++; // 现在是原子操作
    }
    
    // 或使用同步代码块(更灵活控制锁粒度)
    /*
    public void increment() {
        synchronized (this) {
            count++;
        }
    }
    */
    
    public synchronized int getCount() { // 读取也需同步,保证可见性
        return count;
    }
}

// 测试类
public class SynchronizedDemo {
    public static void main(String[] args) throws InterruptedException {
        SafeCounter counter = new SafeCounter();
        int threadNum = 10;
        Thread[] threads = new Thread[threadNum];
        
        // 启动10个线程,每个线程累加1000次
        for (int i = 0; i < threadNum; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }
        
        // 等待所有线程完成
        for (Thread t : threads) {
            t.join();
        }
        
        System.out.println("最终计数:" + counter.getCount()); // 正确输出10000
    }
}

解析

  • count++包含“读-改-写”三步操作,多线程并发时会出现竞态条件。
  • synchronized保证同一时间只有一个线程执行同步代码,确保操作原子性和可见性。

3. volatile关键字的可见性验证

题目:验证volatile关键字的可见性(一个线程修改变量后,其他线程能立即看到最新值)。编写代码展示“无volatile时的不可见问题”和“有volatile时的可见性”。

答案

public class VolatileDemo {
    // 测试1:无volatile,可能出现死循环(不可见)
    static class NoVolatileTest {
        private boolean flag = false; // 无volatile
        
        public void start() {
            new Thread(() -> {
                System.out.println("线程1:开始执行");
                while (!flag) { 
                    // 若flag无volatile,线程可能一直读取缓存,不会退出循环
                }
                System.out.println("线程1:退出循环");
            }).start();
            
            try {
                Thread.sleep(1000); // 确保线程1先启动
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            new Thread(() -> {
                System.out.println("线程2:修改flag为true");
                flag = true; // 修改后,线程1可能看不到
            }).start();
        }
    }
    
    // 测试2:有volatile,保证可见性
    static class WithVolatileTest {
        private volatile boolean flag = false; // 有volatile
        
        public void start() {
            new Thread(() -> {
                System.out.println("线程1:开始执行");
                while (!flag) {} // 能看到线程2的修改,会退出循环
                System.out.println("线程1:退出循环");
            }).start();
            
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            new Thread(() -> {
                System.out.println("线程2:修改flag为true");
                flag = true; // 修改后立即对线程1可见
            }).start();
        }
    }
    
    public static void main(String[] args) {
        System.out.println("测试无volatile:");
        new NoVolatileTest().start(); // 可能卡死(线程1不退出)
        
        // 等待一段时间后测试volatile
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("\n测试有volatile:");
        new WithVolatileTest().start(); // 正常退出
    }
}

解析

  • volatile保证变量的“可见性”:修改后立即刷新到主内存,其他线程读取时从主内存加载,避免缓存不一致。
  • 注意:volatile不保证原子性(如count++仍需同步),仅解决可见性和指令重排序问题。

4. 线程中断(interrupt)的正确处理

题目:编写一个线程,使其在收到中断信号后优雅退出(而非强制终止)。

答案

public class ThreadInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Thread taskThread = new Thread(() -> {
            try {
                // 线程执行逻辑:循环休眠1秒
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("线程运行中...");
                    Thread.sleep(1000); // 休眠时若被中断,会抛出InterruptedException
                }
            } catch (InterruptedException e) {
                // 捕获中断异常后,需手动恢复中断状态(因为sleep会清除中断标记)
                Thread.currentThread().interrupt();
                System.out.println("捕获中断异常,准备退出");
            }
            // 退出前的清理工作
            System.out.println("线程已优雅退出");
        });
        
        taskThread.start();
        Thread.sleep(3000); // 主线程等待3秒
        System.out.println("主线程:发送中断信号");
        taskThread.interrupt(); // 发送中断
    }
}

解析

  • 线程中断是“协作式”的:通过interrupt()设置中断标记,线程需主动检查isInterrupted()或响应InterruptedException
  • sleep()wait()等方法在中断时会抛出InterruptedException,并清除中断标记,需在异常处理中手动恢复(interrupt())。

5. 使用wait/notify实现线程间通信

题目:两个线程交替打印数字(线程A打印1,线程B打印2,线程A打印3,线程B打印4... 直到10)。

答案

public class WaitNotifyDemo {
    private static int count = 1;
    private static final Object lock = new Object(); // 共享锁对象
    
    public static void main(String[] args) {
        // 线程A:打印奇数
        Thread threadA = new Thread(() -> {
            while (count <= 10) {
                synchronized (lock) {
                    // 若当前是偶数,等待线程B打印
                    if (count % 2 == 0) {
                        try {
                            lock.wait(); // 释放锁,进入等待状态
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 打印奇数
                    System.out.println("线程A:" + count);
                    count++;
                    lock.notify(); // 唤醒线程B
                }
            }
        });
        
        // 线程B:打印偶数
        Thread threadB = new Thread(() -> {
            while (count <= 10) {
                synchronized (lock) {
                    // 若当前是奇数,等待线程A打印
                    if (count % 2 != 0) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 打印偶数
                    System.out.println("线程B:" + count);
                    count++;
                    lock.notify(); // 唤醒线程A
                }
            }
        });
        
        threadA.start();
        threadB.start();
    }
}

解析

  • wait():释放锁并让线程进入等待状态,必须在synchronized块中调用(持有锁时)。
  • notify():唤醒一个等待该锁的线程,notifyAll()唤醒所有等待线程。
  • 核心逻辑:通过共享变量count判断当前应由哪个线程执行,完成后唤醒对方。

6. 使用join()等待线程完成

题目:主线程启动3个子线程,每个子线程打印1-5的数字,主线程需在所有子线程完成后打印“所有任务完成”。

答案

public class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> printNumbers("线程1"));
        Thread t2 = new Thread(() -> printNumbers("线程2"));
        Thread t3 = new Thread(() -> printNumbers("线程3"));
        
        t1.start();
        t2.start();
        t3.start();
        
        // 主线程等待t1、t2、t3完成
        t1.join(); // 阻塞主线程,直到t1执行完毕
        t2.join();
        t3.join();
        
        System.out.println("所有任务完成");
    }
    
    // 子线程执行的方法:打印1-5
    private static void printNumbers(String threadName) {
        for (int i = 1; i <= 5; i++) {
            System.out.println(threadName + ":" + i);
            try {
                Thread.sleep(100); // 模拟耗时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

解析

  • join():让当前线程(此处为主线程)等待调用线程(如t1)执行完毕后再继续。
  • 若需设置超时,可使用join(long millis),超时后不再等待。

7. 线程池的基本使用(ExecutorService)

题目:使用线程池执行10个任务(每个任务打印当前线程名和任务编号),并正确关闭线程池。

答案

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

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建固定大小为3的线程池
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // 提交10个任务
        for (int i = 0; i < 10; i++) {
            int taskId = i; // 避免lambda中变量捕获问题
            executor.submit(() -> {
                System.out.println("线程:" + Thread.currentThread().getName() + ",执行任务:" + taskId);
                try {
                    Thread.sleep(500); // 模拟任务耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        
        // 关闭线程池:先停止接收新任务,再等待已提交任务完成
        executor.shutdown();
    }
}

解析

  • 线程池优势:复用线程、控制并发数、管理线程生命周期。
  • shutdown():平缓关闭,允许已提交任务执行完毕;shutdownNow():立即关闭,尝试中断正在执行的任务。

8. ThreadLocal的使用与内存泄漏避免

题目:使用ThreadLocal为每个线程存储独立的SimpleDateFormat实例(解决多线程安全问题),并演示如何避免内存泄漏。

答案

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

public class ThreadLocalDemo {
    // 创建ThreadLocal,每个线程有独立的SimpleDateFormat
    private static ThreadLocal<SimpleDateFormat> sdfThreadLocal = ThreadLocal.withInitial(
        () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    );
    
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // 10个任务使用ThreadLocal中的SimpleDateFormat
        for (int i = 0; i < 10; i++) {
            executor.submit(() -> {
                try {
                    // 获取当前线程的SimpleDateFormat
                    SimpleDateFormat sdf = sdfThreadLocal.get();
                    String time = sdf.format(System.currentTimeMillis());
                    System.out.println(Thread.currentThread().getName() + ":" + time);
                } finally {
                    // 移除ThreadLocal中的值,避免线程池复用导致的内存泄漏
                    sdfThreadLocal.remove();
                }
            });
        }
        
        executor.shutdown();
    }
}

解析

  • ThreadLocal为每个线程提供独立变量副本,解决多线程共享资源的线程安全问题(如SimpleDateFormat非线程安全)。
  • 内存泄漏风险:线程池中的线程长期存活,ThreadLocal变量若不手动移除,会导致关联的对象无法被GC回收。需在finally中调用remove()

9. 死锁的产生与避免

题目:编写代码演示死锁(两个线程互相持有对方需要的锁),并修改代码避免死锁。

答案

public class DeadlockDemo {
    // 两个共享锁对象
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();
    
    // 死锁演示
    static class DeadlockTest {
        public static void start() {
            // 线程1:先锁A,再尝试锁B
            new Thread(() -> {
                synchronized (lockA) {
                    System.out.println("线程1:持有lockA,等待lockB");
                    try { Thread.sleep(100); } catch (InterruptedException e) {}
                    synchronized (lockB) {
                        System.out.println("线程1:获取lockB");
                    }
                }
            }).start();
            
            // 线程2:先锁B,再尝试锁A → 死锁
            new Thread(() -> {
                synchronized (lockB) {
                    System.out.println("线程2:持有lockB,等待lockA");
                    try { Thread.sleep(100); } catch (InterruptedException e) {}
                    synchronized (lockA) {
                        System.out.println("线程2:获取lockA");
                    }
                }
            }).start();
        }
    }
    
    // 避免死锁:固定锁的获取顺序
    static class NoDeadlockTest {
        public static void start() {
            // 线程1和线程2都按"先lockA,再lockB"的顺序获取锁
            new Thread(() -> {
                synchronized (lockA) {
                    System.out.println("线程1:持有lockA,等待lockB");
                    try { Thread.sleep(100); } catch (InterruptedException e) {}
                    synchronized (lockB) {
                        System.out.println("线程1:获取lockB");
                    }
                }
            }).start();
            
            new Thread(() -> {
                synchronized (lockA) { // 改为先获取lockA
                    System.out.println("线程2:持有lockA,等待lockB");
                    try { Thread.sleep(100); } catch (InterruptedException e) {}
                    synchronized (lockB) {
                        System.out.println("线程2:获取lockB");
                    }
                }
            }).start();
        }
    }
    
    public static void main(String[] args) {
        System.out.println("演示死锁:");
        DeadlockTest.start(); // 会卡死(死锁)
        
        // 等待一段时间后测试无死锁版本
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("\n避免死锁:");
        NoDeadlockTest.start(); // 正常执行
    }
}

解析

  • 死锁产生条件:互斥、持有并等待、不可剥夺、循环等待。
  • 避免方法:固定锁的获取顺序(打破循环等待)、使用tryLock设置超时、减少锁持有时间。

10. 原子类(AtomicInteger)的使用

题目:使用AtomicInteger实现一个线程安全的计数器,替代synchronized同步,提高并发性能。

答案

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo {
    // 原子类计数器(线程安全,无锁机制)
    private static AtomicInteger atomicCount = new AtomicInteger(0);
    
    public static void main(String[] args) throws InterruptedException {
        int threadNum = 10;
        Thread[] threads = new Thread[threadNum];
        
        // 启动10个线程,每个累加1000次
        for (int i = 0; i < threadNum; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicCount.incrementAndGet(); // 原子性累加(替代count++)
                }
            });
            threads[i].start();
        }
        
        // 等待所有线程完成
        for (Thread t : threads) {
            t.join();
        }
        
        System.out.println("最终计数:" + atomicCount.get()); // 正确输出10000
    }
}

解析

  • 原子类(java.util.concurrent.atomic)基于CAS(Compare-And-Swap)机制实现线程安全,无需加锁,性能优于synchronized(适用于简单计数器等场景)。
  • 常用方法:incrementAndGet()(自增并返回新值)、getAndIncrement()(返回旧值并自增)、set()get()等。

总结

以上题目覆盖线程编程核心基础:

  • 线程创建与启动(Thread/Runnable);
  • 同步机制(synchronized、volatile、原子类);
  • 线程通信(wait/notify、join、中断);
  • 线程池与ThreadLocal;
  • 死锁与线程安全问题。

掌握这些知识点,能应对大部分线程基础笔试场景,关键在于理解“线程安全的本质是解决共享资源的并发访问问题”,并根据场景选择合适的同步工具。