目录

JavaEE初阶Day 3:多线程(1)

目录

  • [Day 3:多线程(1)](#Day 3:多线程(1))
    • [1. 线程](#1. 线程)
      • [1.1 引入线程的原因](#1.1 引入线程的原因)
      • [1.2 线程的定义](#1.2 线程的定义)
      • [1.3 为何线程更轻量](#1.3 为何线程更轻量)
      • [1.4 问题](#1.4 问题)
    • [2. 多线程代码](#2. 多线程代码)
      • [2.1 继承Thread重写run](#2.1 继承Thread重写run)
      • [2.2 通过实现Runnable接口创建线程](#2.2 通过实现Runnable接口创建线程)
      • [2.3 针对2.1的变形使用匿名内部类](#2.3 针对2.1的变形使用匿名内部类)
      • [2.4 针对Runnable创建匿名内部类](#2.4 针对Runnable创建匿名内部类)
      • [2.5 使用lambda表达式](#2.5 使用lambda表达式)

Day 3:多线程(1)

C++会对进程有更进一步介绍,例如:如何通过编写代码,来进行进程的控制(多进程编程),但是Java并不太关注这些

  • JVM没有提供上述多进程编程的api
  • Java生态中也不太鼓励使用多进程编程

JVM也不是完全没有,也提供了非常粗糙的多进程操作的api,但是控制过程不如C++通过系统原生api更精细

1. 线程

1.1 引入线程的原因

当前的CPU都是多核心CPU,需要通过一些特定的编程技巧,把要完成的任务,拆解成多个部分,并且分别让他们在不同的CPU核心上运行,也就是**"并发编程"**

  • 通过多进程编程 的模式,其实就可以起到"并发编程"的效果,因为进程可以被调度到不同的CPU上运行,此时就可以把多个CPU核心都给很好的利用起来,虽然,多进程编程可以解决上述问题,也带来了新的麻烦
  • 在服务器开发中,并发编程的需求场景非常常见,所以一个服务器要能够同时给多个客户端提供服务,如果同一时间,来了很多客户端,服务器如果只能利用一个CPU核心工作,速度就会比较慢
  • 一种典型的做法:每个客户端连上服务器了,服务器都创建一个进程,给客户端提供服务,这个客户端断开了,服务器再把进程给释放掉,如果这个服务器,频繁的有客户端来来去去,服务器就需要频繁创建/销毁进程

所以引入线程,来解决上述进程"太重量"的问题

1.2 线程的定义

线程(thread),也称为"轻量级进程",创建和销毁的开销更小 ,线程可以理解成"进程的一部分",一个进程中可以包含一个线程或者多个线程,描述进程,使用PCB这样的结构体,事实上,更严格地说,一个PCB其实是描述一个线程的,若干个PCB联合在一起,是描述一个进程的

PCB:pid(每个线程都不一样)、内存指针、文件描述符表、状态、上下文、优先级、记账信息、tgid(同一个进程的tgid是同一个)

同一个进程的若干个线程,是共用相同的内存资源和文件资源的,这里的内存指针和文件描述符表其实是同一个,但是每个线程都是独立在CPU上调度执行

  • 进程是系统分配资源的基本单位
  • 线程是系统调度执行的基本单位

引入线程后,就可以每个客户端分配一个线程来处理,起到优化效果

1.3 为何线程更轻量

为什么线程比进程更轻量/为什么说线程创建和销毁的开销比进程更小

核心在于,创建进程,可能要包含多个线程,这个过程中,涉及到资源分配/资源释放,创建线程,相当于资源已经有了,省去了资源分配/资源释放步骤了,同一个进程包含N个线程,这些线程之间是共用资源的,只有创建第一个线程(也是创建进程的时候),去进行资源申请操作,后续再创建线程,都没有申请资源的过程了

1.4 问题

  • 线程不能无限引入:总的线程越多,单位时间内要进行调度的次数也越多,调度消耗的系统资源自然就更多了,这个时候,线程调度开销就会非常明显,程序的性能可能不升反降
  • 线程安全问题:多个线程之间可能产生冲突
  • 如果一个线程抛出异常,并且没有很好的捕获处理好,就会使得整个进程退出,多线程编程值得关注的难点:一个线程出现问题,会影响到别的线程

2. 多线程代码

线程本身是操作系统提供的,操作系统提供了api让我们操作线程,JVM就对操作系统api进行了封装,Java中提供了Thread类,表示线程

2.1 继承Thread重写run

  • public void run():run只是描述了线程要干啥任务,run不是start调用的,是start创建出来的线程,线程里被调用的

  • t.start();Thread类中自带的方法,调用操作系统提供的"创建线程"api,在内核中创建对应的PCB,并且把PCB加入到链表中,进一步的系统调度到这个线程之后,就会执行上述run方法中的逻辑

像run这种方法,只是定义好,而不用去手动调用,把这个方法的调用,交给系统/其他的库/其他的框架(别人)调用这样的方法(函数)称为**"回调函数"**(callback function)

java 复制代码
package thread;

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

上述代码中有两个线程

  • t 线程
  • main方法所在的线程(主线程):JVM进程启动的时候,自己创建的线程

JDK中包含了jconsole工具,通过这个工具可以更直观地看到内部进程的情况,里面除了main与t线程,剩下的线程,都是JVM帮我们做的一些其他工作,有的是负责垃圾回收的,有的是负责记录调试信息的

Thread.sleep(1000);:让线程主动进入"阻塞状态",主动放弃去CPU上执行,时间到了之后,线程才会接触阻塞状态,重新被调度到CPU上执行,加上sleep就让CPU消耗的资源大幅度降低了,不加入sleep,消耗CPU资源将会特别大,while循环太快了

未来实际开发中,如果服务器程序消耗CPU的资源超出预期,如何排查

  • 先确认是哪个线程消耗的CPU比较高,未来会涉及到到第三方工具,可以看到每个线程的CPU的消耗情况,确定了之后,进一步排查,线程中是否有类似的"非常快速"的循环
  • 确认清楚,这里的循环是否应该这么快,如果应该,说明需要升级更好的CPU,如果不应该,说明需要在循环中引入一些"等待操作"(不一定是sleep)

上述代码补充说明

  • 每秒钟打印一次,每一秒打印的时候,可能是main在前面,也可能是thread在前面
  • 多个线程的调度顺序,是无序的,在操作系统内部称为**"抢占式执行"**,任何一个线程,在执行到任何一个代码的过程中,都可能被其他线程抢占掉它的CPU资源,于是CPU就给别的线程执行了
  • 这样的抢占式执行,充满了随机性,正是这样的随机性,使多线程程序的执行效果也会难以预测,甚至可能会引入bug
  • 主流的系统(Linux、Windows)都是属于这种实现方式,也有一些小众的系统(实时操作系统),通过"协商式"进行调度,虽然牺牲了很多功能,换来了调度的实时性

2.2 通过实现Runnable接口创建线程

  • Runnable的作用 ,是描述了一个"任务",这个任务和具体的执行机制无关 (通过线程的方式执行,还是通过其他的方式执行),run 就是要执行的任务内容本身了

  • 引入Runnable就是为了解耦合 ,未来如果要更换其他的方式来执行这些任务,改动成本比较低,把任务内容和线程 这个概念给拆分开了,这样的任务,就可以给其他的地方来执行

java 复制代码
package thread;
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class Demo2 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
}

2.3 针对2.1的变形使用匿名内部类

java 复制代码
package thread;

public class Demo3 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };


        t.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

此处的new Thread()

  • 创建了一个Thread的子类(不知道啥名字,匿名)
  • 同时创建了一个该子类的实例:对于匿名内部类来说,只能创建这一个实例,之后再也拿不到这个匿名内部类了
  • 此处的子类内部重写了父类的run方法

2.4 针对Runnable创建匿名内部类

java 复制代码
package thread;

public class Demo4 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });

        t.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

此处匿名内部类,只是针对Runnable,和Thread没有关系,只是把Runnable的实例,作为参数传入到了Thread的构造方法中

  • 创建新的类,实现Runnable,但是类的名字是匿名的
  • 创建了这个新类的实例(一次性)
  • 重写run方法

2.5 使用lambda表达式

lambda本质上就是针对匿名内部类的平替

java 复制代码
package thread;

public class Demo5 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
            for (int i = 0; i < 5; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });


        t.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
唐人街都是苦瓜脸7 分钟前
SpringBoot的简单介绍
java·spring boot·后端
2401_8742751712 分钟前
微服务。1 微服务
java·数据库·微服务
菜鸟起航ing24 分钟前
【Java面试系列】Spring Boot微服务架构下的分布式事务处理与Seata框架实现原理详解 - 3-5年Java开发必备知识
java·spring boot·微服务·seata·分布式事务
xq51486327 分钟前
Spring Boot 自动装配原理
java·spring boot·后端
网安小陈32 分钟前
【网安】处理项目中的一些常见漏洞bug(java相关)
java·开发语言·数据库·安全·web安全·网络安全·bug
xinxiyinhe1 小时前
Github最新AI工具汇总2025年4月份第2周
人工智能·github
咖啡里的茶i2 小时前
随机产生4位随机码(java)
java
小小鸭程序员2 小时前
在Spring Boot中实现图片上传和修改
java·数据库·python·mysql·spring
凸头3 小时前
juc并发包的常用类、线程安全实现方式、锁机制及 JVM 优化策略
java
lzq6033 小时前
5分钟上手GitHub Copilot:AI编程助手实战指南
github·copilot·ai编程