JavaEE初阶2.0

发布于:2025-08-12 ⋅ 阅读:(20) ⋅ 点赞:(0)

 多线程基础~

目录

一、认识多线程

1.0 线程概念

2.0  图形化展示

二、Thread类以及常见方法

1.0  展示线程运行情况

2.0 创建线程(5种方法)

3.0 Thread类的更多详细信息

4.0 线程方法

(1)  启动一个线程

(2) 终止一个线程

(3) 等待一个线程  

(4)  休眠当前的线程

三、线程的状态


一、认识多线程

1.0 线程概念

线程是什么   

进程整体式一个比较 的概念  创建进程/销毁进程  开销比较大

(尤其是频繁的创建销毁    mysql客户端服务器结构程序 一个服务器同一时刻,要给多个客户提供服务  服务器像是餐馆   客户端则是来吃饭的客人) 

并发编程就是一种解决方式

为了解决上述问题  引入线程(Thread)  轻量级进程(创建销毁的开销更小)

进程和线程的区别

每个进程相当于要执行的一个大任务  每个线程相当于执行的一个小任务(运行的一段代码指令)

线程是轻量化的进程  进程包含着线程

进程是操作系统分配资源的基本单位 

线程是操作系统调度执行的基本单位

进程内部管辖的多个线程之间,会共享上述的  cpu  内存   硬盘资源  网络带宽

进程和进程之间则不是共享  它们是互不干扰的  它们之间的资源是独立的~

一个进程内部的线程之间  很容易相互影响  这个也是学习多线程编程的主要难点 

cpu资源很难用 共享 词来描述 ~  CPU好比是舞台 每个线程就是演员

每个线程都有去舞台表演的机会   状态 优先级 记账信息啥的 每个线程都有这样一份数据

2.0  图形化展示

上述讨论的  线程的基本特点     进程和线程的区别

非常高频的面试题    操作系统这一类问题中 出场频率最高的问题 没有之一

一个人 一个房间  一张桌子  100只烧鸡(初始任务)

哪个效率高点呢?

两个人  两个房间  两张桌子  100只烧鸡(多进程    消耗资源比较“重”)

两个人   一个房间  一张桌子  100只烧鸡(多线程   消耗资源比较“少”)

十个人   一个房间  一张桌子  100只烧鸡(多线程  的弊端 资源互抢 产生bug)

虽然提高线程的数目  能够提升效率  但是并不是线性增长的

线程数目如果太多,线程调度的开销也会非常明显,因为调度开销拖延程序的性能

有时候 一个线程抛出异常  可能就会带走整个程序(如果及时捕获到异常  也不一定导致进程终止

API:应用程序编程接口  里面有别人写过的一些类接口函数   拿来直接就能用

API是广义的概念  操作系统的中会提供标准库 第三方库  其他的各种开源项目  等等都算API

二、Thread类以及常见方法

1.0  展示线程运行情况

jdk的src里面     运行程序之后  选择连接自己的项目  就可以看到进程的详细信息了

调试的时候 控制台也可以看到当前运行的线程列表了

其中有个 堆栈跟踪   是线程的调用栈  获取线程状态的时刻 

2.0 创建线程(5种方法)

法一:继承Thread,重写run

package Thread;

class MyThread extends Thread{
    @Override
    public void run() {
        while(true) {
            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) throws InterruptedException {
        //1.创建Thread类的子类  在子类中重写run方法
        Thread t = new MyThread();
        t.start();//线程开始创建

        while (true) {
            System.out.println("Hello mian");
            Thread.sleep(1000);
        }
    }
}

法二:实现Runnable,重写run

package Thread;

class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            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) throws InterruptedException {
        Runnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
        t.start();

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

Runnable    表示一个   “可以执行的任务”  实际上还是用的Thread

区别在于Thread本身还是借助一下Runnable

解耦合 

让要执行的任务本身 和线程这个概念  能够解耦合

这样后续要变更代码  采用Runnable这样的方案  代码的修改就会更加简单

thread类在Java.lang 包里面就有  不需要我们手动导入 直接用即可

start  创建了一个新的线程   多了一个执行流 能够干活~  这个代码就可以一心两用 同时做事

sleep休眠   让当前的线程暂时放弃cpu  休息一会 时间过了之后再执行 也是thread类提供的方法

是静态方法    毫秒  1000毫秒相当于1秒

每一秒具体打印哪个  也是有变数的 

多个线程  调度顺序,是随机的(抢占式实行)

这两线程,谁先执行,谁后执行,都有可能,无法预测

调用run方法  这个操作没有创建线程,只是直接调用刚才重写的run 

此时 整个进程中,只有一个main 线程   start是真真正正的两个线程

法三:本质上就是方法一  但是使用匿名内部类

Thread t = new Thread(){  } ;

这个创建匿名内部类 做了三件事: 

创建了一个Thread的子类   大括号里面可以编写子类的定义代码(子类要有哪些属性 哪些方法)

创建这个匿名内部类的实例,并且把实例的引用赋值给t

package Thread;

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread() {
            @Override
            public void run() {
                super.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");
            Thread.sleep(1000);
        }

    }
}

法四:使用Runnable,匿名内部类

package Thread;

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

如果有一天 需要把这里的任务  通过其他的方式执行(不使用多线程了) 

就需要把代码进行大规模的调整

当前写的代码   都是hello world  不涉及实际业务逻辑~  (没有解决问题)

法五:针对法三和法四 Lambda表达式

本质上就是一个  匿名函数    主要的用途 就是作为回调函数    ( )-->{ }

Java中,方法必须要依托于  类 存在   叫做函数式接口

创建了一个匿名的函数式接口的子类  并且创建出对应的实例  并且重写了里面的方法

package Thread;

public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        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");
            Thread.sleep(1000);
        }
    }
}

当前最推荐的写法     比较简单

3.0 Thread类的更多详细信息

Thread类的其他属性和方法~~~~

第一个方法的时候  必须重写run   

第二个方法不需要重写run  

第三个和第四个类似  只不过多了一个参数

起名字不影响线程的使用    给线程起名字的意义是方便程序调试   甚至可以取中文名字 

默认也会有名字 但是缺少描述性

main主线程  不是没有 而是已经结束了    main方法结束  主线程结束  比较快  

可能之前的认知是main执行完  程序就结束了   但是以前的认知是针对单线程 

Thread的几个常见属性  

id  java中给每个运行的线程分配id  标识线程身份的效果   类似于PID

后台线程  前台线程 

后台:我在后面偷偷的守护你(对线程没有影响)

           这种jvm自带的线程  他们的存在 不影响 进程结束 (即使他们继续存在  如果进程结束了  他

           们也随之结束了)

前台:main结束了  但是有的线程没有结束    这几个线程的存在 就能够影响到  进程继续存在

这样的线程   前台线程(说了算  能影响进程)

前台线程 后台线程 都是有多个的  如果有多个前台线程  必须得所有的前台线程结束,进程才结束

进程之间存在父子关系   线程之间不存在

IDEA本身也是一个Java进程    在IDEA中运行一个Java代码 通过IDEA进程 又创建一个新的Java进程 (这两进程是有 父子关系)  

4.0 线程方法

(1)  启动一个线程

前面我们通过覆写run方法创建一个线程  但是并不是真正的启动一个线程

覆写run方法 是提供给线程要做的事情的指令清单

创建线程对象可以认为是把李四 王五叫过来了

调用start() 方法  就是喊一声  行动起来 线程才真正去独立执行

start方法  是Java库提供的方法 本质上是调用操作系统的API

private native void start0( );

源码里面关键逻辑是start0()方法    这个start0本地方法  jvm实质上是C++写的

每个Thread对象  都是只能start一次的  一次性的

每次想要创建一个新的线程  都得创建一个新的Thread对象(不能重复利用)

Java代码中创建的Thread对象 和系统中的线程  是一一对应的关系

翻译 非法线程状态异常

(2) 终止一个线程

线程的入口方法执行完毕  线程就随之结束了

只要能够让线程的入口方法执行完毕 (run方法尽快的return)

private static boolean isFininshed(成员变量) = false ;        while(! isFinished) 

如果是局部变量呢?

lambda里面 希望用外面的变量  就会触发  变量捕获 

(lambda里面使用外面的变量)

lambda的执行时机并不是当下    lambda是个回调函数    

执行时机   是很久之后(操作系统真正创建线程之后    才会执行)

很可能  后续栈程创建好了  当前main这里的方法都执行完了 

//对应的isfinished就销毁了  所以不能是局部变量

解决办法

把捕获的变量给拷贝一份  拷贝lambda里面 

外面的变量是否销毁   就不影响lambad里面的执行

拷贝意味着  这样的变量就不适合进行修改   

修改一方  另一方不会随之变化(本质上是两个变量)

Java大佬想的办法是  压根就不允许你修改  final

lambda本质上是函数式接口  相当于一个内部类

isFinished变量本身就是外部类 的成员变量 内部类本身就能访问外部成员变量

线程终止

Java的Thread对象中提供了现成的变量  直接进行判定  不需要自己创建了

Thread.currentThread().isInterrupted()  判定线程是否终止了

t.interrupt()  主动去终止线程

修改boolean变量这个值    除了设置boolean变量之外,还能够唤醒像sleep这样阻塞的方法

如果不加上述break    会出现下面的情况

其实是sleep搞鬼   正常来说  调用interrupt方法就会修改isInterrupted方法内部的标志位(设为true) 由于上述代码把sleep唤醒了   这种唤醒的情况下,sleep就会在唤醒之后,把isInterruptted方法内部的标志位给设置回false   因此在这样的情况下,如果继续执行到循环的条件判定,就会发现能够继续执行~   

可能是sleep这样设定之后,相当于让程序员在catch语句中有更多的选择空间

程序员可以自行决定 咱们这个线程是要立即结束还是等会结束 还是不结束

立即终止就是加上break  如果是啥都不写就是不终止 catch里面加上其他一些代码  就是等会结束

线程的终止权在t线程自己手上  main不能强制让t线程终止

(3) 等待一个线程  

多个线程之间  并发执行 随机调度

join能够要求多个线程之间,结束的先后顺序

比如在主线程中调用t.join()方法  就是让主线程等待t线程结束

区分好谁等谁   在main线程中,调用t.join  效果是让main线程等待t线程结束

当执行到t.join 此时main线程就会“阻塞等待”  一直等到 t线程执行完毕 join才能继续执行

只要t线程不结束 主线程就会一直等待下去~~

Java提供了带参数的版本,指定  超时时间(等待的最大时间

t.join(3000)  如果超过3000  t还没结束  main就不等了 直接往下面走

例子:某个窗口一段时间内没有响应,你再一点,系统就会弹个框问你  要继续等待 还是终止程序

(4)  休眠当前的线程

sleep方法  实际休眠的时间  不一定等于线程休眠的时间

代码调用sleep 相当于让当前线程  让出cpu资源

后续时间到了的时候,需要操作系统内核,把这个线程重新调到cpu上,才能继续执行

时间到 意味着被允许调度了   而不是立即就执行了

sleep(0)   使用sleep的特殊写法    意味着你当前的线程 立即放弃cpu资源

等待操作系统重新调度~~~    

三、线程的状态

进程状态     就绪和阻塞 (操作系统的视角)

6个状态

new  线程对象有了(new了对象)  但是还没有start

timeinated: 内核中的线程结束了  但是therad对象还在

runnable:就绪状态     线程正在cpu上执行  线程随时可以去cpu上执行

time_waiting:sleep   指定时间的阻塞状态  不参与cpu调度,不继续执行了

                                 join()也是会进入到timed_waiting状态

waiting:死等没有超时间的阻塞

blocked:也是一种阻塞,比较特殊的是  由于锁导致的阻塞 

理解这些状态的作用是为了帮助我们调试程序

比如发现的代码中某个逻辑卡死了   第一件事就是用工具查看进程中的所有线程

找到你对应的逻辑的线程是谁   看到waiting状态  怀疑是不是某个代码中某个方法产生了阻塞 没有被及时提醒 看到blocked 怀疑是不是代码出现死锁等等

感谢大家的支持

更多内容还在加载中...........

如有问题欢迎批评指正,祝大家生活愉快、学习顺利!!!


网站公告

今日签到

点亮在社区的每一天
去签到