Java进阶day04:多线程实战与并发编程入门指南
目录
线程与多线程的基本概念
线程
多线程
创建线程的三种方式
继承 Thread 类
实现 Runnable 接口
实现 Callable 接口
线程常用方法
start()
run()
sleep(long millis)
join()
yield()
isAlive()
线程安全与线程同步
线程安全
线程同步
synchronized 关键字
Lock 接口
线程池
并发和并行
并发
并行
在 Java 编程的世界里,多线程是一项强大且重要的技术。它能显著提升程序的性能和响应能力,让程序在同一时间处理多个任务。接下来,让我们深入了解 Java 多线程的相关知识。
线程与多线程的基本概念
线程
线程是程序执行的最小单位,它是进程中的一个执行路径。一个进程可以包含多个线程,每个线程都有自己独立的栈空间,但它们共享进程的堆空间和方法区。就好比一个工厂里的不同流水线工人,每个工人负责一部分工作,这些工人就类似于线程。
以一个简单的 Java 程序为例,即使我们没有显式地创建线程,Java 程序也至少有一个主线程在运行。下面的代码展示了一个简单的 Java 程序,主线程会执行 main
方法中的代码:
public class SingleThreadExample {
public static void main(String[] args) {
System.out.println("主线程开始执行");
for (int i = 0; i < 5; i++) {
System.out.println("主线程正在执行任务: " + i);
}
System.out.println("主线程执行结束");
}
}
在这个例子中,程序从 main
方法开始执行,这就是主线程的执行路径。主线程依次输出信息,完成任务后结束。
多线程
多线程则是指在一个程序中同时运行多个线程,每个线程执行不同的任务。多线程可以充分利用多核处理器的优势,提高程序的运行效率。例如,在一个视频播放器中,一个线程负责播放视频,另一个线程负责播放音频,这样可以让用户同时看到画面和听到声音。
下面的代码展示了一个简单的多线程程序,创建了两个线程,分别执行不同的任务:
class VideoPlayer extends Thread {
@Override
public void run() {
System.out.println("视频播放线程开始执行");
for (int i = 0; i < 5; i++) {
System.out.println("正在播放视频帧: " + i);
}
System.out.println("视频播放线程执行结束");
}
}
class AudioPlayer extends Thread {
@Override
public void run() {
System.out.println("音频播放线程开始执行");
for (int i = 0; i < 5; i++) {
System.out.println("正在播放音频帧: " + i);
}
System.out.println("音频播放线程执行结束");
}
}
public class MultiThreadExample {
public static void main(String[] args) {
VideoPlayer videoPlayer = new VideoPlayer();
AudioPlayer audioPlayer = new AudioPlayer();
videoPlayer.start();
audioPlayer.start();
}
}
在这个例子中,VideoPlayer
和 AudioPlayer
分别继承自 Thread
类,并重写了 run
方法。在 main
方法中,创建了这两个线程的实例,并调用 start
方法启动它们。这两个线程会并发执行,各自完成自己的任务。
创建线程的三种方式
继承 Thread 类
这是创建线程的最简单方式,只需要定义一个类继承 Thread
类,并重写 run()
方法,在 run()
方法中定义线程要执行的任务。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行任务");
try {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 执行任务: " + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 " + Thread.currentThread().getName() + " 执行任务结束");
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start();
thread2.start();
}
}
在这个例子中,MyThread
类继承自 Thread
类,并重写了 run
方法。在 run
方法中,线程会输出执行信息,并模拟执行任务的过程,每次执行任务后会休眠 100 毫秒。在 main
方法中,创建了两个 MyThread
线程的实例,并启动它们。
实现 Runnable 接口
实现 Runnable
接口也是一种常见的创建线程的方式。定义一个类实现 Runnable
接口,并重写 run()
方法,然后将该类的实例作为参数传递给 Thread
类的构造函数。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行任务");
try {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 执行任务: " + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 " + Thread.currentThread().getName() + " 执行任务结束");
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable);
Thread thread2 = new Thread(myRunnable);
thread1.start();
thread2.start();
}
}
在这个例子中,MyRunnable
类实现了 Runnable
接口,并重写了 run
方法。在 main
方法中,创建了 MyRunnable
类的实例,并将其作为参数传递给 Thread
类的构造函数,创建了两个线程并启动它们。
实现 Callable 接口
Callable
接口与 Runnable
接口类似,但 Callable
接口的 call()
方法可以有返回值,并且可以抛出异常。通常与 FutureTask
类一起使用。
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行任务");
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += i;
System.out.println(Thread.currentThread().getName() + " 执行任务: " + i);
Thread.sleep(100);
}
System.out.println("线程 " + Thread.currentThread().getName() + " 执行任务结束");
return sum;
}
}
public class CallableExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
Integer result = futureTask.get();
System.out.println("线程执行结果: " + result);
}
}
在这个例子中,MyCallable
类实现了 Callable
接口,并重写了 call
方法。call
方法会计算 0 到 4 的整数之和,并返回结果。在 main
方法中,创建了 MyCallable
类的实例,并将其封装在 FutureTask
中,然后将 FutureTask
作为参数传递给 Thread
类的构造函数,创建并启动线程。最后,通过 futureTask.get()
方法获取线程的执行结果。
线程常用方法
start()
启动线程,使线程进入就绪状态,等待 CPU 调度执行。以下代码展示了 start()
方法的使用:
class MyStartThread extends Thread {
@Override
public void run() {
System.out.println("线程 " + Thread.currentThread().getName() + " 开始执行");
}
}
public class StartMethodExample {
public static void main(String[] args) {
MyStartThread thread = new MyStartThread();
thread.start();
}
}
在这个例子中,创建了一个继承自 Thread
类的 MyStartThread
类,并重写了 run
方法。在 main
方法中,创建了 MyStartThread
类的实例,并调用 start
方法启动线程。
run()
线程的执行体,包含线程要执行的任务。以下是一个简单的示例:
class MyRunThread extends Thread {
@Override
public void run() {
System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行任务");
for (int i = 0; i < 3; i++) {
System.out.println("执行任务: " + i);
}
}
}
public class RunMethodExample {
public static void main(String[] args) {
MyRunThread thread = new MyRunThread();
// 注意:直接调用 run 方法不会启动新线程,而是在当前线程中执行
thread.run();
}
}
在这个例子中,MyRunThread
类重写了 run
方法,定义了线程要执行的任务。在 main
方法中,直接调用 run
方法,此时任务会在主线程中执行,而不是启动一个新线程。
sleep(long millis)
使当前线程暂停执行指定的毫秒数,让出 CPU 资源。以下代码展示了 sleep
方法的使用:
class MySleepThread extends Thread {
@Override
public void run() {
System.out.println("线程 " + Thread.currentThread().getName() + " 开始执行");
try {
Thread.sleep(2000); // 线程休眠 2 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 " + Thread.currentThread().getName() + " 执行结束");
}
}
public class SleepMethodExample {
public static void main(String[] args) {
MySleepThread thread = new MySleepThread();
thread.start();
}
}
在这个例子中,MySleepThread
类的 run
方法中调用了 Thread.sleep(2000)
,使线程休眠 2 秒。在 main
方法中,创建并启动了这个线程。
join()
等待该线程执行完毕,其他线程再继续执行。以下代码展示了 join
方法的使用:
class MyJoinThread extends Thread {
@Override
public void run() {
System.out.println("线程 " + Thread.currentThread().getName() + " 开始执行");
try {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " 执行任务: " + i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 " + Thread.currentThread().getName() + " 执行结束");
}
}
public class JoinMethodExample {
public static void main(String[] args) throws InterruptedException {
MyJoinThread thread = new MyJoinThread();
thread.start();
thread.join(); // 主线程等待该线程执行完毕
System.out.println("主线程继续执行");
}
}
在这个例子中,MyJoinThread
类的 run
方法模拟了一个执行任务的过程。在 main
方法中,创建并启动了这个线程,然后调用 join
方法,主线程会等待 MyJoinThread
线程执行完毕后再继续执行。
yield()
暂停当前正在执行的线程对象,并执行其他线程。以下代码展示了 yield
方法的使用:
class MyYieldThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 执行任务: " + i);
if (i == 2) {
Thread.yield(); // 线程让步
}
}
}
}
public class YieldMethodExample {
public static void main(String[] args) {
MyYieldThread thread1 = new MyYieldThread();
MyYieldThread thread2 = new MyYieldThread();
thread1.start();
thread2.start();
}
}
在这个例子中,MyYieldThread
类的 run
方法中,当 i
等于 2 时,调用 Thread.yield()
方法,当前线程会暂停执行,让其他线程有机会执行。
isAlive()
判断线程是否处于活动状态。以下代码展示了 isAlive
方法的使用:
class MyIsAliveThread extends Thread {
@Override
public void run() {
System.out.println("线程 " + Thread.currentThread().getName() + " 开始执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 " + Thread.currentThread().getName() + " 执行结束");
}
}
public class IsAliveMethodExample {
public static void main(String[] args) throws InterruptedException {
MyIsAliveThread thread = new MyIsAliveThread();
System.out.println("线程是否活动: " + thread.isAlive());
thread.start();
System.out.println("线程是否活动: " + thread.isAlive());
Thread.sleep(1500);
System.out.println("线程是否活动: " + thread.isAlive());
}
}
在这个例子中,在不同的时间点调用 isAlive
方法,判断线程是否处于活动状态。在启动线程之前,线程处于未启动状态,isAlive
方法返回 false
;启动线程后,线程处于活动状态,isAlive
方法返回 true
;线程执行完毕后,isAlive
方法返回 false
。
线程安全与线程同步
线程安全
当多个线程访问共享资源时,如果不进行适当的同步控制,可能会导致数据不一致或其他错误,这就是线程不安全的情况。例如,多个线程同时对一个共享变量进行读写操作,就可能会出现数据错误。
以下代码展示了一个线程不安全的例子:
class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
class IncrementThread extends Thread {
private Counter counter;
public IncrementThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
}
public class ThreadUnsafeExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
IncrementThread thread1 = new IncrementThread(counter);
IncrementThread thread2 = new IncrementThread(counter);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("计数器的值: " + counter.getCount());
}
}
在这个例子中,Counter
类有一个共享变量 count
,increment
方法对 count
进行自增操作。IncrementThread
类的 run
方法会调用 increment
方法 1000 次。在 main
方法中,创建了两个 IncrementThread
线程,它们同时对同一个 Counter
对象进行操作。由于 increment
方法不是线程安全的,最终输出的计数器值可能小于 2000。
线程同步
为了保证线程安全,需要使用线程同步机制。在 Java 中,常用的线程同步方法有 synchronized
关键字和 Lock
接口。
synchronized
关键字
synchronized
关键字可以用于修饰方法或代码块,确保同一时间只有一个线程可以访问被修饰的方法或代码块。
以下是使用 synchronized
关键字修饰方法的例子:
class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
class SynchronizedIncrementThread extends Thread {
private SynchronizedCounter counter;
public SynchronizedIncrementThread(SynchronizedCounter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
}
public class SynchronizedMethodExample {
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
SynchronizedIncrementThread thread1 = new SynchronizedIncrementThread(counter);
SynchronizedIncrementThread thread2 = new SynchronizedIncrementThread(counter);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("计数器的值: " + counter.getCount());
}
}
在这个例子中,SynchronizedCounter
类的 increment
方法被 synchronized
关键字修饰,确保同一时间只有一个线程可以调用该方法。最终输出的计数器值应该是 2000。
以下是使用 synchronized
关键字修饰代码块的例子:
class BlockSynchronizedCounter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
public int getCount() {
return count;
}
}
class BlockSynchronizedIncrementThread extends Thread {
private BlockSynchronizedCounter counter;
public BlockSynchronizedIncrementThread(BlockSynchronizedCounter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
}
public class SynchronizedBlockExample {
public static void main(String[] args) throws InterruptedException {
BlockSynchronizedCounter counter = new BlockSynchronizedCounter();
BlockSynchronizedIncrementThread thread1 = new BlockSynchronizedIncrementThread(counter);
BlockSynchronizedIncrementThread thread2 = new BlockSynchronizedIncrementThread(counter);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("计数器的值: " + counter.getCount());
}
}
在这个例子中,BlockSynchronizedCounter
类的 increment
方法中使用 synchronized
关键字修饰了一个代码块,确保同一时间只有一个线程可以进入该代码块执行 count++
操作。
Lock
接口
Lock
接口提供了更灵活的锁机制,例如 ReentrantLock
类。
以下是使用 ReentrantLock
类的例子:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class LockCounter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
class LockIncrementThread extends Thread {
private LockCounter counter;
public LockIncrementThread(LockCounter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
}
public class LockExample {
public static void main(String[] args) throws InterruptedException {
LockCounter counter = new LockCounter();
LockIncrementThread thread1 = new LockIncrementThread(counter);
LockIncrementThread thread2 = new LockIncrementThread(counter);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("计数器的值: " + counter.getCount());
}
}
在这个例子中,LockCounter
类使用 ReentrantLock
类来实现线程同步。在 increment
方法中,先调用 lock.lock()
方法获取锁,然后执行 count++
操作,最后在 finally
块中调用 lock.unlock()
方法释放锁,确保无论是否发生异常,锁都会被释放。
线程池
线程池是一种管理线程的机制,它可以预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务,任务执行完毕后,线程不会销毁,而是返回线程池等待下一个任务。使用线程池可以减少线程创建和销毁的开销,提高程序的性能。
Java 提供了 ExecutorService
接口和 Executors
类来创建和管理线程池。
以下是使用 Executors.newFixedThreadPool
方法创建固定大小线程池的例子:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Task implements Runnable {
private int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行任务 " + taskId);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 " + Thread.currentThread().getName() + " 完成任务 " + taskId);
}
}
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 5; i++) {
Task task = new Task(i);
executorService.submit(task);
}
executorService.shutdown();
}
}
在这个例子中,使用 Executors.newFixedThreadPool(2)
方法创建了一个固定大小为 2 的线程池。创建了 5 个 Task
任务,并将它们提交给线程池执行。最后,调用 executorService.shutdown()
方法关闭线程池。
并发和并行
并发
并发是指在同一时间段内,多个任务交替执行。在单核处理器系统中,多线程实际上是并发执行的,CPU 通过快速切换线程来实现多个任务的交替执行。
例如,在一个简单的文本编辑器中,一个线程负责处理用户的输入,另一个线程负责实时保存文件。在单核处理器上,这两个线程会交替执行,看起来就像是同时在处理输入和保存文件。
并行
并行是指在同一时刻,多个任务同时执行。在多核处理器系统中,多个线程可以在不同的 CPU 核心上同时执行,实现真正的并行处理。
例如,在一个图像编辑软件中,一个线程负责处理图像的亮度调整,另一个线程负责处理图像的色彩校正。在多核处理器上,这两个线程可以在不同的核心上同时执行,大大提高了处理速度。
通过对 Java 多线程的学习,我们了解了线程和多线程的概念、创建线程的三种方式、线程常用方法、线程安全与同步、线程池以及并发和并行的区别。掌握这些知识,能够让我们在 Java 编程中更好地利用多线程技术,提高程序的性能和响应能力。在实际开发中,我们需要根据具体的需求和场景,合理地使用多线程技术,避免出现线程安全问题。
作者:禹曦a