1 操作系统中的线程与进程
1.1 什么是进程
进程是程序运行分配资源(内存为主)的最小单位
1.2 什么是线程
线程是CPU调度的最小单位
1.3 线程与进程的关系
线程依赖于进程,一个进程下可以拥有多个线程
1.4 java进程
java环境下,线程必须有一个父进程。由于jvm本身是一个进程,如果需要在jvm中去运行程序,必须是要去使用线程去运行我们编程的程序。
1.5 进程之间的通信
进程的几种通信方式:
1.管道: pipe-父子进程通信, named pipe-允许非父子进程间通信
2.信号:sign - 软件层面对中断的模拟
3.消息队列: 内存的消息队列
4.共享内存:数据同步
5.信号量:同步和互斥的手段
6.套接字:socket 除了用于进程通信,还能用于网络通信;mysql通过socket连接,所以可以在本机连,也可以远端连,使用同一套代码
1.6 CPU
1个CPU核心数同时可以执行一个线程,逻辑处理器,intel引入的超线程技术,1个物理核心视为2个逻辑核心
Runtime#availableProcessors 可以获取逻辑核心数,一般来说设置线程数和逻辑核心数相关
1.7 上下文切换
一个线程在CPU上运行时,cpu的内部存储会存放这个线程的运行相关的数据;当这个线程A被调度出去,线程B被调度进来的时候,操作系统还需要把A线程的运行时相关数据的数据保存,将B线程的运行数据重新载入。
中断,内核态用户态切换等操作会引发上下文切换,一般来说一次上下文切换会耗费5k到2w个时钟周期,执行指令只需要耗费几到几十个时钟周期,vmstat 可以查询上下文切换
parallel 并行-同时工作;concurrent 并发-交替执行,一段时间内做多件事情,一定会引起上下文切换
系统调用会产生系统调用的上下文切换,会调用操作系统的api
2 认识java线程
java线程的使用方法:
官方只有2种: 1创建一个线程 2派生自runnable
继承Thread,执行start方法
public class MyThread extends Thread{
@Override
public void run(){
System.out.println("hello");
}
}
实现Runnable方法,交给Thread执行
实现Callable 通过FutureTask封装成一个Runnable交给Thread执行,在通过get获取结果
private static class UserCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 22;
}
}
public static void main(String[] args) {
UserCallable userCallable = new UserCallable();
FutureTask<Integer> futureTask = new FutureTask<Integer>(userCallable);
new Thread(futureTask).start();
//xxxx
Integer integer = futureTask.get();
}
线程池
复用创建的线程
2.1 线程的使用
不建议使用的方法:
suspend() 挂起,资源不释放
stop() 直接停止,没有执行完操作,资源没有正确的释放
interrupt()-线程的中断
优雅的中断线程的执行
告诉运行的线程你要中断了,线程可通过isInterrupted()来自己实现中断;不建议自定义取消标记来自己实现,当线程内部存在阻塞方法时,例如sleep,take等方法,线程不知道这个标志位变化了,反应没有这么快速。jdk凡是阻塞类的方法都会有InterruptedException,会重新把true改为false,可以捕获这个异常处理中断的情况。
处于死锁的线程无法被中断
public class MyThread extends Thread{
@Override
public void run(){
while (!isInterrupted()){
System.out.println("hahaha");
}
}
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(10);
thread.interrupt();
System.out.println("interrupt");
Thread.sleep(10);
}
}
start()-线程的启动
start为什么不允许执行2次?
因为start方法是真正启动一个线程,一个Thread对象只能映射一个线程,所以不允许执行2次; start方法就会将thread对象和真正的线程进行挂钩
2.2 线程的状态
yield()-让出cpu调度,concurrentHashMap 初始化过程使用了yield
2.3 线程的调度
协同式线程调度 用完cpu主动通知其他线程
抢占式线程调度 每个线程的执行的时间以及切换都由操作系统决定。java线程调度就是抢占式调度
2.4 线程的实现
语言层面的线程的实现方式: 用户线程 1:n 内核线程 1:1 混合 m:n
内核线程实现
内核线程(Kernel-level Thread KLT)就是只有由操作系统内核支持的线程,
用户线程实现
语言层面的线程操作系统是无法感知到的。用户线程的创建,销毁切换和调度都是必须考虑的问题。例如、阻塞,多处理器都需要考虑
(Go)语言的实现方式就是这种方式
混合实现: 内核线程+用户线程
hotspot中,每一个Java线程都是直接映射到内核线程,也就是通过内核线程进行实现的
2.5 协程
用户线程的实现
出现的原因
互联网架构在处理一次对外部业务的请求的响应,往往需要不同服务器的大量服务共同协作来实现的,也就是微服务架构。由于服务数的增加,导致每个服务的处理的时间需要更短的时间
协程的使用 适用于IO密集型,高并发业务场景
内存占用: 线程 >1M/个 协程 >100kb个
2.6 纤程-Java中的协程
Loom项目
推荐使用Quasar java中较为出名的协程库Quasar 运行时需要加 -javaagent: quasar-core-xxx.jar
jdk19
不推荐使用
jdk19(非LTS)引入协程,并称为轻量级虚拟线程但是只是预览版,不推荐使用,如果要使用需要 javac --release 19 --enable-preview XXX.java编译 并使用java --enable-preview XXX运行改程序
2.7 守护线程
当jvm中运行的都是守护线程时,jvm就会退出 守护的是资源(内存)调度
useThread.setDeamon(true);
3 多线程
3.1 线程间的通信以及协调
java中的管道的输入输出:
PipedInputStream,PipedReader
join()方法,可以控制线程顺序
3.2 synchronized内置锁
对象锁和类锁
锁在静态方法是使用的类锁,加在xx.class这个对象上的锁
synchronized需要锁住同一个对象,不然锁是无用的
3.3 volatile轻量级同步机制
线程a可以感知到线程b对同一对象中volatile变量的修改
不能保证线程安全,适用于一个线程写,多个线程读
3.4 等待/通知机制
wait()/notify()
notifyAll()通知所有线程,推荐使用
notify() 随机唤醒线程
等待和通知的标准范式
wait方法会去释放锁
//等待方:
lock(obj){
while(条件不满足){0
obj.wait();
}
dothings();
}
//通知方:
lock(obj){
dothings();
//改变条件
obj.notify();
}
方法和锁
yield(),sleep()都不会释放锁;wait()会释放持有锁,而且当前被唤醒后,需要重新去获取锁;notify不会对锁有影响,notify一般在同步块代码最后一行
为什么wait和notify需要在同步块里使用(不然java会抛异常)?
//生产者
count + 1;
notify();
//消费者
while(count < 0)
wait();
count--;
生产者和消费者都是2个步骤,消费者准备休眠时,生产者通知,将会被消费者给丢弃掉,然后就无法被唤醒;