🔥你好我是fengxin_rou这是我的个人主页fengxin_rou的主页
❄️欢迎查看我的专栏我的专栏
《Java后端学习》、《JAVASE基础》、《JUC并发》、《redis》、《JVM虚拟机》、《MYSQL》、《黑马点评》、《rabbitmq》、《JavaWeb+AI的talis学习系统》、《苍穹外卖》
目录
前言
一、.进程和线程的区别?
1.基础定义:
2.核心7大区别
二、Go的协程和Java线程的区别
1.首先最核心的区别是它们的调度模型不一样。
2.从资源消耗上来说,差别非常明显
3.调度方式上也不太一样。
4.从使用方式上来讲,Go的协程用起来要简单太多了。
5.通信机制也有很大差异。
6.性能上来说,因为Go协程的轻量级特性和高效的调度器,在高并发场景下,Go的表现通常会更好。
三、线程有几种创建方式?各自优缺点?
1.继承Thread类
2.实现Runnable接口
3.实现Callable接口与FutureTask
4.使用线程池
补:系统吞吐量是指单位时间内系统成功处理的任务或业务量
四、线程的五大生命周期状态及流转过程?
五、什么是守护线程(Daemon Thread)?和用户线程区别?应用场景?
六、sleep ()、wait ()、yield ()、join () 区别?
前言
本栏目将讲解线程相关的知识,这一篇主要讲解一下线程基础
一、.进程和线程的区别?
1.基础定义:
进程:操作系统资源分配的最小单位,独立执行程序
线程:cpu调度执行最小单位,进程里的执行支路
2.核心7大区别
- 资源占用
进程:独占内存、文件、cpu、端口、资源独立
线程:共享进程所有的全部资源,自身存少量栈、寄存器
- 地址空间
进程:各自独立虚拟地址,互不访问
线程:同进程内所有线程共享一片地址空间
- 创建销毁开销
进程:开销巨大,耗时久
线程:轻量级,创建切换成本极低
- 通信方式
进程:只靠消息队列、管道、Socket、共享内存交互
线程:直接读写全局变量,成员变量即可通信
- 崩溃影响
进程崩溃:仅自身终止,其他线程不受影响
线程崩溃:整个所属线程直接退出
- 从属关系
进程相互独立:线程依附进程,不能单独存在
一个进程最少自带一个主线程
- 使用场景
进程:多软件隔离运行、服务隔离部署
线程:单程序内部并发任务、异步处理
二、Go的协程和Java线程的区别
1.首先最核心的区别是它们的调度模型不一样。
Java线程一个线程对应着一个操作系统用户线程,线程的创建销毁调度都是经过操作系统内核的
go的协程也就是goroutine,是用户及的轻量级线程,是Go运行时自己调度的,不需要经过操作系统内核,一个用户态调度器对应着多个goroutine
2.从资源消耗上来说,差别非常明显
Java线程的创建销毁开销很大,一个线程差不多1MB每一个线程都要走操作系统,所以说需要线程池里复用线程
Go协程就比较轻量,一个goroutine栈队列可以无限拉长,并且一个协程 大小也就2KB,所以资源消耗比较小
3.调度方式上也不太一样。
Java线程是抢占式调度,所有的线程切换都是由操作系统决定的,这里就涉及到了用户态和内核态的切换导致开销比较大
Go协程的调度是用户态决定的,在调度时有一个GMP模型,G是goroutine,M是用户态,P是逻辑处理器,Go会把多个写成映射到少量的操作系统上执行,切换完全是在用户态上完成,这样就导致切换成本十分低。而且go的调度器在协程阻塞时,会把这个协程挂起,执行其他协程,十分高效
4.从使用方式上来讲,Go的协程用起来要简单太多了。
在go里面只需要在想要创建的协程前加上一个go关键字就可以了
package main import ( "fmt" "time" ) func doSomething(name string) { for i := 0; i < 3; i++ { fmt.Printf("%s: %d\n", name, i) time.Sleep(100 * time.Millisecond) } } func main() { // 创建协程就这么简单, 加个go关键字 go doSomething("协程1") go doSomething("协程2") go doSomething("协程3") // 等待协程执行完 time.Sleep(time.Second) fmt.Println("主程序结束") }在java里需要new 线程对象,实现Runnable接口,或者用线程池,代码量比较复杂
public class RunnableExample { public static void main(String[] args) { // 实现Runnable接口 Runnable task = new Runnable() { @Override public void run() { for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + ": " + i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }; // 创建Thread对象并传入Runnable Thread thread1 = new Thread(task, "线程1"); Thread thread2 = new Thread(task, "线程2"); Thread thread3 = new Thread(task, "线程3"); thread1.start(); thread2.start(); thread3.start(); } }5.通信机制也有很大差异。
Java通信需要用共享内存来通信,这就要用synchornized、lock这些锁来完善同步,还要避免死锁问题,比较麻烦
go通信虽然也可以使用共享内存,但更推荐使用channel来通信,代码会清晰一点,也不容易出现并发问题
6.性能上来说,因为Go协程的轻量级特性和高效的调度器,在高并发场景下,Go的表现通常会更好。
如果要同时处理几万个请求,Java需要用复杂的异步框架,锁处理机制,而go只需要为每一个请求开一个协程就行了,代码简单,性能还好。
但是在传统企业级项目,有很多框架和中间件的使用,这里使用java还是比较好
三、线程有几种创建方式?各自优缺点?
1.继承Thread类
优点:继承Thread操作简单,用Thread.currentThread()可以得到当前运行的线程,用this也可以直接得到当前运行的线程
缺点:继承Thread类后不能继承其他类
2.实现Runnable接口
若是一个方法继承了其他父类,就可以实现java.lang.Runnable接口,实现接口需要重写它的Run方法,然后在Thread构造器里把Runnable对象作为参数传递进去,然后使用strat()方法创建线程
优点:可以在继承其他父类时,创建线程,并且可以实现多个线程资源共享,可以操作同一个Runnable对象,可以把cpu代码和数据分开,更好体现面向对象的编程思想
缺点:操作比较复杂,访问当前线程只能用Tread.currentTread()方法
3.实现Callable接口与FutureTask
java.util.concurrent.Callable接口类似于Runnable接口,但Callabe接口的call方法有返回值还可以抛出异常,想要创建线程需要先创建Callable对象,然后把对象作为参数传给Future Task任务管理器,然后创建线程,执行Callable任务、还可以得到任务结果
优点:可以继承其他父类,并且实现资源共享
缺点:操作复杂并且访问当前线程只能Thread.currentThead()
4.使用线程池
Java 5 引入的java.util.concurrent.ExecutorService接口和其他类,提供了线程池的支持,避免了查询和创建线程,可以通过Executor的静态方法创建不同类型的线程池、提高了线程管理效率
优点:可以重用预先创建的线程,避免了创建和查询线程的开销,提高了线程管理的效率。线程池可以合理的分配线程,避免资源浪费,如果遇到并发情况,线程池还可以快速提供线程来处理任务,减少等待时间。合理设置线程池的大小,可以提高cpu的利用率和系统吞吐量
补:系统吞吐量是指单位时间内系统成功处理的任务或业务量
缺点:代码操作比较复杂,在设置线程池参数的时候如果出错,可能会引起死锁,资源进程等问题,修复起来比较复杂
四、线程的五大生命周期状态及流转过程?
五大生命周期:new(新的)、runnable(就绪)、running(运行)、blocked(阻塞)、Terminated(终止)
| 线程状态 | 解释 |
|---|---|
| NEW | 尚未启动的线程状态,即线程创建,还未调用start方法 |
| RUNNABLE | 就绪状态(调用start,等待调度)+正在运行 |
| BLOCKED | 等待监视器锁时,陷入阻塞状态 |
| WAITING | 等待状态的线程正在等待另一线程执行特定的操作(如notify) |
| TIMED_WAITING | 具有指定等待时间的等待状态 |
| TERMINATED | 线程完成执行,终止状态 |
流转过程:就是新建之后进入就绪状态,然后争夺锁,争夺锁失败之后进入阻塞,锁释放了之后就进入就绪状态再次争夺直到拿到锁
五、什么是守护线程(Daemon Thread)?和用户线程区别?应用场景?
定义:守护线程是后台服务线程,当所有的普通线程(用户线程)停止了,JVM退出,守护线程也强制退出
区别:
1.退出时机:普通线程是执行完后,jvm才结束退出。守护线程则是jvm退出后不管是否执行完都退出
2.创建设置:线程创建默认就是用户线程,而守护线程需要thread.setDaemon(true)才行,且必须在调用start()之前使用,否则报错
3.生命周期:用户线程独立生命周期,不受其他线程影响。而守护线程的生命周期依附于所有的用户线程
应用场景:
用于后台监控、心跳、定时巡检、垃圾回收、日志这些场景
比如,监听是否全部普通线程都还在执行,监控垃圾回收是否执行完成。
六、sleep ()、wait ()、yield ()、join () 区别?
| 对比维度 | sleep() | wait() | yield() | join() |
|---|---|---|---|---|
| 所属类 | Thread(静态) | Object(实例) | Thread(静态) | Thread(实例) |
| 释放锁 | ❌ | ✅ | ❌ | ✅(底层依赖 wait) |
| 使用位置 | 任意位置 | 仅同步块内 | 任意位置 | 任意位置 |
| 状态变化 | TIMED_WAITING | WAITING/TIMED_WAITING | 仍为 RUNNABLE | WAITING/TIMED_WAITING |
| 恢复方式 | 超时自动恢复 | notify / 超时 | 立即重新竞争 CPU | 目标线程执行完毕 |
类别:sleep()是Thread类 的静态方法,在任何地方都可以通过Thread对象来调用,而wait()方法是Object类的实例方法,意思是需要实例对象来调用
锁释放:sleep不会让线程释放锁,会一直持有锁,wait会释放锁,让其他线程来获取锁
使用前提:sleep在任何时候都能调用,而wait只有在同步块也就是sychronized代码块里才能使用,使用后会释放锁
唤醒机制:sleep虽是超时等待但不涉及线程协作所以notify不能唤醒,只能自动恢复,而wait必须要用notify或notifyAll或超时唤醒
补:notify()只能唤醒调用同一个对象池中等待的线程,而sleep不会进入对象的线程池(因为不释放锁)所以不会被唤醒
释放锁总结 释放锁:
wait()、join()不释放锁:sleep()、yield()同步块限制 只有
wait()强制要求在synchronized中使用,其余三者无限制。- 只有
wait()依赖notify()/notifyAll()唤醒;sleep/yield/join都不受notify影响。
七、为什么 wait、notify 要在 synchronized 里执行?
wait()需要释放锁,所以需要拿到锁才能执行
notify:
1.唤醒操作需要操作锁对象监视器,而操作锁对象监视器需要锁
2.保证唤醒+修改条件的原子性:防止出现修改条件后没有线程在等待的局面
八、什么是线程上下文切换?为什么会耗时间
线程上下文切换:CPU暂停当前线程,并且保存当前线程的运行状态(上下文),切换到另一个线程,这个过程就是上下文切换
上下文:一个线程执行时的寄存器数据,程序计数器、栈信息、程序标记等信息是恢复线程执行的全部数据
耗时原因:
1.保存旧上下文和加载新上下文需要时间
2.线程切换需要从用户态转到内核态需要时间
3.cpu高速缓存失效,再次加载线程需要重新缓存进入cpu
4.操作系统的调度下一个线程的算法也需要开销