Java实现多线程的实用途径
一、Java 多线程基础概念
在深入探讨 Java 实现多线程的实用途径之前,我们先来回顾一些基础概念。线程是程序执行流的最小单元,一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等。在 Java 中,多线程编程使得程序能够同时执行多个任务,提高了程序的效率和响应性。
(一)线程的生命周期
Java 线程具有以下几种状态,这些状态构成了线程的生命周期:
- 新建(New):当创建一个
Thread
实例时,线程处于新建状态。例如:
Thread thread = new Thread();
此时线程还未开始执行。
2. 就绪(Runnable):调用 start()
方法后,线程进入就绪状态。在这个状态下,线程等待 CPU 分配时间片,一旦获得 CPU 资源,就可以执行。
thread.start();
- 运行(Running):线程获取到 CPU 时间片后开始执行
run()
方法中的代码,此时线程处于运行状态。 - 阻塞(Blocked):线程由于某些原因暂时无法继续执行,进入阻塞状态。例如,线程在等待 I/O 操作完成、等待获取锁等情况时会进入阻塞状态。
- 等待(Waiting):线程通过调用
Object
类的wait()
方法、Thread
类的join()
方法等进入等待状态。处于等待状态的线程需要其他线程唤醒才能继续执行。 - 超时等待(Timed Waiting):与等待状态类似,但有一个超时时间。例如,调用
Thread.sleep(long millis)
方法,线程会进入超时等待状态,在指定的时间过去后,线程会自动唤醒。 - 终止(Terminated):线程执行完
run()
方法或者因异常退出,就进入终止状态,此时线程生命结束。
(二)线程的优先级
Java 线程有优先级的概念,优先级范围从 1(Thread.MIN_PRIORITY
)到 10(Thread.MAX_PRIORITY
),默认优先级为 5(Thread.NORM_PRIORITY
)。优先级较高的线程在竞争 CPU 资源时会有更大的优势,但这并不意味着优先级高的线程一定会先执行完,因为 CPU 调度算法是复杂的,而且不同操作系统的实现也有所不同。可以通过 setPriority(int newPriority)
方法来设置线程的优先级,通过 getPriority()
方法获取线程的优先级。例如:
Thread highPriorityThread = new Thread(() -> {
// 线程执行代码
});
highPriorityThread.setPriority(Thread.MAX_PRIORITY);
二、Java 实现多线程的经典途径
(一)继承 Thread 类
这是最直接的实现多线程的方式。通过继承 Thread
类,并重写其 run()
方法来定义线程的执行逻辑。
- 示例代码:
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 正在执行,i = " + i);
}
}
}
public class ThreadInheritanceExample {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 正在执行,i = " + i);
}
}
}
在上述代码中,MyThread
类继承自 Thread
类,并重写了 run()
方法。在 main
方法中,创建了 MyThread
的实例并调用 start()
方法启动线程。主线程和新创建的线程会并发执行,输出相应的信息。
2. 优缺点分析
- 优点:实现简单,代码结构清晰,对于简单的多线程任务易于理解和实现。
- 缺点:Java 不支持多重继承,如果一个类已经继承了其他类,就无法再继承 Thread
类。这在一定程度上限制了其使用场景。
(二)实现 Runnable 接口
实现 Runnable
接口也是常用的实现多线程的方式。这种方式将线程执行逻辑与线程对象分离,更符合面向对象的设计原则。
- 示例代码:
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 正在执行,i = " + i);
}
}
}
public class RunnableImplementationExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 正在执行,i = " + i);
}
}
}
在这个例子中,MyRunnable
类实现了 Runnable
接口,并重写了 run()
方法。然后在 main
方法中,创建 MyRunnable
的实例,并将其作为参数传递给 Thread
类的构造函数来创建线程对象,最后调用 start()
方法启动线程。
2. 优缺点分析
- 优点:一个类可以实现多个接口,因此避免了单继承的限制,更灵活。同时,将线程执行逻辑与线程对象分离,使得代码的可维护性和扩展性更好。
- 缺点:相比继承 Thread
类,代码稍微复杂一些,需要创建 Thread
对象并传入 Runnable
实例。
(三)实现 Callable 接口
Callable
接口与 Runnable
接口类似,但 Callable
接口的 call()
方法可以有返回值,并且可以抛出异常。这使得在多线程编程中可以获取线程执行的结果。
- 示例代码:
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
public class CallableImplementationExample {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Integer> future = executorService.submit(myCallable);
try {
Integer result = future.get();
System.out.println("线程执行结果:" + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}
在上述代码中,MyCallable
类实现了 Callable
接口,call()
方法计算 1 到 100 的累加和并返回。在 main
方法中,使用 ExecutorService
来提交 Callable
任务,并通过 Future
获取任务的执行结果。
2. 优缺点分析
- 优点:能够获取线程执行的返回值,适用于需要得到线程执行结果的场景。同时,异常处理更加灵活,可以在调用处捕获 call()
方法抛出的异常。
- 缺点:使用起来相对复杂,需要结合 ExecutorService
和 Future
等类来管理线程和获取结果,对于简单的多线程任务可能显得过于繁琐。
三、使用线程池实现多线程
(一)线程池的概念
线程池是一种多线程处理形式,它维护着一个线程队列,线程池中的线程可以被重复使用来执行任务。当有新任务提交时,线程池会从线程队列中取出一个空闲线程来执行任务,如果线程队列中没有空闲线程,新任务会被放入任务队列等待执行。使用线程池可以避免频繁创建和销毁线程带来的开销,提高系统性能和资源利用率。
(二)Java 中的线程池实现
- 使用 Executors 工厂类创建线程池
- 固定大小线程池(FixedThreadPool):
- 示例代码:
- 固定大小线程池(FixedThreadPool):
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
int taskNumber = i;
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + taskNumber);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
- **原理**:`Executors.newFixedThreadPool(int nThreads)` 方法创建一个固定大小的线程池,池中线程数量固定为 `nThreads`。当提交的任务数量超过线程池的大小,多余的任务会被放入任务队列等待执行。
- **缓存线程池(CachedThreadPool)**:
- **示例代码**:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
int taskNumber = i;
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + taskNumber);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
- **原理**:`Executors.newCachedThreadPool()` 方法创建一个缓存线程池。该线程池没有固定大小,当有新任务提交时,如果线程池中有空闲线程则复用,否则创建新线程。线程池中的线程如果 60 秒内没有被使用,会被回收。
- **单线程化线程池(SingleThreadExecutor)**:
- **示例代码**:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadExecutorExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
int taskNumber = i;
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + taskNumber);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
- **原理**:`Executors.newSingleThreadExecutor()` 方法创建一个单线程的线程池。所有任务会按照提交顺序依次在这个单线程中执行,保证了任务的顺序性。
2. 使用 ThreadPoolExecutor 类自定义线程池
ThreadPoolExecutor
类提供了更灵活的方式来创建线程池,可以自定义线程池的各种参数。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolExample {
public static void main(String[] args) {
BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>(10);
ThreadPoolExecutor executorService = new ThreadPoolExecutor(
2,
4,
10,
TimeUnit.SECONDS,
taskQueue
);
for (int i = 0; i < 15; i++) {
int taskNumber = i;
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + taskNumber);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
在上述代码中,通过 ThreadPoolExecutor
的构造函数创建了一个自定义线程池。构造函数参数解释如下:
- corePoolSize:核心线程数,线程池会一直维护这么多线程,即使它们处于空闲状态。
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数。
- keepAliveTime:当线程数大于核心线程数时,多余的空闲线程的存活时间。
- unit:存活时间的单位。
- workQueue:任务队列,用于存放提交但未执行的任务。
(三)线程池的优点和适用场景
- 优点
- 提高性能:避免了频繁创建和销毁线程的开销,提高了线程的复用性,从而提高系统性能。
- 资源控制:可以控制线程池的大小,避免线程过多导致系统资源耗尽。同时,通过任务队列可以控制任务的数量,避免任务过多积压。
- 便于管理:线程池提供了统一的管理方式,如线程的生命周期管理、任务的提交和执行等,使得多线程编程更加规范和易于维护。
- 适用场景
- 高并发场景:如 Web 服务器处理大量用户请求,使用线程池可以有效处理并发请求,提高系统的吞吐量。
- 重复性任务:对于一些需要重复执行的任务,如定时任务、批量数据处理等,使用线程池可以提高效率。
四、Java 多线程中的同步与通信
(一)线程同步的必要性
在多线程编程中,当多个线程同时访问共享资源时,可能会出现数据不一致的问题。例如,多个线程同时对一个共享变量进行读写操作,可能导致数据的错误更新。为了保证数据的一致性和完整性,需要进行线程同步。
(二)使用 synchronized 关键字实现同步
- 同步方法:在方法声明中加上
synchronized
关键字,使得该方法在同一时间只能被一个线程访问。
class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
在上述代码中,increment()
和 getCount()
方法都被声明为同步方法。当一个线程调用 increment()
方法时,其他线程无法同时调用这两个同步方法,从而保证了 count
变量的操作是线程安全的。
2. 同步块:使用 synchronized
关键字修饰代码块,对特定的代码区域进行同步。
class SynchronizedBlockExample {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
public int getCount() {
synchronized (this) {
return count;
}
}
}
在这个例子中,increment()
和 getCount()
方法通过同步块对 count
变量的操作进行同步。同步块的锁对象为 this
,即当前实例对象。
(三)使用 Lock 接口实现同步
Lock
接口提供了比 synchronized
关键字更灵活的同步控制。Lock
接口的实现类如 ReentrantLock
提供了可中断的锁获取、公平锁等功能。
- 示例代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class LockExample {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
在上述代码中,ReentrantLock
用于同步对 count
变量的操作。lock()
方法用于获取锁,unlock()
方法用于释放锁,为了确保锁一定被释放,通常将 unlock()
方法放在 finally
块中。
(四)线程通信
- 使用 wait()、notify() 和 notifyAll() 方法:这三个方法是
Object
类的方法,用于线程间的通信。wait()
方法使当前线程等待,直到其他线程调用notify()
或notifyAll()
方法唤醒它。
class ThreadCommunicationExample {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程1获取到锁,开始等待");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1被唤醒");
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程2获取到锁,开始工作");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.notify();
System.out.println("线程2唤醒其他线程");
}
});
thread1.start();
thread2.start();
}
}
在上述代码中,thread1
调用 lock.wait()
方法进入等待状态,释放锁。thread2
执行一段时间后调用 lock.notify()
方法唤醒 thread1
。
2. 使用 Condition 接口:Condition
接口是 Lock
接口的配套接口,提供了更灵活的线程通信方式。Condition
可以创建多个等待队列,而 Object
的 wait()
和 notify()
方法只能使用一个等待队列。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ConditionExample {
private static final Lock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
lock.lock();
try {
System.out.println("线程1获取到锁,开始等待");
condition.await();
System.out.println("线程1被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread thread2 = new Thread(() -> {
lock.lock();
try {
System.out.println("线程2获取到锁,开始工作");
Thread.sleep(2000);
condition.signal();
System.out.println("线程2唤醒其他线程");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
thread1.start();
thread2.start();
}
}
在这个例子中,thread1
通过 condition.await()
方法进入等待状态,thread2
执行一段时间后通过 condition.signal()
方法唤醒 thread1
。
五、Java 多线程中的并发工具类
(一)CountDownLatch
CountDownLatch
是一个同步工具类,它允许一个或多个线程等待其他线程完成操作。CountDownLatch
初始化一个计数器,线程可以调用 await()
方法等待计数器归零,其他线程可以调用 countDown()
方法将计数器减一。
- 示例代码:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) {
int threadCount = 3;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
Thread.sleep((long) (Math.random() * 2000));
System.out.println(Thread.currentThread().getName() + " 完成任务");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}).start();
}
try {
System.out.println("主线程等待所有子线程完成任务");
countDownLatch.await();
System.out.println("所有子线程任务完成,主线程继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上述代码中,CountDownLatch
初始化计数器为 3。三个子线程分别执行任务,完成后调用 countDown()
方法将计数器减一。主线程调用 await()
方法等待计数器归零,即所有子线程任务完成后才继续执行。
(二)CyclicBarrier
CyclicBarrier
也是一个同步工具类,它允许一组线程相互等待,直到所有线程都到达某个屏障点。与 CountDownLatch
不同的是,CyclicBarrier
的计数器可以重置,即可以重复使用。
- 示例代码:
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier cyclicBarrier = new CyclicBarrier(threadCount, () -> {
System.out.println("所有线程都到达屏障点,开始执行后续任务");
});
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
Thread.sleep((long) (Math.random() * 2000));
System.out.println(Thread.currentThread().getName() + " 到达屏障点");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " 继续执行");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
在这个例子中,CyclicBarrier
初始化时设置参与的线程数为 3,并传入一个屏障动作。当所有线程调用 await()
方法到达屏障点时,会执行屏障动作,然后所有线程继续执行。
(三)Semaphore
Semaphore
是一个计数信号量,它通过一个计数器来控制同时访问某个资源的线程数量。当一个线程获取信号量时,如果计数器大于 0,则计数器减一,线程可以继续执行;如果计数器为 0,则线程会被阻塞,直到其他线程释放信号量(计数器加一)。
- 示例代码:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int permits = 2;
Semaphore semaphore = new Semaphore(permits);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 获取到信号量,开始执行任务");
Thread.sleep((long) (Math.random() * 2000));
System.out.println(Thread.currentThread().getName() + " 任务执行完毕,释放信号量");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
在上述代码中,Semaphore
初始化允许的信号量数量为 2,即最多允许两个线程同时获取信号量并执行任务。其他线程需要等待有线程释放信号量后才能获取并执行任务。
通过以上多种方式,在 Java 编程中能够有效地实现多线程,并处理多线程编程中遇到的同步、通信以及资源管理等问题,从而编写出高效、稳定的多线程应用程序。不同的实现途径和工具类适用于不同的场景,开发者需要根据具体需求进行选择和组合使用。