在当今多核处理器盛行的时代,多线程编程已成为 Java 开发中不可或缺的技能。它能充分利用硬件资源,提升程序的执行效率和响应速度。对于春招面试而言,多线程与并发知识是重点考察的领域,深入理解这些知识不仅有助于应对面试,更能为实际开发打下坚实的基础。
一、线程生命周期
线程的生命周期包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五个状态。
- 新建状态:当使用new关键字创建一个线程对象时,线程处于新建状态,此时线程还未开始执行。例如Thread thread = new Thread(() -> System.out.println("Hello, Thread!"));。
- 就绪状态:调用start()方法后,线程进入就绪状态,此时线程已经具备了运行的条件,但还没有被分配到 CPU 时间片,需要等待 CPU 调度。
- 运行状态:当线程获得 CPU 时间片后,进入运行状态,开始执行run()方法中的代码。
- 阻塞状态:在运行过程中,线程可能会因为某些原因进入阻塞状态,如调用wait()方法、sleep()方法,或者等待获取锁等。处于阻塞状态的线程不会被 CPU 调度,直到满足特定条件后才会重新进入就绪状态。
- 死亡状态:当线程的run()方法执行完毕,或者因异常退出,线程进入死亡状态,此时线程生命周期结束。
面试题 1:线程在什么情况下会进入阻塞状态?
答案:线程进入阻塞状态主要有以下几种情况:
- 调用Thread.sleep(long millis)方法,使线程睡眠指定的时间,在睡眠期间线程进入阻塞状态。
- 调用对象的wait()方法,线程会释放持有的锁,并进入该对象的等待队列,等待其他线程调用notify()或notifyAll()方法唤醒。
- 线程在获取锁时,如果锁被其他线程占用,线程会进入阻塞状态,等待获取锁。
- 调用join()方法,当前线程会等待被调用join()方法的线程执行完毕,在此期间当前线程进入阻塞状态。
二、同步机制
在多线程环境下,为了避免线程安全问题,需要使用同步机制来保证共享资源的正确访问。Java 提供了多种同步机制,如synchronized关键字和Lock接口。
- synchronized 关键字:可以修饰方法或代码块。修饰方法时,该方法成为同步方法,同一时刻只能有一个线程执行该方法;修饰代码块时,只有获取到该代码块关联的对象锁的线程才能执行代码块中的内容。例如:
java
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public void incrementBlock() {
synchronized (this) {
count++;
}
}
}
- Lock 接口:java.util.concurrent.locks包下的Lock接口提供了更灵活的锁机制,如可中断的锁获取、公平锁与非公平锁等。常用的实现类有ReentrantLock。例如:
java
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
面试题 2:synchronized 关键字和 Lock 接口有什么区别?
答案:
- 语法结构:synchronized是 Java 关键字,在编译时由编译器处理;Lock是一个接口,通过调用接口方法来实现锁的功能。
- 锁获取方式:synchronized获取锁是隐式的,在进入同步代码块或方法时自动获取,退出时自动释放;Lock需要手动调用lock()方法获取锁,调用unlock()方法释放锁,通常放在finally块中以确保锁一定会被释放。
- 锁的特性:synchronized提供的是不可中断的锁获取方式,一旦获取不到锁,线程会一直等待;Lock可以通过tryLock()方法尝试获取锁,获取不到时可以返回false,还可以通过lockInterruptibly()方法获取可中断的锁,在等待锁的过程中可以响应中断。
- 锁的类型:synchronized只有一种非公平锁;Lock可以通过构造函数选择公平锁或非公平锁,ReentrantLock默认是非公平锁,创建时传入true可变为公平锁。
三、并发包
Java 的java.util.concurrent包(简称并发包)提供了丰富的并发工具类,如线程池、并发集合、同步器等。
- 线程池:通过线程池可以复用线程,减少线程创建和销毁的开销,提高系统性能。常用的线程池有ThreadPoolExecutor,可以通过Executors工具类创建不同类型的线程池,如FixedThreadPool(固定大小线程池)、CachedThreadPool(缓存线程池)、SingleThreadExecutor(单线程线程池)等。例如:
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is running");
});
}
executorService.shutdown();
}
}
- 并发集合:并发包提供了多种线程安全的集合类,如ConcurrentHashMap(前面已介绍)、ConcurrentLinkedQueue(基于链表的无界线程安全队列)、CopyOnWriteArrayList(写时复制的线程安全ArrayList)等。这些集合类在多线程环境下具有更好的性能和线程安全性。
- 同步器:CountDownLatch、CyclicBarrier和Semaphore是常用的同步器。CountDownLatch用于让一个或多个线程等待其他线程完成一组操作后再继续执行;CyclicBarrier用于让一组线程相互等待,直到所有线程都到达某个屏障点后再继续执行;Semaphore用于控制同时访问某个资源的线程数量。例如:
java
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("Thread 1 is done");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}).start();
new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("Thread 2 is done");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}).start();
new Thread(() -> {
try {
Thread.sleep(3000);
System.out.println("Thread 3 is done");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}).start();
latch.await();
System.out.println("All threads are done, main thread can continue");
}
}
面试题 3:线程池的原理和优势是什么?
答案:
- 原理:线程池内部维护了一个线程队列和一个任务队列。当提交任务时,线程池会从线程队列中获取空闲线程来执行任务,如果线程队列中没有空闲线程,且任务队列未满,则将任务加入任务队列等待执行;如果任务队列也满了,根据线程池的拒绝策略处理新提交的任务。
- 优势:
-
- 减少线程创建和销毁开销:线程的创建和销毁需要消耗系统资源,线程池可以复用已创建的线程,避免频繁创建和销毁线程带来的开销。
-
- 控制并发线程数量:通过设置线程池的最大线程数和核心线程数,可以控制并发执行的线程数量,防止因线程过多导致系统资源耗尽。
-
- 提高系统响应速度:由于线程池中有空闲线程可以立即执行任务,相比每次创建新线程执行任务,能更快地响应任务请求。
掌握 Java 多线程与并发知识,是在春招面试中脱颖而出的关键。下一篇,我们将探索 Java IO 与 NIO 的奥秘,继续为你的面试之路助力。