多线程详解:从基础到实践
在现代编程中,多线程是一种常见的并发执行技术,它允许程序同时执行多个任务。本文将详细介绍多线程的基本概念、实现方式、线程控制、线程同步、死锁、线程间通信以及线程池等高级主题。
多线程概述
进程与线程
进程:是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源。例如,IDEA、阿里云盘、WeGame、Steam等都是以进程的形式运行的。
线程:是进程中的单个顺序控制流,是一条执行路径。一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行路径,则称为多线程程序。
Java程序运行原理
思考:Java程序启动时,是单线程程序还是多线程程序?答案是多线程程序,因为它至少包括主线程和垃圾回收线程。
实现多线程的三种方式
在Java中实现多线程有多种方式,其中三种常见的实现方案包括继承Thread
类、实现Runnable
接口以及实现Callable
接口。下面将详细介绍这三种方式的实现方法,并比较它们的优缺点。
1. 继承Thread类
实现方法:
class MyThread extends Thread {
@Override
public void run() {
// 线程执行的代码
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // 启动线程
}
}
优点:
- 直接继承
Thread
类,使用简单直观。
缺点:
- 无法继承其他类,因为Java不支持多重继承,这限制了继承
Thread
类的方式使用。 - 线程执行完毕后无法返回结果给主线程。
2. 实现Runnable接口
实现方法:
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动线程
}
}
优点:
- 实现
Runnable
接口的方式没有类的单继承的局限性,可以继承其他类。 - 适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码、数据有效分离,较好的体现了面向对象的设计思想。
缺点:
- 线程执行完毕后无法返回结果给主线程。
3. 实现Callable接口
实现方法:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 线程执行的代码
return 123; // 有返回值
}
}
public class CallableExample {
public static void main(String[] args) throws Exception {
MyCallable callable = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(callable);
Thread t = new Thread(task);
t.start();
// 获取线程执行的结果
Integer result = task.get();
System.out.println("Result: " + result);
}
}
优点:
- 可以有返回值,主线程可以通过
Future
对象获取子线程的执行结果。 - 可以抛出异常,使得异常处理更加灵活。
缺点:
- 代码相对复杂,需要结合
FutureTask
使用。 - 创建和维护的开销相对较大。
比较
- 继承
Thread
类的方式是最直观的实现多线程的方法,但它限制了类的继承结构,且无法处理线程返回结果。 - 实现
Runnable
接口的方式更加灵活,可以避免继承限制,且代码与线程的分离更符合面向对象的设计原则。 - 实现
Callable
接口的方式在功能上最为强大,支持返回值和异常处理,适用于需要从子线程获取结果的场景,但实现相对复杂。
在实际开发中,选择哪种方式取决于具体需求。如果需要从线程中获取结果或抛出异常,Callable
接口是更好的选择。如果只是简单的执行任务,且不需要返回结果,Runnable
接口或继承Thread
类都是可行的方案。
线程控制
休眠线程
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
加入线程
public final void join() {
// 调用join方法,使当前线程等待该线程终止
}
礼让
public static void yield() {
Thread.yield(); // 礼让CPU给其他线程
}
后台线程
public final void setDaemon(boolean on) {
Thread.currentThread().setDaemon(on); // 设置为后台线程
}
中断线程
public final void stop()
public void interrupt()
用户线程:优先级高于守护线程。
守护线程【后台线程】:当一个程序没有了用户线程,守护线程也就没有了。
线程同步
线程同步是确保多个线程在访问共享资源时,保持数据一致性和完整性的一种机制。
同步代码块
public synchronized void synchronizedMethod() {
// 需要同步的代码
}
同步方法
public class SynchronizedExample {
public synchronized void method() {
// 需要同步的代码
}
}
Lock锁的使用
在Java中,synchronized
关键字提供了一种内置的同步机制,但它在某些情况下不够灵活。例如,它不允许尝试非阻塞地获取锁,也不支持尝试超时获取锁。为了解决这些问题,Java并发库提供了Lock
接口及其实现类,如ReentrantLock
。
Lock接口
Lock
接口提供了比synchronized
更复杂的线程同步控制。它允许更灵活的结构,可以具有多个相关联的锁,并且可以被多个不同的线程持有。
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
ReentrantLock类
ReentrantLock
是Lock
接口的一个具体实现,它是一个可重入的互斥锁,这意味着同一线程可以多次获得锁。
基本使用
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
public void performAction() {
lock.lock(); // 获取锁
try {
// 保护的代码块
System.out.println("执行任务...");
} finally {
lock.unlock(); // 释放锁
}
}
}
在上述代码中,我们创建了一个ReentrantLock
对象,并在performAction
方法中使用lock()
和unlock()
方法来保护代码块,确保同一时间只有一个线程可以执行该代码块。
尝试获取锁
public void performAction() {
if (lock.tryLock()) {
try {
// 保护的代码块
System.out.println("执行任务...");
} finally {
lock.unlock(); // 释放锁
}
} else {
System.out.println("无法获取锁");
}
}
在这个例子中,我们尝试非阻塞地获取锁。如果获取成功,我们就执行保护的代码块;如果获取失败,我们就打印一条消息。
尝试超时获取锁
import java.util.concurrent.TimeUnit;
public void performActionWithTimeout(long timeout, TimeUnit unit) {
if (lock.tryLock(timeout, unit)) {
try {
// 保护的代码块
System.out.println("执行任务...");
} finally {
lock.unlock(); // 释放锁
}
} else {
System.out.println("在指定的时间内无法获取锁");
}
}
在这个例子中,我们尝试在指定的时间内获取锁。如果在指定时间内获取成功,我们就执行保护的代码块;如果超时,我们就打印一条消息。
条件对象
ReentrantLock
还支持条件对象,它允许线程在某些条件下等待或唤醒。
import java.util.concurrent.locks.Condition;
public class ConditionExample {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean ready = false;
public void waitForCondition() {
lock.lock();
try {
while (!ready) {
condition.await(); // 等待条件
}
// 条件满足后执行的代码
System.out.println("条件满足,执行任务...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signalCondition() {
lock.lock();
try {
ready = true;
condition.signalAll(); // 唤醒所有等待的线程
} finally {
lock.unlock();
}
}
}
在这个例子中,我们创建了一个条件对象,并在waitForCondition
方法中使用await()
方法等待条件满足。在signalCondition
方法中,我们设置条件为满足,并使用signalAll()
方法唤醒所有等待的线程。
解析
使用Lock
和ReentrantLock
的好处是它们提供了更细粒度的控制,包括尝试非阻塞获取锁、尝试超时获取锁以及条件对象。这些特性使得Lock
在某些场景下比synchronized
关键字更加灵活和强大。例如,当你需要在等待锁时执行更复杂的逻辑,或者需要多个条件控制线程的等待和唤醒时,Lock
和ReentrantLock
就显得非常有用。
通过使用Lock
,我们可以更精确地控制线程的同步,从而提高程序的性能和响应性。同时,Lock
也提供了更好的错误处理能力,因为它允许我们在无法获取锁时采取其他措施,而不是简单地阻塞线程。
死锁
死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种僵局。
线程间通信
线程间通信通常涉及到生产者和消费者模式,其中生产者线程生成数据,消费者线程消费数据。
等待唤醒机制
public class CommunicationExample {
private List<Integer> list = Collections.synchronizedList(new ArrayList<>());
public void produce() {
synchronized (list) {
while (list.size() == 10) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Integer(1));
list.notifyAll();
}
}
public void consume() {
synchronized (list) {
while (list.size() == 0) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int item = list.remove(0);
list.notifyAll();
}
}
}
Java 线程组详解
什么是线程组?
Java中的线程组(ThreadGroup
)是一个用于管理和组织一组相关线程的容器。每个线程在Java中必须隶属于一个线程组,它不仅提供了一种逻辑上的分组方式,也便于进行批量控制和异常处理等操作。线程组通过树状结构来表示层级关系,从而实现对线程生命周期的集中管理。
线程组的主要作用
- 组织:将相似或相关的线程放在同一个组内,便于管理。
- 控制:可以对整个线程组执行操作,如挂起、恢复、中断等。
- 监视:可以获取线程组的状态信息,如活动线程数、线程组名称等。
- 安全性:线程组可以用于设置安全性策略,限制组内线程的权限。
如何创建线程组
要创建线程组,你可以使用ThreadGroup
类的构造函数。以下是一个创建线程组的示例:
ThreadGroup parentGroup = new ThreadGroup("ParentGroup"); // 创建一个名为ParentGroup的线程组
ThreadGroup childGroup = new ThreadGroup(parentGroup, "ChildGroup"); // 创建一个名为ChildGroup的子线程组
在上面的示例中,我们首先创建了一个名为ParentGroup
的线程组,然后在该组内创建了一个名为ChildGroup
的子线程组。
线程组的管理
活动线程数
要获取线程组内的活动线程数,可以使用activeCount()
方法。该方法返回线程组中当前活动线程的估计数目。
int activeThreads = threadGroup.activeCount();
线程组的中断
通过调用interrupt()
方法,你可以中断线程组内的所有线程。这将导致线程组内的每个线程抛出InterruptedException
异常。
threadGroup.interrupt();
线程组的异常处理
在Java中,线程组可以实现对一组线程的批量操作和统一管理。例如,通过重写ThreadGroup
类的uncaughtException(Thread t, Throwable e)
方法,可以在一个线程组中的任意线程抛出未捕获异常时,由该线程组统一进行异常处理。
public class ThreadGroupExceptionHandlerDemo {
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("MyGroup") {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName() + " threw an exception: " + e.getMessage());
}
};
Thread thread1 = new Thread(threadGroup, () -> {
throw new RuntimeException("An unchecked exception from Thread 1");
});
thread1.start();
}
}
在这个例子中,所有属于"MyGroup"线程组的线程在其run()
方法内抛出未被捕获的异常时,都会触发自定义的uncaughtException()
方法,从而实现了对整个线程组内异常的集中处理。
线程组的优先级限制
Java线程组还提供了设置其下所有线程最大优先级的功能,这意味着即使某个线程尝试将优先级设置得高于线程组的最大允许值,最终也会被限制在最大优先级以下。
public class ThreadGroupPriorityLimitDemo {
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("LimitedPriorityGroup");
threadGroup.setMaxPriority(6);
Thread highPriorityThread = new Thread(threadGroup, () -> {
Thread.currentThread().setPriority(9); // This will be capped at 6
System.out.println("Actual priority of this thread: " + Thread.currentThread().getPriority());
});
highPriorityThread.start();
}
}
运行上述代码后,尽管高优先级线程试图将其优先级设为9,但受限于线程组的限制,实际执行时其优先级仍会被调整为6。
总结
线程组在Java多线程编程中提供了层次化的线程组织模型,并通过数据结构属性、创建与继承关系以及权限控制机制,实现了对线程集合的有效管理和安全性保障。合理使用线程组可以提高程序的可维护性和健壮性,同时也能有效地管理和监控线程的执行状态。
线程池
线程池是一种执行器(Executor),用于在一个后台线程中执行任务。线程池的主要目的是减少在创建和销毁线程时所产生的性能开销。
创建线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
提交任务
pool.submit(() -> {
System.out.println("任务执行中...");
});
关闭线程池
pool.shutdown();
总结
多线程编程是一种强大的技术,它允许程序同时执行多个任务,提高程序的效率和响应性。通过继承Thread
类或实现Runnable
接口,我们可以轻松地创建和管理线程。线程同步和死锁是多线程编程中需要特别注意的问题,而线程池则提供了一种高效的方式来管理大量线程。通过这些技术,我们可以构建出高性能、高并发的应用程序。