以下是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();
}
}
解析:
- 核心区别:
- 继承Thread:单继承限制(Java类只能单继承),线程逻辑与线程对象绑定。
- 实现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;
- 死锁与线程安全问题。
掌握这些知识点,能应对大部分线程基础笔试场景,关键在于理解“线程安全的本质是解决共享资源的并发访问问题”,并根据场景选择合适的同步工具。