Java多线程编程指南
Java 多线程编程详解
什么是多线程
多线程是 Java 中实现并发编程的核心机制,允许程序同时执行多个任务。在 Java 中,主线程(main 方法)是程序的入口点,我们可以通过创建额外的线程来实现多任务并行处理。
线程的创建方式
Java 提供了三种主要的线程创建方式,每种方式都有其适用场景。
1. 继承 Thread 类
继承 Thread
类并重写其 run()
方法是创建线程的基本方式。
public class ThreadExtendExample {public static void main(String[] args) {// 创建线程对象MyThread myThread = new MyThread();// 启动线程(注意:调用start()而非直接调用run())myThread.start();// 主线程执行的代码for (int i = 0; i < 5; i++) {System.out.println("主线程执行: " + i);try {// 让主线程休眠500毫秒,便于观察线程交替执行Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}}// 自定义线程类,继承Threadstatic class MyThread extends Thread {@Overridepublic void run() {// 线程要执行的任务for (int i = 0; i < 5; i++) {// 获取当前线程名称并输出System.out.println(Thread.currentThread().getName() + " 执行: " + i);try {// 线程休眠500毫秒Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}}}
}
代码说明:
- 继承
Thread
类后必须重写run()
方法,该方法包含线程要执行的任务 - 启动线程必须调用
start()
方法,而非直接调用run()
方法 start()
方法会创建一个新线程并让它执行run()
方法中的代码- 可以通过
Thread.sleep()
方法让线程暂时休眠
2. 实现 Runnable 接口
实现 Runnable
接口是一种更灵活的方式,它避免了单继承的限制,推荐优先使用。
public class RunnableExample {public static void main(String[] args) {// 创建Runnable实现类对象MyRunnable myRunnable = new MyRunnable();// 创建线程对象,并将Runnable对象作为参数传递Thread thread1 = new Thread(myRunnable, "线程A");Thread thread2 = new Thread(myRunnable, "线程B");// 启动线程thread1.start();thread2.start();}// 实现Runnable接口static class MyRunnable implements Runnable {private int count = 0;@Overridepublic void run() {for (int i = 0; i < 5; i++) {count++;System.out.println(Thread.currentThread().getName() + " 计数: " + count + " 当前线程ID: " + Thread.currentThread().getId());try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}}}}
}
代码说明:
Runnable
接口只有一个run()
方法需要实现- 可以将同一个
Runnable
实例传递给多个Thread
对象,实现资源共享 - 可以通过
Thread
构造函数为线程指定名称,便于调试 - 相比继承
Thread
类,这种方式更适合多个线程共享同一资源的场景
也可以使用匿名内部类简化 Runnable
的使用:
public class AnonymousRunnableExample {public static void main(String[] args) {// 使用匿名内部类创建线程Thread thread = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("匿名线程执行中...");}}, "匿名线程");thread.start();}
}
Java 8 及以上版本还可以使用 Lambda 表达式进一步简化:
public class LambdaThreadExample {public static void main(String[] args) {// 使用Lambda表达式创建线程Thread thread = new Thread(() -> {System.out.println("Lambda线程执行中...");}, "Lambda线程");thread.start();}
}
3. 实现 Callable 接口
Callable
接口是 Java 5 引入的,它类似于 Runnable
,但可以返回结果并抛出受检异常。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class CallableExample {public static void main(String[] args) {// 创建Callable实现类对象MyCallable myCallable = new MyCallable();// 创建FutureTask对象,用于接收返回结果FutureTask<Integer> futureTask = new FutureTask<>(myCallable);// 创建线程对象并启动Thread thread = new Thread(futureTask, "计算线程");thread.start();// 主线程可以做其他事情System.out.println("主线程执行其他任务...");try {// 获取线程执行结果,get()方法会阻塞直到结果返回Integer result = futureTask.get();System.out.println("线程计算结果: " + result);} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}// 实现Callable接口,指定返回值类型为Integerstatic class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {System.out.println(Thread.currentThread().getName() + " 开始计算...");// 模拟耗时计算int sum = 0;for (int i = 1; i <= 100; i++) {sum += i;Thread.sleep(10); // 模拟计算耗时}System.out.println(Thread.currentThread().getName() + " 计算完成!");return sum; // 返回计算结果}}
}
代码说明:
Callable
是一个泛型接口,需要指定返回值类型call()
方法可以返回结果,而run()
方法没有返回值call()
方法可以抛出受检异常- 需要使用
FutureTask
来包装Callable
对象,并获取返回结果 FutureTask.get()
方法会阻塞当前线程,直到获取到计算结果
多线程执行原理
- 当调用线程的
start()
方法时,JVM 会创建一个新的线程,并在该线程中执行run()
方法 - 主线程和新创建的线程是并行执行的,它们的执行顺序由操作系统的线程调度机制决定
- 线程调度机制采用时间分片策略,快速切换不同线程执行,造成多线程同时执行的假象
- 即使主线程执行完毕,其他线程也会继续执行直到完成
- 所有非守护线程执行完毕后,JVM 才会退出
public class ThreadExecutionDemo {public static void main(String[] args) {System.out.println("主线程开始执行");// 创建并启动第一个线程Thread thread1 = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("线程1执行: " + i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}, "线程1");// 创建并启动第二个线程Thread thread2 = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("线程2执行: " + i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}, "线程2");thread1.start();thread2.start();// 主线程执行自己的任务for (int i = 0; i < 5; i++) {System.out.println("主线程执行: " + i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("主线程执行完毕");}
}
执行结果分析:
上述代码的执行结果中,三个线程(主线程、线程1、线程2)的输出会交替出现,每次运行的顺序可能都不同,这正是多线程并发执行的特性。
Thread 类常用方法
方法 | 说明 |
---|---|
start() | 启动线程,JVM会调用该线程的run() 方法 |
run() | 线程执行的任务代码,通常需要重写 |
getName() | 获取线程名称 |
setName(String name) | 设置线程名称 |
currentThread() | 返回当前正在执行的线程对象 |
yield() | 线程让步,暂停当前线程,让其他线程执行 |
join() | 线程插队,等待该线程执行完毕后再继续执行其他线程 |
sleep(long millis) | 让当前线程休眠指定毫秒数 |
interrupt() | 中断线程,设置线程的中断状态 |
isInterrupted() | 判断线程是否被中断 |
stop() | 强制停止线程(已过时,不推荐使用) |
setPriority(int newPriority) | 设置线程优先级(1-10,默认5) |
getPriority() | 获取线程优先级 |
setDaemon(boolean on) | 设置为守护线程(后台线程) |
isDaemon() | 判断是否为守护线程 |
public class ThreadMethodsDemo {public static void main(String[] args) {Thread thread = new Thread(() -> {// 获取当前线程并设置名称Thread currentThread = Thread.currentThread();currentThread.setName("演示线程");System.out.println(currentThread.getName() + " 开始执行");System.out.println(currentThread.getName() + " 优先级: " + currentThread.getPriority());try {// 线程休眠1秒Thread.sleep(1000);for (int i = 0; i < 3; i++) {System.out.println(currentThread.getName() + " 执行中: " + i);// 每执行一次就让步一次if (i == 1) {System.out.println(currentThread.getName() + " 进行让步");Thread.yield();}}} catch (InterruptedException e) {System.out.println(currentThread.getName() + " 被中断");return;}System.out.println(currentThread.getName() + " 执行完毕");});// 设置线程优先级thread.setPriority(Thread.NORM_PRIORITY + 1);// 启动线程thread.start();// 主线程等待演示线程执行完毕try {System.out.println("主线程等待演示线程执行完毕...");thread.join(); // 等待线程执行完毕} catch (InterruptedException e) {e.printStackTrace();}System.out.println("主线程执行完毕");}
}
线程安全
当多个线程同时操作同一个共享资源时,可能会出现数据不一致的问题,这就是线程安全问题。
public class ThreadSafetyProblem {public static void main(String[] args) {// 创建共享资源Counter counter = new Counter();// 创建多个线程操作共享资源Thread thread1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {counter.increment();}}, "线程1");Thread thread2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {counter.increment();}}, "线程2");// 启动线程thread1.start();thread2.start();// 等待两个线程执行完毕try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}// 预期结果应该是20000,但实际可能小于这个值System.out.println("最终计数: " + counter.getCount());}// 共享资源类static class Counter {private int count = 0;// 自增方法(线程不安全)public void increment() {count++; // 这行代码实际包含三个操作:读取、修改、写入}public int getCount() {return count;}}
}
问题分析:
上述代码中,count++
操作看似简单,实则包含三个步骤:
- 读取当前 count 值
- 将 count 值加 1
- 将结果写回 count
当两个线程同时执行这三个步骤时,可能会出现数据覆盖,导致最终结果小于预期的 20000。
线程同步(解决线程安全问题)
解决线程安全问题的核心是保证对共享资源的原子操作,即同一时间只能有一个线程访问共享资源。
1. 同步代码块
使用 synchronized
关键字创建同步代码块,将访问共享资源的核心代码包裹起来。
public class SynchronizedBlockExample {public static void main(String[] args) {// 创建共享资源SafeCounter counter = new SafeCounter();// 创建多个线程操作共享资源Thread thread1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {counter.increment();}}, "线程1");Thread thread2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {counter.increment();}}, "线程2");// 启动线程thread1.start();thread2.start();// 等待两个线程执行完毕try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}// 现在结果应该正确是20000System.out.println("最终计数: " + counter.getCount());}// 线程安全的计数器类static class SafeCounter {private int count = 0;// 锁对象,多个线程必须使用同一个锁对象private final Object lock = new Object();// 使用同步代码块保证线程安全public void increment() {synchronized (lock) { // 加锁count++;} // 自动解锁}public int getCount() {synchronized (lock) { // 读取也需要同步return count;}}}
}
代码说明:
- 同步代码块格式:
synchronized(锁对象) { ... }
- 锁对象可以是任意对象,但多个线程必须使用同一个锁对象
- 进入同步代码块前需要获取锁,执行完毕后自动释放锁
- 同一时间只有一个线程能获取到锁,保证了代码块的原子性
2. 同步方法
将 synchronized
关键字修饰在方法上,使整个方法成为同步方法。
public class SynchronizedMethodExample {public static void main(String[] args) {// 创建共享资源MethodCounter counter = new MethodCounter();// 创建多个线程操作共享资源Thread thread1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {counter.increment();}}, "线程1");Thread thread2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {counter.increment();}}, "线程2");// 启动线程thread1.start();thread2.start();// 等待两个线程执行完毕try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("最终计数: " + counter.getCount());}// 使用同步方法的计数器类static class MethodCounter {private int count = 0;// 同步实例方法,隐式锁是this对象public synchronized void increment() {count++;}// 同步实例方法public synchronized int getCount() {return count;}// 静态同步方法,隐式锁是类对象(MethodCounter.class)public static synchronized void staticMethod() {// 静态方法中的同步代码}}
}
代码说明:
- 同步方法有隐式的锁对象:
- 实例方法的锁对象是
this
(当前对象) - 静态方法的锁对象是类的
Class
对象(如MethodCounter.class
)
- 实例方法的锁对象是
- 同步方法的整个方法体都是同步的,保证了方法的原子性
3. Lock 锁
Lock
是 Java 5 引入的更灵活的锁机制,比 synchronized
功能更强大。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class LockExample {public static void main(String[] args) {// 创建共享资源LockCounter counter = new LockCounter();// 创建多个线程操作共享资源Thread thread1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {counter.increment();}}, "线程1");Thread thread2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {counter.increment();}}, "线程2");// 启动线程thread1.start();thread2.start();// 等待两个线程执行完毕try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("最终计数: " + counter.getCount());}// 使用Lock锁的计数器类static class LockCounter {private int count = 0;// 创建Lock锁对象,使用ReentrantLock实现private final Lock lock = new ReentrantLock();public void increment() {lock.lock(); // 加锁try {// 访问共享资源的核心代码count++;} finally {lock.unlock(); // 解锁,放在finally中确保一定会执行}}public int getCount() {lock.lock(); // 加锁try {return count;} finally {lock.unlock(); // 解锁}}}
}
代码说明:
Lock
是接口,常用实现类是ReentrantLock
- 使用步骤:
- 创建
Lock
对象:Lock lock = new ReentrantLock();
- 在操作共享资源前调用
lock.lock()
加锁 - 在
finally
块中调用lock.unlock()
解锁,确保锁一定会释放
- 创建
Lock
相比synchronized
提供了更多功能,如尝试获取锁、可中断锁、超时锁等
线程池
线程池是一种管理线程的机制,它可以重复利用线程,避免频繁创建和销毁线程带来的性能开销。
线程池的优势
- 减少线程创建和销毁的开销
- 控制并发线程数量,避免资源耗尽
- 提高响应速度,线程可以立即处理任务
- 便于管理和监控线程
线程池的使用
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;public class ThreadPoolExample {public static void main(String[] args) {// 1. 创建线程池,指定核心线程数为3ExecutorService executorService = Executors.newFixedThreadPool(3);// 2. 提交任务for (int i = 0; i < 10; i++) {final int taskNum = i;// 提交Runnable任务executorService.execute(() -> {System.out.println("任务 " + taskNum + " 由线程 " + Thread.currentThread().getName() + " 执行");try {// 模拟任务执行时间TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}});}// 3. 关闭线程池executorService.shutdown();try {// 等待所有任务完成,最多等待10秒if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {// 如果超时,强制关闭executorService.shutdownNow();}} catch (InterruptedException e) {executorService.shutdownNow();}System.out.println("所有任务执行完毕");}
}
常见的线程池类型
-
newFixedThreadPool(int nThreads)
:创建固定大小的线程池- 核心线程数和最大线程数都是指定的 nThreads
- 适用于任务数量已知且相对稳定的场景
-
newCachedThreadPool()
:创建可缓存的线程池- 核心线程数为0,最大线程数为 Integer.MAX_VALUE
- 线程空闲60秒后会被回收
- 适用于短期任务较多的场景
-
newSingleThreadExecutor()
:创建单线程的线程池- 只有一个线程执行任务,任务按顺序执行
- 适用于需要保证任务顺序执行的场景
-
newScheduledThreadPool(int corePoolSize)
:创建可定时执行任务的线程池- 可以延迟执行或定期执行任务
- 适用于需要定时任务的场景
线程池的核心参数(ThreadPoolExecutor)
对于更复杂的场景,可以直接使用 ThreadPoolExecutor
类创建线程池,它有更多可配置的参数:
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, // 核心线程数maximumPoolSize, // 最大线程数keepAliveTime, // 非核心线程空闲时间unit, // 时间单位workQueue, // 任务队列threadFactory, // 线程工厂handler // 拒绝策略
);
任务拒绝策略
当线程池中的线程数达到最大线程数且任务队列已满时,新提交的任务会被拒绝,此时会触发拒绝策略:
AbortPolicy
:直接抛出RejectedExecutionException
异常(默认策略)CallerRunsPolicy
:由提交任务的线程自己执行该任务DiscardPolicy
:直接丢弃该任务,不抛出异常DiscardOldestPolicy
:丢弃任务队列中最旧的任务,然后尝试提交新任务
并行与并发
-
并行(Parallelism):多个任务同时执行,需要多个CPU核心支持
- 例如:两个CPU核心同时处理两个不同的任务
-
并发(Concurrency):多个任务交替执行,通过CPU时间分片实现
- 例如:一个CPU核心快速切换处理多个任务,造成同时执行的假象
在Java多线程编程中,我们通常关注的是并发编程,通过多线程实现并发处理,提高程序的执行效率和响应速度。
总结
Java多线程编程是实现高效并发程序的基础,本文介绍了线程的创建方式、执行原理、常用方法、线程安全问题及解决方案,以及线程池的使用。掌握多线程编程可以帮助我们开发出更高效、响应更快的应用程序,但同时也需要注意线程安全问题,合理设计同步机制,避免死锁、活锁等问题的发生。
在实际开发中,应根据具体场景选择合适的线程创建方式和同步机制,并合理使用线程池管理线程资源,以提高程序性能和可靠性。