目录
线程的基础知识
线程的历史 也就是一部对cpu充分压榨的历史
单进程人工切换
- 纸片机 cpu利用不高 很多时候是等人
多进程的批处理
- 多个任务批量执行
多进程的并行处理
- 把程序写在不通的内存位置上来回切换
多线程
- 一个程序内部不同任务的来回切换
- selector - epoll
线程是属于一个进程内部的 ,并行执行的 ,和io相配
协程/纤程
- 绿色线程,用户管理的(而不是OS管理的)线程 go语言里叫协程 java里叫纤程
就是原来的多线程是操作系统帮我管理的,但绿色线程是用户自己管理的。这样的线程
尽自己最大的努力压榨cpu 对cpu的利用率最高。
什么是进程?线程?纤程/协程?
什么是程序? exe 可执行文件。
可执行的文件。在内存里放很多份 每一份都是一个进程 操作系统会为每个进程分配他相关的资源
最简单的资源 比如说 内存空间 io的端口号等等 静静的放在硬盘里 等需要执行的时候 才会放在内存里
放在内存后 哪个程序需要执行 ok 就放到cpu里去执行
一个程序可以有多个进程 当然也可以通过代码只有一个进程也可以 作为进程来讲 它是操作系统进行资源分配的基本单位
什么是线程?
通俗来讲 基本上 一个程序里面不同的执行路径 你的程序里没有同时都在运行的路径 那就是单线程 也叫主线程 main 方法所开启的线程也叫主线程
什么样的才叫多线程?
通俗来讲 当你启动一个程序的时候 它在中间会产生分支,不同的分支在同时运行 这个东西叫多线程。
从底层来讲
程序真正开始的时候 是以线程为单位开始执行 我们的操作系统会找到我们的主线程 扔给cpu去执行 如果主线程开启了其他线程 再来线程之间的来回切换 A线程执行了一会儿 然后再让B线程执行一会儿
进程 资源分配的基本单位 是一个静态概念 ; 线程 是在进程的内部 调度执行的基本单位 是一个动态的概念 多个线程共享同一个进程
什么是线程的切换
ALU 计算单元
Registers 寄存器组 用来存储数据的
PC 也是一种计算器 用来存储到底执行到哪条指令了
如果想让一个t1线程执行的话 就是把指令放在pc 数据放在寄存器组
如果这时候要切换t2线程的话怎么操作的讷
是把指令和数据 放到cache 缓存里
再把另一个t2线程的相关指令和数据放到cpu来进行计算
cpu是一个特别傻的东西 它就管算 你给他数据指令 它来算 其他什么都不管
至于说这个指令和数据是谁的 他不管 那是操作系统的事情 如果这时候需要继续做t1的话 也是一样的
把t2 东西放到cache 缓存里 再把t1的东西从缓存立取出 中间的这些调度操作 都是 操作系统的 这些调度也是需要消耗性能资源的 线程切换就是这么 专业名词是 Thread Context Switch 线程上下文切换
线程切换也是需要消耗资源
提问:单核cpu 设定多线程是否有意义?
答:有意义
why
因为线程其实是一段所谓的操作 这些操作 并不是所有操作都需要消耗资源 在第一个线程 等待或sleep的时候 不消耗资源的时候 让其他线程进行执行 这样就充分利用cpu的资源 所以也是有意义的。 有两种不同的线程 有的线程叫 cpu密集型 意思就是大量的时候是在做for循环啊 加法减法乘除做各种计算 这种线程就是对cpu的利用率特别高 还有一种就是io密集型 意思就是它大量的时间 来等待输入输出的这种 等待之后可能只是做一些简单的拷贝等等就完成了。大多数的线程是既有io也有cpu 不同的线程设定不同的数量是有区别的
提问:多线程是可以提高cpu的利用率,那是不是线程数设置的越大越好讷。
答:当然不是。线程之间的切换也是需要资源的 如果线程太多 那完蛋了 cpu的资源全消耗在对线程的切换上
工作线程数(线程池中线程数)设置多少合适?
方法1 可以自己进行压测 进行实际测试 是经常使用的一种方式 当然也可以进行推算 我的计算机是多少核 根据你的cpu 核数 根据你cpu的能力
那是不是多少核就设多少线程讷。肯定不是 一个计算机除了你的程序还有其他程序 一个java程序启动 本来就启动了很多线程 帮我们分配资源的线程 除了这些 还有计算机上 自己其他的线程在跑 得看实际机器的情况
在服务安全的角度上考虑不能让每一个cpu 都百分百 运行到百分之80 当然也是有一个线程的一个公式
这个公式来源于一个老外的Java并发编程实践 网上有很多变种公式 其实都一样
线程状态
java 的线程状态一共有六种
- NEW 线程刚刚创建,还没有启动
- RUNNABLE 可运行状态,由线程调度器可以安排执行
- WAITING 等待被唤醒
- TIME WAITING 隔一段时间自动唤醒
- BLOCKED 被阻塞,等待锁
- TERMINATED 线程结束
创建线程
创建线程的五种方法
- 继承Thread 重写它的run方法 启动的方法就是new一个 然后调用start方法。这是继承的方式
- 实现runnable 接口 重写它的run方法 启动的方法是new一个Thread 把这个类传进去 start
- 第三种当然也是一个new 一个thread 进行lamda表达式的重写 不进行继承和实现接口
- 使用线程池ThreadPool
- 带返回值的任务执行 callable 接口和 runnable的区别是一个实现run方法 一个实现call方法 而call 方法的好处是根据泛型可以定义返回的类型。
ps:有面试题会问这第一种第二种方法哪种更好 当然是第二种实现接口更好 因为一个class 实现runnable接口过后 还可以从其他类继承 但是如果你是继承了Thread过后 是不能再继承其他的了 这也是java语言的单继承而不是多继承的特性来的 第二种也更加灵活
虽说有五种 其实到底层源码来说也只有一种 不管是什么线程池还是实现什么接口 最终导致都是new了一个thread出来调用他的start方法 。