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表达式这种写法,这种方式比较简洁,但是也有不足,逻辑复杂时可读性下降,难以调试

相关推荐
Scott9999HH4 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔4 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区8 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大8 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai9 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说9 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json