【JavaEE】多线程01

张开发
2026/5/4 9:56:03 15 分钟阅读
【JavaEE】多线程01
1.为何需要线程通过多进程的方式可以实现 并发编程 的效果但是进程是一个比较重的概念在创建或者销毁一个进程的时候开销都比较大尤其是在需要频繁创建进程的时候。(关于进程/CPU/操作系统/进程调度 等知识请看https://blog.csdn.net/Zzzzmo_/article/details/159430255?spm1001.2014.3001.5502)为了解决这个问题就有了线程(Thread)这个概念它是 轻量级的进程 即创建销毁的开销更小且调度线程比调度进程速度更快。⼀个线程就是⼀个 执行流。每个线程之间都可以按照顺序执行自己的代码多个线程之间 同时 执行着多份代码。举例理解线程⼀家公司要去银办理业务既要进行财务转账又要进行福利发放还得进行缴社保。如果只有张三⼀个会计就会忙不过来耗费的时间特别长。为了让业务更快的办理好张三⼜找来两位同事李四、王五⼀起来帮助他三个⼈分别负责⼀个事情分别申请⼀个号码进行排队自此就有 了三个执行流共同完成任务但本质上他们都是为了办理⼀家公司的业务。此时我们就把这种情况称为多线程将⼀个大任务分解成不同小任务交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的所以张三⼀般被称为主线程Main Thread。2.进程和线程的区别每个进程相当于一个要执行的任务(运行的一段代码命令)而每一个线程也是一个要执行的任务。进程是包含线程的每个进程至少有⼀个线程存在即主线程(一个进程主线程)。进程创建时需要申请资源销毁时需要释放资源而对于线程来说只需要第一个线程创建时(和进程一起创建的时候)才需要申请资源后续再创建不涉及资源的申请操作(干的事少快)只有当所有线程都销毁(进程销毁)才真正释放了资源在运行过程中销毁某个线程不会释放资源。进程是操作系统资源分配的基本单位如分配CPU、内存、硬盘等的资源进程内部管辖的多个进程之间会共享这些资源也就是说进程内部的线程之间容易相互影响(线程不安全)而进程和进程之间所涉及到的资源则是各自独立的彼此之间互不干扰。进程是操作系统资源分配的最小单位而线程则是操作系统调度的基本单位CPU 最终切换和运行的是线程不是进程。系统调度 操作系统决定现在让谁在 CPU 上跑、跑多久、什么时候换下一个。所以进程调度其实就是线程调度。⼀个进程挂了⼀般不会影响到其他进程但是⼀个线程挂了可能把同进程内的其他线程⼀起带⾛(整 个进程崩溃)。线程之间容易相互影响那么就有可能发生冲突当一个线程抛出异常时可能带走整个进程所有线程都无法继续工作但是如果及时捕获这个异常处理掉也不一定导致进程终止。一句话核心进程太重、切换太慢、资源浪费线程轻量、共享资源、切换快能让程序 “同时干多件事还不卡顿”。3.Java的线程 和 操作系统线程 的关系线程是操作系统中的概念操作系统内核实现了线程这样的机制并且对用户层提供了一些 API 供用户使用。Java 标准库中Thread 类可以视为是对操作系统提供的 API 进行了进⼀步的抽象和封装。什么是 APIAPI 即 Application Programming Interface - 应用程序编程接口就是别人写好的可以直接调用的功能 / 方法 / 接口 / 类不用管内部怎么实现只要知道1.叫什么名字2.传什么参数3.能得到什么结果。4.创建线程如下图当创建一个Thread 对象时并没有像ArrayList类一样自动导入包说明Thread类是在 java.lang 包下的一个类默认import。创建一个 Thread 线程的方法有两种。方法一继承 Thread 类创建一个线程类创建一个类继承时需要重写 run() 方法。方法二实现 Runnable 接口创建一个类实现 Runnable 接口创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为参数。(这种写法能够更好的解耦合)方法三匿名内部类创建 Thread 子类对象该方法本质上就是 方法一 只是将方法一的形式使用匿名内部类的方式创建。方法四匿名内部类创建 Runnable 子类对象该方法本质上就是 方法二 只是将方法二的形式使用匿名内部类的方式创建。方法五lambda 表达式创建 Runnable 子类对象这种方法比 方法四 更简洁。5.启动线程 start()之前我们已经看到了如何通过覆写 run 方法创建⼀个线程对象但线程对象被创建出来并不意味着线程就开始运行了。覆写 run 方法是提供给线程要做的事情的指令清单线程对象可以认为是把李四、王五叫过来了而调用 start() 方法相当于让李四王五行动起立线程才真正独立去执行了只有调用Thread 的 start 方法,才真的在操作系统的底层创建出⼀个线程.(JVM调用操作系统的API 完成线程创建操作 —— start 是 Java 标准库/JVM 提供的方法本质上是调用操作系统的API)如果只是调用了重写的run 方法(Thread/Runnable的方法)并没有启动线程只是执行调用这时整个进程中就只有main这个线程。(run是线程入口方法新的线程启动了就要执行这里的代码并不需要自己手动调用新的线程创建好之后自动去执行相当于回调函数)启动线程后除了 main 这个主线程外又多了一个线程感受多线程程序和普通程序的区别每个线程都是⼀个独立的执行流多个线程之间是 并发 执行的我们可以通过以下的示例验证使用 Thread类的sleep()方法让当前正在执行的线程暂停执行休眠指定的时间让出 CPUCPU 就可以去执行其他线程 → 实现线程切换、并发效果。必须捕获 InterruptedException 异常。注意sleep方法是静态方法通过 类名.sleep 即Tread.sleep() 调用。看以上的运行结果有时是 main在前thread在后有时候又相反多个线程的调度顺序是随机的无法预测。jconsole程序 观察线程我们可以使用jconsole程序 观察线程(在 jdk 的 bin 目录中)注意保证上面的代码是运行起来的状态。选择我们所创建的线程6.Thread类及常见方法Thread 类是 JVM 用来管理线程的⼀个类换句话说每个线程都有⼀个唯⼀的 Thread 对象与之关联。用我们上面的例⼦来看每个执行流也需要有⼀个对象来描述类似下图所示而 Thread 类的对象 就是用来描述⼀个线程执行流的JVM 会将这些 Thread 对象组织起来用于线程调度线程管理。Thread 的常见构造方法Thread() —— 使用这个方法必须重写Thread 的 run() 方法Thread(Runnable tarhet) —— 必须重写 Runnable 的 run() 方法Thread(String name) —— 在创建线程的同时给线程起名字t1t2t3注意之前通过jconsole程序 观察的线程 的名称 就像 Thread-0Thread-1 等是线程的默认名称我们可以以上的方法为线程自定义一个名字此时可以再次观察线程发现线程的名字是我们自定义的以上的所有线程与之前的对比发现少了一个 main 主线程原因main方法执行完毕了主线程就结束了以前的认知中main方法执行结束那么代表整个程序(进程)就结束了实际上以前的认知只是针对单线程程序的现在是多线程的程序主线程main结束了但是还有其他的线程没有结束。如果想要看到 main 线程那么只要让 该线程不要结束即可Thread(ThreadGroup group,Runnable target) —— 第一个参数表示线程组该构造方法表示把多个线程放到一个组里统一针对这个线程组里所有的线程进行一些属性设置。Thread 的几个常见属性ID 是Java中给每个运行的线程分配的id线程的唯⼀标识类似于PID不同线程不会重复名称 是各种调试工具用到状态表示线程当前所处的⼀个情况优先级高的线程理论上来说更容易被调度到关于后台线程需要记住⼀点JVM会在⼀个进程的所有非后台线程结束后才会结束运行是否存活即简单的理解为 run 方法是否运行结束了线程的中断问题下面我们进⼀步说明# isDaemon() —— 是否后台线程什么是后台什么是前台isDaemon 其实表示 是否是守护线程而守护线程 后台线程即默默守护没有存在感这样的线程就称为后台线程。而像前面 t1 , t2 , t3 这几个线程的存在会影响到 进程 继续存在这样的线程就称为前台线程。而像这些JVM自带的线程他们的存在不影响进程结束即使它们继续存在如果进程结束它们也会随之结束(JVM 提供的这些线程属于有特殊功能的线程跟随整个进程持续执行比如垃圾回收的线程)也就是后台线程总结前台线程是否结束决定进程是否结束后台线程无论是否结束都不影响进程如果有多个前台线程那么必须所有前台线程都结束进程才会结束。我们在创建线程时包括main主线程默认都是前台线程可以通过 setDaemon() 方法来修改注意区分isDaemon() 是查看石是否是后台线程而 setDaemon() 方法可以修改前后台线程(参数是true表示修改为了后台线程)但是setDaemon的设置必须在启动线程start之前设置如以下代码 main 在执行3次之后就会结束但是该线程结束之后另一个线程会继续执行为了让 main主线程结束之后进程彻底结束我们可以在另一个线程启动之前将其修改为后台线程既让该进程中只有 main 这一个前台线程IDEA 本身也是一个Java进程在 IDEA 中运行一个Java代码通过IDEA进程又创建了一个新的Java进程这两个进程是有 父子关系 的。进程之间有父子关系线程之间不存在父子关系# isAlive() —— 是否存活(run方法是否运行结束)Java代码中创建的Thread 对象和系统中的线程是一一对应的关系但是Thread对象的生命周期和系统中线程的生命周期是不同的可能存在Thread对象还存活但是系统中的线程已经销毁的情况。示例以下代码的逻辑是3s之后线程的入口方法run()里的逻辑结束了操作系统中对应的线程就随之销毁了但是 thread 这个对象仍然存在也就是说当线程未销毁前使用 对象.isAlive()即thread.isAlive() 结果返回的是 true证明线程未销毁/结束当3秒之后线程结束了而 thread.isAlive() 仍然可以返回只不过变成了 false因为线程销毁了。这就是 Thread对象 还存活而县城已经销毁了的情况。总结Thread对象与线程一一对应一个对象只能创建一个线程但是一个对象的生命周期与线程是不同的。即每个Thread对象都只能 start 启动一次每次想创建一个新的线程都得创建一个新的Thread对象。7.中断线程准确来说是终止一个线程让这个线程彻底结束即让线程的入口方法 run() 尽快 return执行结束。我们继续最开始的例子举例李四⼀旦进到工作状态他就会按照行动指南上的步骤去进行工作不完成是不会结束的。但有时我们需要增加⼀些机制例如老板突然来电话了说转账的对方是个骗子需要赶紧停⽌转账那张三 该如何通知李四停止呢这就涉及到我们的停⽌线程的方式了。目前常见的有以下两种方式通过共享的标记来进行沟通调用 interrupt() 方法来通知1使用自定义的变量作为标记位定义一个静态成员变量isQuit表示标记线程是否中断。如果把isQuit定义成局部变量是否可以如下图是不可以的我们可以看报错的原因 ——isQuit应该是 final 或者 实际上是final 类型的变量而 final 修饰的变量其实就是常量也就是说这句话的意思是 isQuit应该是不可修改的。这就涉及到了 lambda 表达式中的变量捕获在 lambda中被捕获的变量是不允许修改的因此不能是局部变量而是一个成员变量这样就不再是一个变量捕获的语法了而是切换成内部类(匿名)访问外部类的成员 的语法这样也不必限制 final 之类的。2使用 thread.interrupted() 和 Thread.currentThread.isInterrupted() 代替自定义第二三个方法作用相同。Thread.currentThread.isInterrupted() —— 表示判断线程是否被终止相当于判断 Thread 里的 boolean 变量的值。thread.interrupted() —— 表示主动去进行终止相当于修改 boolean 变量的值为 true。为何是先.currentThread然后再.isInterrupted()—— 原因lambda 这里的定义是在 new Thread 之前的也是在 Thread thread 声明之前的lambda 的定义相当于 Runnable 类子类的创建 即 需要子类作为参数 传入 Thread才能创建出 线程因此我们不能直接通过 Thread 的引用 thread 直接调用isInterrupted() 方法在调用 isInterrupted() 方法之前应该先调用 currentThread() 静态方法来获取到当前线程的引用(该方法的作用在哪个线程中调用就获得哪个线程的 Thead 引用 )。——————如运行结果缺失终止了而且会报错/异常原因修改完 boolean 变量的值之后唤醒了 sleep 这样的阻塞方法即 使得休眠失效/不成功抛出 sleep 的异常。如果想要让结果更好看可以 break但是本质不变针对sleep的异常处理如果不加 break让 catch 部分空着呢会发生什么看运行结果发现当 main 尝试 interrupt 终止 thread 线程时并不能终止线程thread还是可以继续执行下去。原因针对这个情况其实是 sleep 在搞鬼正常来说调用了 Interrupt 方法就会修改 isInterrupt 方法内部的标志位设置为 true但是由于将 sleep 唤醒了这种提前唤醒的情况下sleep 就会在唤醒之后把 isInterrupt 的标志位给设置回 false因此如果继续执行到循环的条件判断就会发现能够继续执行。总结加上 break 就是立即终止什么都不写就是不终止catch 中先执行一些其他的逻辑再执行 break就是稍后终止。Java中的线程终止不是一个“强制性”的措施不是main线程让thread终止就终止选择权在thread自己手里。8.等待一个线程 join()多个线程之间并发执行随机调度而join 能够要求多个线程之间结束的先后顺序。有时我们需要等待⼀个线程完成它的工作后才能进行自己的下⼀步工作。例如张三只有等李四 转账成功才决定是否存钱这时我们需要⼀个方法明确等待线程的结束。虽然可以使用 sleep 休眠的时间来控制线程结束的顺序但是有的情况下这种设定并不科学有时候希望 thread 先结束main 就可以紧跟着结束了此时通过设置休眠时间的方式不一定靠谱。示例以上代码运行的结果有两种可能我们应该使用 join() 方法来控制。示例在主线程 main 中调用 join意味着让主线程等待 thread 线程先结束。那么当执行到 thread.join 时main线程就会“阻塞等待”一直等到 thread 线程执行完毕join 才能继续执行。join 也会抛出和 sleep 一样的异常 InterruptedException这样无论怎么重新运行输出的结果都是 thread 线程先结束我们可以通过 jconsole 程序观察 main 的阻塞状态可以看到main一直在 27行处阻塞也就是 thread.join 处只要 thread 不结束主线程就会一直等待下去除了以上不带参数的 join() 方法外还有带参数的版本指定了“超时时间”即等待的最大时间超过这个最大时间就不再等待。第三个方法精确到 纳秒 级别一般很少用。示例如果在 2000毫秒之内thread 就结束了那么此时 join 就立即继续执行不会等满 2000毫秒如果 2000 毫秒 之内 thread 没有结束超时了此时 join 也继续执行下去就不会再等了。9.获取当前线程引用这个方法我们已经熟悉了这里不再展开。10.休眠当前线程该方法也一样。要记得因为线程的调度是不可控的所以这个方法只能保证 实际休眠时间是大于等于参数设置的休眠时间的。代码调用 sleep相当于让当前线程让出CPU资源当前线程会回到阻塞状态后续时间到了需要操作系统内核把这个线程重新调度到CPU上才能继续执行即时间到了意味着允许被调度了而不是立即执行。sleep 的特殊写法 ——sleep(0)该写法的作用是让当前的线程立即主动放弃CPU资源等待操作系统重新调度而不需要时间等待让出 CPU 后当前线程会回到就绪状态调度器可能重新选它也可能选其他线程。一句话区别sleep(0) 我暂时不用 CPU 了你们先用sleep(N) 我睡 N 毫秒期间别叫我总结sleep(0) 特殊作用主动触发线程调度让出 CPU 使用权不真正休眠和普通 sleep 区别普通 sleep 是固定时长阻塞sleep (0) 是立即让位调度10.线程状态在前一篇文章中我们了解过操作系统进程调度中的进程状态就绪状态阻塞状态而我们现在要学习的线程状态其实是Java针对以上进程状态的重新封装。查看线程的所有状态线程的状态是⼀个枚举类型 Thread.State10.1 NEWNEW安排了工作还未开始行动即new 了Thread对象还没有 start。示例使用 getState() 属性 获取当前线程状态10.2 TERMINATEDTERMINATED工作完成了即内核中的线程已经结束了但是 Thread 对象还在。示例以下代码中thread线程已经结束了但是 Thread对象还在。10.3 RUNNABLERUNNABLE可工作的又可以分为正在工作中和即将开始工作也就是就绪状态。线程正在CPU上执行线程随时可以去CPU上执行随叫随到即就绪状态示例如下图 thread线程正在执行中(while循环中虽然什么都没写但是本身也是一条指令一直循环执行也是需要在CPU上运行的)RUNNABLE 是处于 NEW 和 TERMINATED 之间的状态。10.4 TIMED_WAITINGTIMED_WAITING表示排队等着其他事情也就是阻塞状态。指定时间的阻塞期间不参与CPU调度不继续执行示例在 jconsole 中观察另外join(时间)也会进入该状态10.5 WAITINGWAITING表示排队等其他事情也是阻塞状态。区别该状态是死等也就是没有超时时间的阻塞等待join()示例10.6 BLOCKEDBLOCKED也是一种阻塞状态比较特殊是由于锁导致的阻塞。这个在讲到 锁 时在学习。----------------------------------------

更多文章