多线程基础(保姆级教程)

本文详细介绍了Java中的多线程概念,包括如何通过继承Thread类和实现Runnable接口创建线程,以及start和run方法的区别。还探讨了Runnable接口的优势,匿名内部类和lambda表达式的使用,以及Tread类中的重要方法如线程ID、名称、后台线程、中断和join等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言:当我们了解进程和线程之后,那就让我们来认识一下什么是多线程吧。


一、创造多线程

1.用Tread类创造线程

方式如下:

public class Mythread extends Thread{

        public void run(){
            while(true){
                System.out.println("hello thread");
            }
        }
}

在类中继承我们的Tread类,重写我们的run方法就能再次创建一个线程了。

⭐️重点解析:

①创造了一个类之后继承Tread类之后,重写run方法是多态的体现

②然而这个run方法也是这个线程入口标志。

🔆这个入口的标志就类似于主方法main()的入口标志。要运行一个java程序,必须有一个进程,进程中最少有一个线程吧。所以在java程序中,main函数也就是一个(主)线程,它是jvm自动自己创造出来的线程。所以主函数线程的入口就是main()方法,run()方法就也是线程的入口啦~

2.调用/启动线程

(1)run

当我们创造出Tread类的时候就有了一个线程,当我们new出这一对象这有了这一线程。那我们应该如何启动这个线程呢?

有人会说直接调用run方法就好了啊。

很好,如果你只调用run方法,只能证明你学过javase。但这并不是真正启动线程的方式!上面已经提及到run方法只是一个入口(描述的是一个任务)当你调用run方法时只能按代码顺序依次执行,当你在run方法加入一个死循环时就再也执行不到main线程里的指令了。

(2)start

所以我们该怎么样才能同时执行到这两个线程或者多个线程呢?

这时候就要用到这个start()方法了。当我们调用到t.start()这个方法时,它就能启动t的这个线程,同时执行下面的主线程。

public static void main(String[] args) {
        Mythread t1 = new Mythread();
        t1.start();
        while(true){
            System.out.println("hello main");
        }
}
    
public class Mythread extends Thread{

        public void run(){
            while(true){
                System.out.println("hello thread");
            }
        }
}

af906cfb3b294743a4fe81a8f5c3b5f8.png

⚠️这里打印的顺序都是随机的,并不是固定的。这是因为多个线程之间的调度顺序是随机的,所以不一定这次执行这个线程下次执行另一个线程。每次每个线程都是随机执行的。

(3)run和start区别

run:描述的只是一个任务,一个入口

start:调用的系统的API,系统内核创造线程


二、Runnable

1.用类实现Runnable接口

上面介绍了继承Tread类创建线程,那我们就来学第二种创建线程的方式实现Runnable接口。

 class MyRunnable implements Runnable {
    public void run() {
        while (true) {
            System.out.println("Hello from MyRunnable");
        }
}

可以看到和Tread类实现方式大差不差。主要区别是通过实现接口的方式来继承类。这样可以更便于我们向上转型的使用。

三、Tread和Runnable

同:

要非说相同点那就都是同在java.lang包下面了。都不需要导包。

区别:

①Tread是继承的关系,它只能单一的继承,不能多个继承。

②Runnable是实现接口的关系,多态是最大的体现。

⭐️⭐️⭐️最大区别再于:解耦合。因为Runnable是一个函数式接口,在Runnable中run方法可以表示为一个可执行程序,可以设置线程和开启线程,任务只是一个任务,启动时可以启动。各司其职,肯定选择第二种方式创建线程更好!由于第一种方式run方法也不是函数式接口最多也就只能用匿名内部类的方式创建,不能用lamdba表示了

四、匿名内部类和lambda

当然我们用匿名内部类和lamdba表达式创建是最简单不过的了

public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                while(true){
                    System.out.println("匿名类内部类创建多线程");
                }
            }
        };
        t1.start();
        while (true){
            System.out.println("主线程");
        }
    }
public static void main(String[] args) {
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("Runnable匿名内部类创建线程");
                }
            }
        });
        t2.start();
        while (true){
        System.out.println("主线程");
    }

ps:注意两者细小的区别

1.Runnable匿名内部类后面有一个小反括号}

2.构造完任务需要一个分号;

 public static void main(String[] args) {
        Thread t3 = new Thread(()->{
                while(true){
                    System.out.println("lamdba表达式创建");
                }
            });
        t3.start();
 }

lamdba表达式后面只需要跟要执行的任务内容就可以了,不需要再重写run方法表示方法的入口。因为lamdba表达式本来就是一种函数式接口,{ }内部就表示一种逻辑。 

所以只有Runnable能实现lamdba表达式,因为它实现的Runnable函数接口的模式;而继承Thread的类就不行了。

五、Tread类中的方法

以下介绍几个重要一点的方法

下面几个构造方法中

1.id

public static void main(String[] args) {
        Thread t3 = new Thread(()->{
        });
        System.out.println(": ID: " + t3.getId());

        t3.start();
        }

id往往是java提供的id 

2.name

public static void main(String[] args) {
        Thread t3 = new Thread(()->{
        });
        System.out.println(": name: " + t3.getName());

        t3.start();
        }

线程中的名字呢往往都是用jconsole调试观察线程

3.后台线程

public static void main(String[] args) {
        Thread t3 = new Thread(()->{
        });
        System.out.println(t3.isDeamon());
        t3.setDeamon();//也可以设置为后台线程
        t3.start();
        }

与之对立的就是前台线程。两者最主要的区别呢就是后台线程不会影响程序的结束,是一个比较独立的线程;而前台线程呢就会影响进程的结束,也就是前台线程结束了,进程也就结束了。

4.存活

public static void main(String[] args) {
        Thread t3 = new Thread(()->{
        });
        System.out.println(t3.isAlive());
        t3.start();
        }

一般来说线程的周期是大于我们内核线程的周期的。

5、sleep

public static void main3(String[] args) {
        Thread t3 = new Thread(() -> {
        });
        while (true) {
            System.out.println("hello");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

sleep是使线程处于休眠状态,是一个类方法,通过输入的数字决定线程休息多少秒。
sleep()一般单位为ms。1000ms=1s。也可用于更好的观察线程的运行。

一般使用sleep都会有报错,我们只需要将这个错误抛出,或者抓住就好了。

六、线程终止interrupt

1.定义

interrupt是终止线程、停止线程的意思。

2.引入interrupt

怎么让一个线程停止呢?一个线程靠run来执行,run执行结束了线程就结束了。所以现在我们只需要让循环提前结束就好了。

第一种方法我们可以手动设置一个标志物,设置一个成员变量,然后在第五秒修改为fales结束循环。

public class demo3 {
    public static boolean isQuite = false;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            while (!isQuite) {
                System.out.println("线程工作ing");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("线程工作结束");
        });
        t.start();
        System.out.println("主线程启动");
        Thread.sleep(5000);
        isQuite = true;
    }
}

384e0675cb75495a8c333ea26d3ee24d.png

由此可见这种方法是可以的 ,但也有缺陷:需要自己手动设置太麻烦;不能在进入sleep的时候结束循环。

3.interrupt

🌰

    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while ((!Thread.currentThread().isInterrupted())) {
                System.out.println("正在工作ing~");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();;
                }
            }
        });
        t.start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();//停止线程
}

我们可以用Tread.currentTread()获取到t这个对象 然后.IsInterrupt()调用方法。当你想终止的时候就可以t.interrupt终止了。

c2d08ec7afa64b25a750700cf3ba4366.png这里终止是终止了,但是另外一个问题又出现了。当我在sleep的过程的抛出了一个异常没错,但是为什么又继续循环了呢?这是因为当你在sleep抛出异常的时候,它会清除你设置的标志位又变成true进入循环开始打印了。所以解决方法就是:①你可以继续执行②你可以在后面break结束循环③或者主动抛出异常处理其他业务。所以这种情况就给我们带来了很大的便利性。而这也仅仅是在唤醒sleep报异常的时候做的处理。

76e8ae4042864aac971a217c6a460e96.png

ps:如果把isquite设成局部变量是否能行呢?这就涉及到lamdba表达式里面的内容了。

答案是肯定不行的,因为变量捕获的原因,内部的那一份变量属于复制的变量,两者是完全不一样的。当要修改常量时当然三是不行的啦~

七、线程等待join

1.定义:

线程等待顾名思义就是让一个线程等待另一个线程结束

栗子

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i<5; i++) {
                System.out.println("t线程正在执行");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        System.out.println("主线程开始执行");
        t.join();
        System.out.println("主线程结束");
    }

在上面栗子中就是让main线程等待t线程结束再结束

这里敲黑板注意了:谁被调用谁被等待。在main方法里调用t.join;就是main线程等待t线程执行结束。

2.作用:

线程等待呢最主要的作用是:控制线程结束时的顺序

八、线程状态

线程一般有下面几种状new,runnable,waiting,timed-wait,terminated,blocked,具体请看大荧幕

681862c2ab584173ad35842c820c3aec.png

看懂这张图你也就大成不就了。虽然挺复杂的,但是挺简单的。

1.就绪

Runable

在cpu上的执行、运行状态或者是准备运行、等待(就绪)的状态。

2.堵塞

一般分为三种:

(1)sleep

在线程上休眠,不占用cpu内存空间。

(2)wait

(3)锁

   后续线程安全讲~

3.终端

trminated

意味线程运行终止运行结束。

 

<think>嗯,用户现在想要一个关于文件树扫描的保姆教程,详细解释。我需要先回顾之前的对话历史。用户之前的问题是关于在Android虚拟驱动中实现PDF文件扫描和浏览,涉及到文件系统操作、虚拟驱动层、文件扫描层、渲染层和系统集成。现在用户的新问题是在文件树中进行扫描的详细教程,特别是保姆的,所以需要分步骤详细说明。首先,用户提到了参考的引用内容,但那些引用是关于YOLOv5、StableDiffusion部署的,可能和当前问题无关。不过用户可能希望我参考之前的回答结构,比如分步骤、代码示例、相关引用等。之前的回答结构包括分点、代码块、表格对比、引用标注,所以这次也需要保持类似结构。 用户的问题是关于文件树扫描,可能涉及文件系统遍历、递归扫描、性能优化等。需要先定义问题,然后分步骤讲解,包括环境准备、核心实现、优化策略、系统集成、测试验证等。同时要提供代码示例,比如Python或Java的递归扫描代码,可能还要提到多线程、异步处理、索引优化等。 需要注意用户之前提到的Android虚拟驱动中的文件扫描,可能这次的文件树扫描是更通用的,不特定于Android。但可能需要结合之前的上下文,比如在文件系统中如何高效扫描,使用B+树索引等。不过用户这次的问题更基础,是寻找文件树扫描的教程,所以应该从基础开始,逐步深入。 需要确保使用正确的LaTeX格式,行内公式用$...$,独立公式用$$...$$。例如,在讲解时间复杂度时,可以用$O(n)$。同时,引用之前提到的B+树索引优化,可能需要添加引用标记[^2],因为之前的回答中提到过B+树索引结构。 可能还需要考虑不同编程语言的实现,比如Python的os.walk,Java的Files.walk等,给出示例代码。同时,性能优化部分可以包括多线程、缓存机制、实时监控等,比如使用FileObserver在Android中监听文件变化。 用户可能需要了解如何避免权限问题,特别是在系统扫描时,比如Android的Scoped Storage限制,但这个问题可能更偏向于通用文件树扫描,所以可能需要分平台讨论,或者以通用方法为主,再提到特定平台的注意事项。 最后,生成相关问题,比如如何优化递归性能、处理符号链接、实时监控的实现等,这些问题需要与教程内容相关,帮助用户进一步学习。总结,教程结构应包括:概述、环境准备、核心实现(递归、非递归)、优化策略(多线程、缓存、索引)、系统集成(如数据库、监控)、测试验证、常见问题。每个部分提供代码示例和解释,引用之前的相关内容如B+树索引,并正确使用LaTeX格式。</think>### 文件树扫描保姆教程 #### 一、核心概念 文件树扫描是通过递归遍历文件系统层结构,获取目录及子目录下所有文件的元数据(如路径、大小、类型等)的过程。其时间复杂度为$O(n)$(n为文件总数),空间复杂度取决于遍历方式[^1]。 #### 二、环境准备 1. **Python示例环境** ```bash pip install psutil # 用于获取文件元数据 ``` 2. **Java示例环境** ```xml <!-- Maven依赖 --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency> ``` #### 三、核心实现 1. **递归扫描法** Python实现(深度优先): ```python import os def scan_dir(path): for entry in os.scandir(path): if entry.is_dir(): yield from scan_dir(entry.path) else: yield entry.path # 示例:扫描/home/docs目录 for file in scan_dir("/home/docs"): print(file) ``` 2. **非递归扫描法** Java实现(广度优先): ```java import org.apache.commons.io.FileUtils; import java.io.File; import java.util.Queue; import java.util.LinkedList; public class FileScanner { public static void scan(String rootPath) { Queue<File> queue = new LinkedList<>(); queue.add(new File(rootPath)); while (!queue.isEmpty()) { File current = queue.poll(); if (current.isDirectory()) { File[] children = current.listFiles(); if (children != null) { for (File child : children) { queue.offer(child); } } } else { System.out.println(current.getAbsolutePath()); } } } } ``` #### 四、优化策略 1. **多线程扫描** 对目录层进行分块并行处理: $$ \text{加速比} = \frac{T_{\text{串行}}}{T_{\text{并行}}} \leq \frac{1}{1 - \alpha + \alpha/n} $$ 其中$\alpha$为可并行化代码比例,$n$为线程数[^2] 2. **缓存机制** 使用LRU缓存存储高频访问目录结构: ```python from functools import lru_cache @lru_cache(maxsize=256) def get_cached_dir(path): return list(os.scandir(path)) ``` #### 五、系统集成 1. **数据库存储** 将扫描结果存入SQLite: ```sql CREATE TABLE file_metadata ( id INTEGER PRIMARY KEY, path TEXT UNIQUE, size INTEGER, last_modified TIMESTAMP ); ``` 2. **实时监控** 使用`watchdog`库实现文件变化监听: ```python from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler class MyHandler(FileSystemEventHandler): def on_modified(self, event): print(f"文件变更: {event.src_path}") observer = Observer() observer.schedule(MyHandler(), "/target/path", recursive=True) observer.start() ``` #### 六、测试验证 1. **性能基准测试** ```bash # Python性能分析 python -m cProfile -s cumtime scanner.py ``` 2. **正确性检查** ```python # 验证文件总数 assert len(list(scan_dir("/path"))) == find /path -type f | wc -l ``` #### 七、常见问题 1. **符号链接处理** 需明确是否跟随链接: ```python os.scandir(path, follow_symlinks=True) # 慎用,可能导致循环 ``` 2. **权限问题** 添加异常处理: ```java try { Files.walk(Paths.get("/")).forEach(path -> { /*...*/ }); } catch (AccessDeniedException e) { System.err.println("权限拒绝: " + e.getFile()); } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值