掌握JavaSmack:线程管理与并发控制内幕,专家级调试【5大核心策略】
立即解锁
发布时间: 2025-08-23 14:24:09 阅读量: 2 订阅数: 5 


张孝祥Java多线程与并发库高级应用视频教程练习代码


# 摘要
本文全面探讨了Java线程管理和并发控制的各个方面,从基础概念到高级技术,涵盖了线程的创建、生命周期管理、并发机制、并发问题案例分析、线程池原理与实现、锁的优化技术、无锁编程实践、JavaSmack调试工具的使用,以及并发编程的实战应用。通过对这些核心内容的深入研究,文章旨在提供一套完整的Java并发编程解决方案,帮助开发者在设计、调试和优化多线程程序时,能够有效地处理并发问题,并实现高性能的应用。
# 关键字
Java线程管理;并发控制;线程池;锁优化;无锁编程;JavaSmack调试;并发安全数据结构
参考资源链接:[Java资源大全中文版:awesome-java-cn项目介绍与参与指南](https://wenku.csdn.net/doc/4duqvpdjum?spm=1055.2635.3001.10343)
# 1. Java线程管理基础
Java线程管理是并发编程中的核心概念,是构建高效、稳定多线程应用的基础。本章将简述Java线程的创建、执行与生命周期,并概览Java并发的基本工具和机制,为后续深入分析并发模型打下坚实基础。
## 1.1 理解Java线程
Java中的线程是程序执行流的最小单元,可以通过实现Runnable接口或继承Thread类来创建。线程一旦启动,便进入了新生状态,然后通过调度器进入运行状态,执行完毕后则转为终止状态。
```java
class MyThread extends Thread {
public void run() {
// 线程要执行的代码
}
}
MyThread mt = new MyThread();
mt.start(); // 启动线程
```
在上述代码中,MyThread继承了Thread类,并重写了run方法。创建实例后,通过start方法启动线程,这将导致线程调度器调用run方法。
## 1.2 线程状态
Java线程有六种状态:初始(New)、可运行(Runnable)、阻塞(Blocked)、等待(Waiting)、定时等待(Timed Waiting)和终止(Terminated)。线程状态的转换由线程调度器控制,开发者可以通过调用特定的方法触发状态转换。
以阻塞状态为例,当线程试图执行一个需要同步的操作,而锁已被其他线程持有时,它就会转入阻塞状态,直到获得锁为止。
```java
synchronized (lock) {
// 执行同步代码块
}
```
通过synchronized关键字,线程在进入和退出同步块时,可以进入阻塞和非阻塞状态。
## 1.3 线程优先级
Java线程具有优先级的概念,优先级高的线程更容易获得CPU资源。优先级范围从1(最低)到10(最高),默认优先级为5。设置和获取线程优先级的方法如下:
```java
Thread t = new Thread();
t.setPriority(Thread.MAX_PRIORITY); // 设置最高优先级
int priority = t.getPriority(); // 获取当前优先级
```
通过合理设置线程优先级,可以优化应用程序的响应性和性能。
通过本章的内容,您将掌握创建和管理Java线程的基本知识,并为进一步深入学习Java线程并发模型打下坚实的基础。
# 2. 深入理解Java线程并发模型
### 2.1 Java线程的创建与生命周期
#### 2.1.1 线程的创建方式与实例
在Java中,线程的创建和启动是并发编程的基本操作。线程可以通过实现`Runnable`接口或者继承`Thread`类的方式来创建。尽管继承`Thread`类是更直观的方式,但实现`Runnable`接口是推荐的做法,因为它允许类继承其他类,同时使代码更加模块化。
下面是一个简单的线程创建示例:
```java
public class MyThread extends Thread {
@Override
public void run() {
// 线程执行的操作
System.out.println("MyThread.run()");
}
}
public class ThreadCreationDemo {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // 启动线程
}
}
```
另一种方式是使用`Runnable`接口:
```java
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的操作
System.out.println("MyRunnable.run()");
}
}
public class RunnableDemo {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动线程
}
}
```
在实际应用中,根据不同的业务需求和场景选择合适的创建方式是至关重要的。
#### 2.1.2 线程状态与生命周期管理
Java线程的生命周期经历了多种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Terminated)。线程状态的转换过程如下图所示:
```mermaid
graph LR
A(New) -->|start()| B(Runnable)
B -->|scheduler| C(Running)
C -->|yield() / sleep()| B
C -->|wait() / join()| D(Blocked)
D -->|notify() / interrupt()| C
C -->|run()结束| E(Terminated)
```
每个状态的具体含义如下:
- **新建(New)**: 当使用`new Thread()`创建一个线程后,线程处于新建状态。
- **就绪(Runnable)**: 调用`start()`方法后,线程进入就绪状态,等待CPU调度。
- **运行(Running)**: 获得CPU资源后,线程开始执行`run()`方法中的代码。
- **阻塞(Blocked)**: 由于某种原因放弃CPU执行权,暂时停止运行。如执行`sleep()`方法、等待输入/输出操作完成、或执行`wait()`方法等。
- **死亡(Terminated)**: 线程执行完毕或因异常退出`run()`方法时,线程状态变为死亡。
通过调用`Thread.interrupt()`方法可以中断线程,使其从阻塞状态返回到就绪状态。同样,通过调用`Thread.join()`方法可以等待一个线程结束执行,这通常用于父线程等待子线程完成任务。
生命周期管理确保了线程能够有效地工作,也使得程序对线程的控制更加灵活。正确的生命周期管理是实现稳定和高效并发程序的关键。
# 3. Java并发控制高级技术
## 3.1 线程池的原理与实现
### 3.1.1 线程池的内部工作机制
线程池是Java并发编程中非常重要的概念之一,它不仅能够有效管理线程资源,还能提高程序的运行效率。线程池内部通过使用一个任务队列来管理待执行的任务。当任务提交给线程池时,线程池首先会检查当前运行的线程数量是否达到核心线程数,如果没有达到,则创建新的线程来处理任务;如果已达到核心线程数,线程池则会将任务加入到队列中等待执行。如果任务队列满了,则根据线程池的配置可能会创建新的非核心线程,或者直接拒绝任务。
在实现线程池时,通常需要考虑以下几个关键点:
- **线程池的创建与销毁**:合理地创建线程池实例,以及在适当的时候销毁线程池,可以避免资源的无谓消耗。
- **任务的调度与执行**:如何有效地从任务队列中取出任务,并安排给线程执行。
- **线程的复用与管理**:通过线程复用可以减少线程创建和销毁的开销。
- **线程池的关闭策略**:优雅地关闭线程池,处理正在执行的任务和队列中的任务。
### 3.1.2 线程池的配置与监控
线程池的配置是性能调优的关键。配置不当可能会导致线程资源的浪费或应用性能的下降。以下是几个主要的配置参数:
- `corePoolSize`:线程池的核心线程数。
- `maximumPoolSize`:线程池能够容纳的最大线程数。
- `keepAliveTime`:非核心线程空闲时的最大存活时间。
- `workQueue`:线程池的任务队列。
为了监控线程池的工作状态,可以使用JMX(Java Management Extensions)技术,或者通过线程池提供的`getPoolSize()`, `getActiveCount()`, `getTaskCount()`, 和 `getCompletedTaskCount()`等方法来获取运行状态信息。
下面是一个简单的线程池创建和监控示例代码:
```java
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExample {
public static void main(String[] args) throws InterruptedException {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
// 提交任务给线程池
for (int i = 0; i < 5; i++) {
final int taskNumber = i;
executor.execute(() -> {
System.out.println("Running task " + taskNumber + " on thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭线程池并等待所有任务完成
executor.shutdown();
executor.awaitTermination(60, TimeUnit.SECONDS);
// 获取并打印线程池状态
System.out.println("Pool Size: " + executor.getPoolSize());
System.out.println("Active Threads: " + executor.getActiveCount());
System.out.println("Task Count: " + executor.getTaskCount());
System.out.println("Completed Tasks: " + executor.getCompletedTaskCount());
}
}
```
在上面的代码中,我们创建了一个固定大小的线程池,并提交了五个任务。提交后关闭线程池并等待所有任务执行完成。最后我们打印出了线程池的一些状态信息。
理解线程池的工作机制和配置方法是使用Java并发编程中高效利用资源的关键,这不仅可以帮助我们设计出更加健壮的应用程序,同时也能更好地控制程序的性能。
## 3.2 锁的高级特性与应用
### 3.2.1 锁的种类与选择
在Java并发编程中,锁是同步访问共享资源的一种机制。在多线程环境下,正确的使用锁是保证线程安全的关键。Java提供了不同类型的锁,每种锁都有其特定的使用场景:
- **互斥锁(Synchronized)**:最基础的锁类型,可以保证同一时刻只有一个线程能够访问被锁定的代码块或方法。
- **可重入锁(ReentrantLock)**:增强了互斥锁的功能,例如支持尝试获取锁,或者中断线程。
- **读写锁(ReadWriteLock)**:当读操作远多于写操作时,可以提升性能。允许多个读操作同时进行,但在写操作时会独占锁。
- **自旋锁**:通过循环尝试获取锁,适用于锁被持有的时间短,但线程切换成本高的场景。
- **乐观锁与悲观锁**:分别基于乐观和悲观的并发策略,乐观锁通常通过CAS(Compare-And-Swap)操作实现,而悲观锁则使用互斥锁。
在选择锁的类型时,应考虑以下因素:
- **锁的粒度**:锁的粒度越细,对资源的控制越精确,但相应的开销也会增大。
- **性能需求**:在高并发场景下,需要选择更适合的锁来减少竞争和提高吞吐量。
- **实现复杂度**:复杂的锁机制(如读写锁)可能会增加程序的复杂性,需要权衡实现的复杂性和性能提升。
### 3.2.2 锁的优化技术
在使用锁时,如果不加控制地使用可能会导致性能问题,如死锁、饥饿或者资源的不公平访问。因此,针对锁的优化是非常重要的。
- **减少锁的持有时间**:只在必要时持有锁,并且尽量缩短锁的作用范围。
- **细粒度锁**:对于可以独立操作的资源,考虑使用独立的锁,而不是单一的大锁,以减少锁的争用。
- **锁分离**:对于读多写少的数据结构,使用读写锁,提高读操作的并发度。
- **锁粗化**:如果一段代码频繁地请求同一个锁,可以将这些代码合并为一个大的临界区,减少锁的申请和释放次数。
- **使用无锁算法**:当竞争不激烈时,可以使用原子变量等无锁编程技术代替传统的锁。
使用Java中的一些工具类,如`java.util.concurrent.atomic`中的原子类,可以实现无锁操作,减少锁的开销。
```java
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private AtomicInteger atomicInteger = new AtomicInteger(0);
public void increment() {
// 使用原子操作进行自增
atomicInteger.incrementAndGet();
}
public int get() {
// 使用原子操作获取当前值
return atomicInteger.get();
}
}
```
上面的例子展示了使用`AtomicInteger`来实现线程安全的计数器。这种方法避免了使用同步块,从而减少了锁的竞争和相关的开销。
锁的高级特性和优化技术对于设计高性能并发应用至关重要。在实际开发中,合理选择和优化锁的使用,可以显著提高应用的性能和响应速度。
## 3.3 无锁并发编程
### 3.3.1 无锁编程原理
无锁编程(Lock-Free Programming)是一种多线程编程范式,在这种范式中,开发者尽量避免使用锁结构来同步对共享资源的访问。其核心思想是通过原子操作保证操作的原子性和一致性,从而避免锁带来的线程阻塞和上下文切换开销。
无锁编程依赖的原子操作通常由现代CPU通过特定的指令提供支持,常见的操作包括CAS(Compare-And-Swap),这种操作可以保证数据的一致性而不需要阻塞线程。由于原子操作通常是非阻塞的,因此可以在多核处理器上实现高度的并行性。
无锁编程通常用于实现高效的并发数据结构,其优点包括:
- 高性能:由于不需要等待和阻塞,可以在高竞争的环境下保持较高的吞吐量。
- 低延迟:因为避免了线程上下文切换,可以实现更低的响应时间。
### 3.3.2 原子变量与无锁算法实现
在Java中,`java.util.concurrent.atomic`包提供了多种原子变量类,如`AtomicInteger`, `AtomicLong`, `AtomicReference`等,这些类通过提供原子操作方法,使得开发者可以以无锁的方式实现一些基本操作。
无锁算法的实现通常基于以下步骤:
1. **读取共享变量**:使用原子操作安全地读取共享变量的值。
2. **计算新的值**:根据需要进行的计算来更新共享变量的值。
3. **更新共享变量**:尝试使用原子操作更新共享变量的值,如果成功则表示无竞争,如果失败则重新开始算法的执行。
以下是使用`AtomicInteger`来实现无锁计数器的一个例子:
```java
import java.util.concurrent.atomic.AtomicInteger;
public class LockFreeCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
// 使用compareAndSet来安全地增加计数器
count.incrementAndGet();
}
public int getCount() {
// 获取当前计数器的值
return count.get();
}
}
```
在这个例子中,我们使用`AtomicInteger`类提供的原子操作来实现计数器的自增操作,避免了锁的使用。
无锁并发编程是一门高级技术,它对开发者理解并发控制有很高的要求,但是正确使用无锁编程可以极大提升应用程序的性能。在实现复杂的并发数据结构时,理解和利用好无锁编程的原理和技巧,对于构建高效和可扩展的并发程序至关重要。
# 4. JavaSmack在并发调试中的应用
## 4.1 JavaSmack的安装与配置
### 4.1.1 安装JavaSmack环境
JavaSmack是一个用于Java程序分析和调试的工具,可以极大地简化并发调试的过程。它提供了一种直观的方式来观察程序执行和线程间交互。安装JavaSmack之前,请确保你的开发环境满足以下要求:
- Java开发工具包 (JDK) 1.8 或更高版本
- 支持的操作系统(如Windows, Linux, macOS)
下载并安装JavaSmack可以通过以下命令行指令完成:
```bash
java -jar jsmack.jar
```
安装完成后,打开JavaSmack的GUI界面,开始进行环境配置。界面会引导你设置JVM参数,这些参数允许JavaSmack在调试时附加到目标进程。务必记录下这些参数,因为在后续步骤中需要将它们加入到你的应用程序启动脚本中。
### 4.1.2 JavaSmack的配置要点
在配置JavaSmack之前,需要考虑以下几个关键点:
- **确保目标应用程序和JavaSmack使用相同的JVM版本**:这样可以避免兼容性问题。
- **修改应用程序的启动脚本**:加入JVM参数,以便JavaSmack可以连接到正在运行的应用程序。
- **设置调试参数**:指定哪些类或方法需要进行调试监视。JavaSmack支持设置断点、观察变量以及执行步进操作。
一个典型的Java应用程序启动脚本配置示例如下:
```shell
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar your-app.jar
```
在这个示例中,`-agentlib:jdwp`参数配置了调试代理,它告诉JVM启动一个调试端口(在这个例子中是5005端口),从而让JavaSmack能够通过这个端口附加到JVM进程。
## 4.2 JavaSmack的调试功能
### 4.2.1 使用JavaSmack进行断点调试
在Java程序中设置断点是定位问题和理解程序行为的一种有效手段。在JavaSmack中设置断点的步骤如下:
1. **打开JavaSmack的断点视图**:在主界面中,找到“断点”视图并打开它。
2. **添加新的断点**:右键点击目标类或方法,选择“添加断点”选项。
3. **指定断点条件**:根据需要设置断点触发条件。例如,可以设置断点仅在特定线程中触发,或当某个变量满足特定条件时触发。
一旦断点设置完成,就可以启动或继续程序的执行。当程序执行到断点处时,执行会暂停,允许你查看当前的调用堆栈、变量值和其他调试信息。
### 4.2.2 运行时分析与性能监控
JavaSmack提供了强大的运行时分析和性能监控功能,主要包括以下方面:
- **CPU使用情况分析**:帮助识别CPU密集型方法,以及它们的调用次数和消耗时间。
- **内存使用情况分析**:监控堆内存分配和垃圾回收行为,以便发现内存泄漏或过度内存消耗的问题。
- **线程分析**:查看所有活跃线程的当前状态,识别死锁和线程间阻塞情况。
为了实现上述功能,JavaSmack允许用户安装专门的分析器,并且提供了与JConsole和VisualVM等Java监控工具类似的视图和报告。借助这些工具,开发者可以获得关于程序运行状态的深入见解。
## 4.3 JavaSmack的高级调试技巧
### 4.3.1 线程分析与死锁检测
并发程序中的线程分析是一项挑战。JavaSmack提供了一系列工具来帮助开发者分析线程行为,特别是识别死锁条件。
- **线程列表视图**:显示所有活跃线程及其状态,包括运行、等待、睡眠等状态。
- **线程间调用关系**:以图形化的方式展示线程间的调用关系,帮助开发者理解线程间交互。
- **死锁检测**:自动检测应用程序中的死锁情况,并提供死锁发生时的线程调用堆栈信息。
使用这些工具,开发者可以有效地诊断并发问题,并且通过分析线程间的通信模式来预防死锁的发生。
### 4.3.2 内存泄漏诊断与解决策略
内存泄漏是长时间运行的Java应用程序中常见的问题,JavaSmack提供了以下功能来诊断和解决内存泄漏:
- **内存快照比较**:通过比较两个内存快照,可以找到占用内存增加的对象。
- **对象引用追踪**:追踪对象的引用路径,帮助识别哪些对象被无意中保留。
- **内存泄漏报表**:生成详细报告,包括泄露对象的大小、类型和实例数量。
解决内存泄漏的策略一般包括:
- **优化数据结构的使用**:避免在缓存中长期存储大量数据。
- **调整垃圾回收策略**:基于应用程序的具体情况选择合适的垃圾回收算法。
- **重构代码**:移除不再使用的对象,避免循环引用。
以下是使用JavaSmack诊断内存泄漏的代码块示例:
```java
// 示例代码片段,用于分析内存泄漏
MemoryAnalyzer mAnalyzer = new MemoryAnalyzer();
mAnalyzer.setSnapshot("path_to_first_heap_dump.hprof");
mAnalyzer.setSnapshot("path_to_second_heap_dump.hprof");
mAnalyzer.analyze();
// 分析结果输出
List<LeakSuspects> suspects = mAnalyzer.getLeakSuspects();
suspects.forEach(println);
```
在此代码中,`MemoryAnalyzer`类用于加载和分析两个内存快照,然后报告内存泄漏的潜在原因。输出的`LeakSuspects`对象包含了所有可能的内存泄漏候选者,并可以逐个进行详细检查。
# 5. Java并发编程实战演练
在Java并发编程的世界里,理解理论基础和高级技巧固然重要,但将这些知识转化为实践能力同样关键。在本章节中,我们将通过实战演练的方式,深入探讨如何设计一个高效的线程池,如何构建并发安全的数据结构,以及如何解决并发中遇到的实际问题。
## 5.1 设计一个高效的线程池
线程池是Java并发编程中一个非常重要的组件,它能够有效地控制线程数量,提高程序性能,管理线程生命周期。在设计线程池时,需要考虑以下原则和最佳实践:
### 5.1.1 线程池设计原则与最佳实践
线程池的设计需要考虑任务的特性(CPU密集型、IO密集型)、系统资源、CPU核心数等因素。一般建议使用`ThreadPoolExecutor`或`ScheduledThreadPoolExecutor`来创建线程池。
**最佳实践包括:**
- **避免使用Executors创建线程池**,因为默认的线程池可能无法满足特定的性能要求。
- **使用线程池的构造函数直接创建**,可以自定义线程工厂、拒绝策略等。
- **合理设置线程池参数**,例如核心线程数、最大线程数、存活时间等。
- **动态调整线程池参数**,根据实际应用的负载情况,动态调整线程池参数。
### 5.1.2 实现自定义线程池的案例分析
我们可以通过一个案例来演示如何实现一个自定义的线程池。
```java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPool {
public static void main(String[] args) {
int corePoolSize = Runtime.getRuntime().availableProcessors();
int maximumPoolSize = 2 * corePoolSize;
long keepAliveTime = 60;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory
);
// 使用executor执行任务...
executor.shutdown();
}
}
```
在这个案例中,我们创建了一个自定义线程池`executor`,它使用了CPU核心数作为核心线程数,最大线程数为CPU核心数的两倍,任务队列容量为100。
## 5.2 构建并发安全的数据结构
在多线程环境下,数据的一致性是至关重要的。Java提供了并发集合,但有时我们需要构建自定义的数据结构来满足特定的并发需求。
### 5.2.1 并发集合的原理与选择
Java并发集合主要通过以下几种方式实现线程安全:
- **锁分段技术**:如`ConcurrentHashMap`。
- **无锁设计**:如`ConcurrentLinkedQueue`。
- **读写锁**:如`ReadWriteLock`。
根据业务需求选择合适的并发集合类型是构建并发安全数据结构的关键。
### 5.2.2 实现并发安全队列的步骤与技巧
让我们来构建一个简单的并发安全队列的示例。
```java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class CustomConcurrentQueue<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
private final Object[] items;
private int putPosition = 0, takePosition = 0;
public CustomConcurrentQueue(int capacity) {
items = new Object[capacity];
}
public void put(T element) throws InterruptedException {
lock.lock();
try {
while (putPosition == items.length) {
notFull.await();
}
items[putPosition++] = element;
if (putPosition == items.length) {
putPosition = 0;
}
notEmpty.signal();
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (takePosition == putPosition) {
notEmpty.await();
}
T x = (T) items[takePosition++];
if (takePosition == items.length) {
takePosition = 0;
}
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
```
在上面的代码中,我们定义了一个`CustomConcurrentQueue`类,使用`ReentrantLock`和`Condition`来确保线程安全。
## 5.3 解决并发中的实际问题
在并发编程中,性能瓶颈和问题诊断是常见挑战。理解这些问题并掌握相应的优化策略对于开发高性能应用至关重要。
### 5.3.1 并发应用中的性能瓶颈诊断
性能瓶颈的诊断通常包括以下几个方面:
- **识别瓶颈类型**:CPU、内存、IO等。
- **使用分析工具**:如JProfiler、VisualVM等。
- **分析线程执行情况**:查看线程状态、执行时间等。
### 5.3.2 处理高并发场景下的策略与优化
对于高并发场景,可以采取如下策略进行优化:
- **优化业务逻辑**:减少不必要的计算,优化算法。
- **利用缓存**:使用缓存减少数据库访问。
- **读写分离**:数据库读写分离提高读性能。
- **异步处理**:使用异步编程模型减少阻塞。
通过这些策略,我们可以提高系统的吞吐量和响应速度,适应更高的并发量。
在本章节中,我们通过实战演练,展示了如何设计高效线程池、构建并发安全的数据结构,以及解决并发中的实际问题。通过具体的案例和代码示例,我们不断深入理解并发编程的精髓,为实际应用提供了有力的技术支持。
0
0
复制全文
相关推荐









