✨哈喽,进来的小伙伴们,你们好耶!✨
🛰️🛰️系列专栏:【JavaEE】
✈️✈️本篇内容:了解多线程(初阶)
🚀🚀代码存放仓库gitee:JavaEE初阶代码存放!
⛵⛵作者简介:一名双非本科大三在读的科班Java编程小白,道阻且长,星夜启程!
接着上篇博客我们已经学习了进程的相关概念,了解到进程里面的相关重要属性,那么这篇博客呢我们要学习一个新的知识点——线程!
一、引入进程的目的
首先引入进程这个概念,是为了解决“并发编程”这样的问题。因为CPU 再往小了做,比较困难了,这是因为 CPU 进入了多核心的时代,要想进一步提高程序的执行速度,就需要充分的利用 CPU 的多核资源。
但是呢,多进程编程,已经可以解决并发编程的问题了已经可以利用起来 cpu 多核资源了。
弊端:
进程太重了!(消耗资源多 & 速度慢)。
1、创建一个进程,开销比较大。
2、销毁一个进程,开销也比较大。
3、调度一个进程,开销也比较大。
说进程重,主要就是重在”资源分配/回收。
线程:
所以我们的线程也就应运而生.线程也叫做“轻量级进程"。
目的就是解决并发编程问题的前提下,让创建,销毁,调度的速度,更快一些!
轻的原因:主要是"把申请资源/释放资源"的操作给省下了。
二、多线程的优点
假设小帅开了一家工厂,他在这个工厂里买了一台机器用来生产空调,小帅发现自己的产品销量不错,于是他想加大生产力度,那可以通过什么方法来提高生产效率呢?
方案一:在这个工厂旁边再开一家工厂购买一台机器用来生产空调(多进程的解决方案)
方案二 :在原本的工厂内再购买一台机器用来生产空调(多线程的解决方案)
那么很显然第二种解决方案更划算,场地和空间都是复用之前的,共用一套资源。
线程和进程的关系:
进程包含线程!
只有第一个线程启动的时候,开销是比较大的.后续线程就省事了个进程可以包含一个线程,也可以包含多个线程.(不能没有)。你线程1 new 的对象在线程2,3,4 里都可以直接使用。线程1 打开的文在线程2,3,4 里都可以直接使用。
同一个进程里的多个线程之间,共用了进程的同一份资源(主要指的是 内存 和 文件描述符表)。
注:操作系统,实际调度的时候是以线程为单位进行调度的.
如果每个进程有多个线程了,每个线程是独立在 CPU 上调度的。
线程是操作系统调度执行的基本单位,每个线程也都有自己的执行逻辑 (执行流)。
面试题:进程和线程之间的区别与联系。
1.进程包含线程! 一个进程里面可以有一个线程,也可以有多个线程。
2.进程线程都能解决并发编程问题场景.但是进程在频繁创建和销毁中,开销更高.线程开销更低,(线程比进程更轻量)。
3.进程是系统分配资源(内存,文件资源....) 的基本单位。线程是系统调度执行的基本单位(CPU)。
4进程之间是相互独立的各自有各自的虚拟地址空间,同一个进程内部的多个线程之间,共用同一个内存空间以及文件资源,一个进程挂了其他进程一般都没事。但是一个线程挂了,很可能把整个进程都带走!
三、第一个多线程程序
那么在Java中进行多线程编程的话如何实现呢?
在 Java 标准库中,就提供了一个 Thread 类,来表示/操作线程。
Thread 类也可以视为是 Java 标准库提供的 API。
即创建好的 Thread 实例,其实和操作系统中的线程是一一对应的关系。
操作系统,提供了一组关于线程的 API(C 语言风),Java 对于这组 API 进一步封装了一下,就成了 Thread 类。
一、Thread类的基本用法
写法一:
那么通过Thread类创建线程有很多种方法,其中最简单的就是创建子类继承自Thread,重写run()方法。
class MyThread extends Thread{
@Override
public void run() {
System.out.println("hello Thread");
}
}
注意,run()方法体中描述了这个线程内部要执行哪些代码。因为每个线程都是并发执行的.(各自执行各自的代码)因此就需要告知这个线程,你执行的代码是什么。
run 方法中的逻辑,是在新创建出来的线程中,被执行的代码,意思就是并不是我一定义这个类,一写 run 方法,线程就创建出来了,相当于领导已经把任务安排好了,我还没开始干呢!
public class demo1 {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
}
}
需要调用这里的start方法,才是真正在系统中创建了线程,才开始真正执行上面的run操作。
运行结果:
那么我们再写一个程序来仔细观察一下线程的执行情况,我们这里通过一个while循环来打印一个语句,通过sleep方法来控制打印的速度。
class MyThread2 extends Thread{
@Override
public void run() {
while (true){
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class demo2 {
public static void main(String[] args) {
Thread t = new MyThread2();
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
那么我们知道,在一个进程中,至少会有一个线程。
在一个java 进程中,也是至少会有一个调用 main 方法的线程(这个线程不是你手动搞出来的)。
自己创建的 t 线程和 自动创建的 main 线程,就是并发执行的关系(宏观上看起来是同时执行)。
此处的"并发 = 并行+并发"宏观上是区分不了并行和并发的.都取决于系统内部的调度。
看运行结果:
我们可以发现现在两个线程,都是打印一条,就休眠个1s。
当 1s 时间到了之后,系统先唤醒谁呢?
通过运行结果看起来这个顺序不是完全确定(随机的)。
每一轮,1s 时间到了之后,到底是先唤醒 main 还是 thread,这是随机的,对于操作系统来说,内部对于线程之间的调度顺序,在宏观上可以认为是随机的,即所谓的——抢占式执行!
那么对于这个随机性,会给多线程编程带来很多其他的麻烦!!
写法二:
创建一个类,实现Runnable接口,再创建Runnable实例传给Thread实例,通过Runnable来描述任务的内容。
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello java");
}
}
public class demo3 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
运行结果:
写法三:
也就是上面两种写法的翻版,通过一个匿名内部类来实现:
public class demo4 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("hello javaEE");
}
};
t.start();
}
}
* 通过匿名内部类来实现。
* 创建一个匿名内部类,继承自Thread类。
* 同时重写run方法。
* 同时再new出这个匿名内部类的实例。
运行结果:
写法四:
* 匿名内部类的方法2
* new 的 Runnable,针对这个创建的匿名内部类 同时new出的Runnable实例传给Thread的构造方法。
* 通常认为这种写法好一点 能够做到让线程和线程执行的任务,更好的进行解耦。写代码追求高内聚,低耦合。
* Runnable 单纯的只是描述了一个任务 至于这个任务是要通过一个进程来执行,还是线程池来执行 Runnable本身并不关心。
public class demo5 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello 多线程");
}
});
t.start();
}
}
写法五:
使用lambda表达式来实现!
public class demo6 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello Thread");
});
t.start();
}
}
运行结果:
OK,以上的五种写法要能够全部掌握,大体上都是差不多的,还是需要多复习几遍就能够熟能生巧了!