多线程Thread(初阶一:认识线程)

简介: 多线程Thread(初阶一:认识线程)



一、引用线程的原因

多任务操作系统,希望系统能同时运行多个任务。所以会涉及到进程,需要对进程进行管理、调度等。

而单任务操作系统,就完全不涉及到进程,也不需要管理、调度了。

而进程,就是解决并发编程的这样问题,事实上,进程也能解决大部分并发编程的问题(Java不提倡多进程编程)。但有些情况就很乏力了,如下:

网站 / Web开发,是一种服务器程序,我们知道,一个网站服务器在同一时刻,会受到很多请求,针对这些请求,会创建进程,一个请求创建一个进程,创建完一个进程,又要销毁这个进程,这意味着这个网站服务器要频繁的创建和释放资源,这个操作开销是比较大的,

原因:

我们知道,进程是资源(CPU,硬盘,内存,网络带宽)分配的基本单位,而一个进程,刚启动的时候,首当其冲的就是申请内存资源,因为进程需要把依赖的代码 / 数据,从磁盘加载到内存中。

而从系统分配一个内存,并非是件容易的事,一般来说,申请内存的时候需要指定一个大小,系统内部就要把各自大小的空闲内存,通过一定的数据结构,给组织起来,实际申请的时候,就需要去这样的空间中进行查找,找到一个大小合适的空闲内存,进行分配。

结论:进程在创建和销毁的时候,开销比较大,主要体现在申请和释放资源上。

这时,我们就引入线程,来解决开销比较大的问题。


二、线程的概念

线程也可以理解成“轻量级进程”,基于进程做的一些改进和调整,使其变得开销(资源的申请和释放)不那么大。

因为进程的独立性,一个进程在内存中申请一块资源时,那个块资源只能让那个进程使用,其他的进程不能使用。而一个线程在内存中申请了一块资源,其他不同的线程也可以使用这块资源,这样就避免了多次的资源申请和释放。PCB可以表示进程,也可以表示线程。

进程在内存中的使用范围,如图

PCB有个属性,是内存指针

多线程的PCB,也有内存指针,但可以指的是同一块内存空间,以及进程有的pid、状态、

上下文、优先级等,线程也有。


三、进程和线程的区别

1、进程包含线程,进程可以理解成多个线程的组合,这些线程称为线程组。

关系图如下:

2、进程扮演的角色是申请内存空间,而线程扮演的角色是调度数据 / 执行代码。

3、1个进程至少有1个线程,每个进程有自己的资源空间,而进程里的线程共用这块资源空间。

4、进程和进程之间不会相互影响,但是进程中的某个线程出问题了,可能会影响到这个进程中的其他线程,导致这个进程也出问题。

5、同一个进程中的线程之间,可能会相互干扰,引起线程安全问题。

6、线程不是越多越好,应该要合适,如果线程太多了,调度开销可能非常明显。


四、操作系统的内核

内核,是操作系统最核心的功能模块,操作系统 = 内核 + 配套的应用程序,操作系统大概可以分为两个模块,用户空间(用户态),内核空间(内核态),操作系统理解为银行,银行大概分为两个区域,如图:

办事窗口里面的区域是做一些比较重要的事,一般人都在大堂,只能在大堂里活动,办事窗口里面能做一些核心,重要的事,外面的人也不能闯进来,这时,我们可以理解为内核空间是办事窗口,用户空间是大堂,如图:

那如果用户空间里的程序,想要使用内核资源,需要针对系统内的软硬件资源进行操作,要怎么办呢,这时,Windows操作系统就站出来了,提供系统的api给这些程序使用,调用内核资源,进一步在内核中完成这样的操作。

为什么要划分出内核空间(内核态)和用户空间(用户态)呢?

我们想想,如果一个应用程序可以直接调用硬件资源,如果出现一个bug了呢?不直接把硬件给干烧了,所以,划分出这两模块,主要目的还是为稳定,系统会封装一些api,这些api都是安全的操作,供应用程序使用,就不至于对系统 / 硬件设备造成危害了。


五、多线程编程

1、线程创建的几种方式

(1)、继承Thread类,重写run

一个简单的线程创建,代码如下:

class MyThread extends Thread {
    @Override
    public void run() {
        //run方法是该线程的入口方法
        System.out.println("Hello World");
    }
}
public class SystemCode {
    public static void main(String[] args) {
        //2、根据刚才的类,创建一个实例
        Thread t = new MyThread();
        //3、调用Thread的start方法,才会真正调用系统的api,在系统内核中创建线程
        t.start();
    }
}

执行结果:

(2)、实现Runneble接口,重写run

代码如下:

class MyThread implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("Hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyThread());
        t.start();
        while (true) {
            System.out.println("Hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行效果:

要怎么查看进程的情况呢?

1、使用jdk自带的jconsole.exe文件。

看到这里有这么多的线程,我们发现,其实一个java的进程,包含的线程也挺多的。

这里除了main线程和我们自己创建的线程,其余的线程,都是 jvm 自带的线程,用来完成垃圾回收(gc自动帮你释放内存),监控统计各种指标(如果我们的代码出问题了,这些指标就可以给我们提供一些参考和线索),把统计指标通过网络的方式,传输给其他程序。

堆栈跟踪信息:是很有意义的,描述了线程的调用栈,线程里当前执行道路哪个方法的第几行代码了,这个方法是怎么一层一层调用过去的。

和我们所写的代码行序是对应的

2、使用idea查看线程

debug调试我们的代码,然后打断点到这:

这有个下拉窗,我们就可以看到现有的线程了:

(3)继承Thread类,重写run,但使用匿名内部类

代码:

public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        t.start();
        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

执行效果和上面的一样

说明:内部类的最大用途,也就是匿名内部类,没有名字,意味着不能重复使用,用一次就不要了。new Thread()后面 { }里的意思是要定义一个类,这个类继承自Thread,此处的 { } 中可以定义子类的属性和方法,此处的 { } 最主要目的就是重写run方法。

这个代码还创造出了子类的示例

       

这里的 t 指向的实例,并非单纯的Thread,而是Thread的子类,因为我们不知道这个子类叫啥,因为是匿名的。

(4)、实现Runneble接口,重写run,使用内部类

代码:

public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t.start();
        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

注意:匿名内部类是在()里面的。

(5)使用lambada(推荐)

代码:

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

注意:lambda表达式来代替功能接口,所以要代替的是实现接口的功能,重写的是run方法,所以要写在Thread()的括号里面。


六、star()和run()的区别

看到这,是不是觉得对这两个区别优点疑惑?但事实上,这两个概念是毫不相干的两个东西,创建出来的线程实例,只有调用 star,才会真正调用系统的api,在系统内核创建出线程;而 run 是线程执行的入口。这里,就有老铁疑惑了,我用 run 也可以调用这个重写的方法啊,和用 start 一样,哎,这就不对了,举个以下例子:

两个代码分别单独执行star 和 run

代码:

run:

class MyThread extends  Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("这是我的线程,正在工作");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.run();
        //t.start();
    }
}

start:

class MyThread extends  Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("这是我的线程,正在工作");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        //t.run();
        t.start();
    }
}

执行效果:

这两代码的执行效果是一样的,但我们现在改一下,如下:

run:

class MyThread extends  Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("这是我的线程,正在工作");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.run();
        //t.start();
        while (true) {
            System.out.println("这是主线程,正在工作");
            Thread.sleep(1000);
        }
    }
}

start:

class MyThread extends  Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("这是我的线程,正在工作");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        //t.run();
        t.start();
        while (true) {
            System.out.println("这是主线程,正在工作");
            Thread.sleep(1000);
        }
    }
}

分别执行效果:

run:

start:

可以看到不同了吧,这是因为star之后,这里有两个线程(t 线程和主线程),他们都是各自调度各自的线程,就有两个循环在执行了,但是如果使用 run 的话就只有一个线程了,只在一个循环里执行,所以之后在当前方法里面循环,不会执行下面的循环。


都看到这了,点个赞再走吧,谢谢谢谢谢!!!

相关文章
|
9天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
11 3
|
9天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
10 2
|
9天前
|
Java
在Java多线程编程中,实现Runnable接口通常优于继承Thread类
【10月更文挑战第20天】在Java多线程编程中,实现Runnable接口通常优于继承Thread类。原因包括:1) Java只支持单继承,实现接口不受此限制;2) Runnable接口便于代码复用和线程池管理;3) 分离任务与线程,提高灵活性。因此,实现Runnable接口是更佳选择。
22 2
|
9天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
19 2
|
9天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
20 1
|
25天前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
38 1
C++ 多线程之初识多线程
|
9天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
21 1
|
9天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
17 1
|
2月前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
25天前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
42 6