Java---多线程(一)

多线程(一)

一、认识线程(Thread):揭开并发执行的神秘面纱

1.线程概念

⼀个线程就是⼀个"执行流".每个线程之间都可以按照顺序执行自己的代码.多个线程之间"同时"执行

着多份代码

在操作系统的世界中,进程是一个相当比较重的概念,针对于创建进程/销毁进程的开销都会比较大 (尤其是频繁的创建销毁),所以在90年代的时候并发编程基本是多进程的方式,每一个客户端请求发到服务器上,服务器提供一个进程,给这个客户端进行服务,所以为了解决这个问题,便引入了线程(Thread),轻量级进程(创建销毁的开销更小)
线程(Thread) 是进程内部的 "最小执行单元",所以在进程包含多个线程,每个线程可以执行相关的任务逻辑,从而共同的完成进程的整体任务

线程的核心特性:

  • 独立性:每个线程有专属的程序计数器与栈空间,确保执行逻辑不相互干扰;
  • 并发性:宏观上多个线程 "同时" 执行,微观上单核 CPU 通过时间片轮转实现切换,多核 CPU 可真正并行;
  • 依附性:线程无法独立存在,必须依托进程运行,一个进程至少包含一个主线程(如 Java 程序的 main 线程)。

2.为什么要有线程

因为,并发编程已经成为了刚需,其实也就是解决效率的问题:

  1. 单核CPU的发展遇到了瓶颈.要想提⾼算⼒,就需要多核CPU.而并发编程能更充分利用多核CPU资源
  2. 有些任务场景需要"等待IO",为了让等待IO的时间能够去做⼀些其他的工作,也需要用到并发编程
  3. 进程是操作系统分配资源的最小单位,线程是调度的最小单位。相比进程,线程的创建、销毁与调度成本极低
    • 创建成本:线程无需独立内存空间,仅需少量栈空间(默认 1MB),而进程需分配完整的虚拟内存
    • 销毁成本:线程仅需释放栈资源,进程则需回收代码段、数据段、打开文件等大量资源
    • 调度成本:线程切换无需切换进程地址空间,上下文切换速度更快

若线程仍无法满足高性能需求,可进一步使用线程池 (复用线程,减少创建销毁开销)或协程 (用户态线程,调度成本趋近于零)

协程的话题会在后续进行讲解

3.进程和线程的区别

  1. 进程是包含线程的,每个进程至少有一个线程的存在,即主线程
  2. 进程之间不共享一个内存空间,在同一个进程中的线程之间共享一个内存空间
  3. 进程是系统分配资源的最小单位,线程是系统调度的最小单位
  4. 如果多个线程之间发生了"冲突",那么就会产生bug (线程安全问题)
  5. 一个线程抛出了异常,可能就会带走整个进程,所有的线程都i无法继续进行工作;如果及时捕获到并处理掉,也不一定会导致进程终止

4.Java线程与操作系统线程的关系

线程是操作系统中的概念,操作系统内核实现了线程这样的机制,所以操作系统提供了一些api(Application Programming interface)

  1. 操作系统提供的原生线程api是C语言的
  2. 不同操作系统的线程api也是不同的
    基于此特点,那么Java对于上述内容进行统一封装:Thread类 (标准库中提供的类)
    简单地说:Java线程是程序员可见的"接口",操作系统的线程是真正执行任务的"引擎",两者一一对应。

二、Java多线程的创建(基本)

常用四种大的方式:

  • 继承Thread
  • 实现Runnable接口
  • 实现Callable接口
  • 通过线程池创建线程

我们如何观察多线程呢?

我们可以在Java中自带的jconsole来去观察线程

堆栈跟踪是线程的调用栈,获取线程状态的时刻,线程里的代码执行到哪里了

这节我们主要介绍线程的前两种大的方式的创建,核心是 "定义线程执行逻辑(run() 方法)",不同方式适用于不同场景,以下是四种常用方式的详细解析:

方式 1:继承 Thread 类

  1. 自定义类继承Thread
  2. 重写 run() 方法,编写线程执行逻辑
  3. 创建自定义类实例,调用 start() 方法启动线程。

代码示例:

java 复制代码
class MyThread extends Thread{
    @Override
    public void run() {

        while (true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);//这里会有异常,是运行时异常,只能通过try catch
                //具体原因:因为继承自父类的thread,run这个方法中是没有写异常地抛出和接收,所以重写的该方法也无法去做
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    /*
    这种情况下,如果需要把这里的任务,通过其他是方式去执行(不使用多线程了),
    那么便需要把代码进行大规模的调整
     */
}
public class Demo1 {
    //main方法对应的线程,(就是一个进程至少要包含的那个线程)就是主线程
    public static void main(String[] args) throws InterruptedException{
        //向上转型:
        Thread thread = new MyThread();
        //真正在系统中创建一个线程
        thread.start();
        
        //thread.run();
        //这个thread.run操作,是没有创建线程,只是直接调用刚才重写的run,此时在整个进程中,只有一个main线程,当执行到thread.run
        //这条语句的时候,就会进入到这个方法中,但是由于while是死循环,所以也就无法执行其余代码

        while(true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

start方法 是真正的创建线程,其实就是多了一个执行流,能够干活(一心两用)

这个时候我们可以通过jconsole可以观察到这个线程在执行

java 复制代码
/**
 * 线程写法1:
 * 继承Thread,重写run方法
 * run()方法就是线程执行逻辑任务的入口
 */
class MyThread extends Thread{
    //run相当于线程的入口函数,并不会直接调用,jvm会帮助我们进行调用
    @Override
    public void run() {

        while (true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);//这里会有异常,是运行时异常,只能通过try catch
                //具体原因:因为继承自父类的thread,run这个方法中是没有写异常地抛出和接收,所以重写的该方法也无法去做
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    /*
    这种情况下,如果需要把这里的任务,通过其他是方式去执行(不使用多线程了),
    那么便需要把代码进行大规模的调整
     */
}
public class Demo1 {
    //main方法对应的线程,(就是一个进程至少要包含的那个线程)
    //就是主线程
    public static void main(String[] args) throws InterruptedException{
        //向上转型:
        Thread thread = new MyThread();
        thread.run();

        while(true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}


这个时候我们看jconsole中的连接的,仅有一个main类,说明这个run()方法并没有创建线程

但是我们还会为何还看到打印许多的hello Thread呢?

那是因为thread.run操作,是没有创建线程,只是直接调用刚才重写的run()方法,此时在整个进程中,只有一个main线程,当执行到thread.run这条语句的时候,就会进入到这个方法中,但是由于while是死循环,所以也就无法执行其余代码

Thread.sleep()表示的是sleep()方法,并且是一个静态方法 ,作用是:

休眠;让当前线程暂时放弃cpu资源,并不会释放所持有的锁,等到时间过了之后便会再次执行的状态(或准备执行的状态)

方式 2:实现Runnable接口

  1. 实现Runnable接口
  2. 重写run方法,编写任务逻辑
  3. 创建Runnable实例,再传入给Thread的构造方法
java 复制代码
//自定义任务类(实现Runnable接口)
class MyRunnable implements Runnable{
    @Override
    public void run() {
        while (true) {
            System.out.println("Hello world");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
     	thread.start();//创建线程
        while (true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

Runnable接口本质就是一个"任务载体"
run()方法其实就是一个任务就是一段要执行的逻辑,最终还是要通过Thread,真正创建线程

方式 3:匿名内部类

(3).1:Thread匿名内部类

java 复制代码
public class Demo3 {
    public static void main(String[] args) throws InterruptedException{
        Thread thread = new Thread(){
            //子类的定义
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.start();
        while (true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

Thread thread = new Thread(){ };

1.{ }里面就可以写子类的定义代码,子类里要有属性,要有哪些方法,重写父类的哪些方法...

2.创建了这个匿名内部类的实例,并且把实例的引用赋值给thread,但是我们不知道这个子类的实例的名字是什么,我们不必过多关注于这个

3.(2) Runnable匿名内部类

java 复制代码
public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();

        while (true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

一般如果某个代码是"一次性",就可以使用匿名内部类的写法;
如果需要多次引用这块代码,那么就可以进行单独进行定义的子类

方式 4:Lambda 表达式

lambda表达式,就是对于方式3的进一步改进

lambda其实本质就是一个"匿名函数",最主要的用途,就是作为"回调函数",方法必须依托于类来存在,所以就有了"函数式接口"

java 复制代码
public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while(true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();

        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

总结:

对于Runnable接口的重写,那么降低耦合,就是为了后续改代码方便;如果需要将任务通过其他的方式去执行(不使用多线程),可以用线程池,协程等,那么只需要把 runnable传给它们即可;如果不去使用runnable那么就会需要将代码及逆行大规模的调整;使用Runnable,任务和线程概念是分离的

我们更推荐于写Lambda表达式这种写法,这种方式比较简洁,但是也有不足,逻辑复杂时可读性下降,难以调试

相关推荐
小白学大数据2 小时前
基于 Python 的知网文献批量采集与可视化分析
开发语言·爬虫·python·小程序
Ulyanov2 小时前
PyVista战场可视化实战(一):构建3D战场环境的基础
开发语言·python·3d·tkinter·gui开发
这就是佬们吗2 小时前
力扣---leetcode48
java·笔记·后端·算法·leetcode·idea
冗量2 小时前
Cucumber: 参考
java·bdd·cucumber
冗量2 小时前
Cucumber:参数类型与配置详解
java·bdd·cucumber
qq_338032922 小时前
Vue/JS项目的package.json文件 和java项目里面的pom文件
java·javascript·vue.js·json
霸道流氓气质2 小时前
Java 实现折线图整点数据补全与标准化处理示例代码讲解
java·开发语言·windows
冬奇Lab2 小时前
【Kotlin系列10】协程原理与实战(上):结构化并发让异步编程不再是噩梦
android·开发语言·kotlin
薛不痒2 小时前
项目:矿物分类(训练模型)
开发语言·人工智能·python·学习·算法·机器学习·分类