秋招准备(三)操作系统和线程池校招笔试面试整理

看了很多笔经面经,操作系统这块问的不多。说实话自己对于操作系统底层也不是特别了解,在这里稍微整理一下常见的内容吧

进程和线程

进程是CPU分配资源的最小单元,线程是CPU调度的基本单元、一个进程可以包含多个线程

  • 进程
    一个程序在一个数据集上的一次运行过程
    系统资源分配的单位
    进程是独立的,有自己的内存空间和上下文环境。
  • 线程
    进程的实体,被系统独立调度和执行的基本单位,cpu调度的基本单位。
    同一进程的线程可以共享一内存的空间
    线程占用的资源比较少。

进程和线程之间的通信方式

1.进程

  • 管道:
    • 面向字节流
    • 生命周期随内核
    • 自带同步互斥机制
    • 半双工、单向通信、两个管道实现双向通信
  • 消息队列
    在内核中创建一个队列,队列中每个元素都是一个数据报,不同的进程可以通过句柄去访问这个队列
    • 消息队列可以被认为是一个全局的链表,链表节点中存放数据报的类型和内容
    • 消息队列运行一个或多个进程写入或读取消息
    • 消息队列的生命周期随内核
    • 消息队列可实现双向通信
  • 信号量
    在内核中 创建一个信号量集合(本质是个数组),数组的元素(信号量)都是1,使用P操作进行-1,使用V操作+1;
    • PV操作用于同一进程,实现互斥。
    • PV操作用于不同进程,实现同步。
    • 对临界资源进行保护。
  • 共享内存
    将同一块物理内存一块映射到不同的进程的虚拟地址空间中,实现不同进程对同一资源的共享。
    • 不用从用户态到内核态的频繁切换和拷贝数据,直接草内存中读取就可以。
    • 共享内存是临界资源,所以需要操作时必须要保证原子性。使用信号量或者互斥锁都可以。
    • 生命周期随内核

2.线程
可以使用管道或者共享内存。

缓存的更新算法

  • LRU 近期最少使用算法
    LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

  1. 新数据插入到链表头部;

  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;

  3. 当链表满的时候,将链表尾部的数据丢弃。

  • LRU -K
    LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。

相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。详细实现如下:

  1. 数据第一次被访问,加入到访问历史列表;

  2. 如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;

  3. 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;

  4. 缓存数据队列中被再次访问后,重新排序;

  5. 需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。

LRU-K具有LRU的优点,同时能够避免LRU的缺点,实际应用中LRU-2是综合各种因素后最优的选择,LRU-3或者更大的K值命中率会高,但适应性差,需要大量的数据访问才能将历史访问记录清除掉。

  • TWO QUEUES
    Two queues(以下使用2Q代替)算法类似于LRU-2,不同点在于2Q将LRU-2算法中的访问历史队列(注意这不是缓存数据的)改为一个FIFO缓存队列,即:2Q算法有两个缓存队列,一个是FIFO队列,一个是LRU队列。

当数据第一次访问时,2Q算法将数据缓存在FIFO队列里面,当数据第二次被访问时,则将数据从FIFO队列移到LRU队列里面,两个队列各自按照自己的方法淘汰数据。详细实现如下:

  1. 新访问的数据插入到FIFO队列;

  2. 如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;

  3. 如果数据在FIFO队列中被再次访问,则将数据移到LRU队列头部;

  4. 如果数据在LRU队列再次被访问,则将数据移到LRU队列头部;

  5. LRU队列淘汰末尾的数据。

  • Multi Queue
    MQ算法根据访问频率将数据划分为多个队列,不同的队列具有不同的访问优先级,其核心思想是:优先缓存访问次数多的数据。
  1. 新插入的数据放入Q0;

  2. 每个队列按照LRU管理数据;

  3. 当数据的访问次数达到一定次数,需要提升优先级时,将数据从当前队列删除,加入到高一级队列的头部;

  4. 为了防止高优先级数据永远不被淘汰,当数据在指定的时间里访问没有被访问时,需要降低优先级,将数据从当前队列删除,加入到低一级的队列头部;

  5. 需要淘汰数据时,从最低一级队列开始按照LRU淘汰;每个队列淘汰数据时,将数据从缓存中删除,将数据索引加入Q-history头部;

  6. 如果数据在Q-history中被重新访问,则重新计算其优先级,移到目标队列的头部;

  7. Q-history按照LRU淘汰数据的索引。

线程池

在这里补充一些线程池的内容吧

什么是线程池

线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的。在jdk1.5之后这一情况有了很大的改观。Jdk1.5之后加入了java.util.concurrent包,这个包中主要介绍java中线程以及线程池的使用。为我们在开发中处理线程的问题提供了非常大的帮助。

为了避免重复的创建线程,线程池的出现可以让线程进行复用。通俗点讲,当有工作来,就会向线程池拿一个线程,当工作完成后,并不是直接关闭线程,而是将这个线程归还给线程池供其他任务使用。

继承数和相关类

在这里插入图片描述
接下来我们来逐个分析一下

  • Executor
    线程池的顶级接口,用于启动线程任务
    void execute(Runnable command);
  • ExecutorService
    提供了线程生命周期的管理方法
    定义了线程的状态:Running,ShuttiongDown,Terminated
    //优雅关闭,不是强行关闭线程池,回收线程池中的资源,
    //而是不再处理新的任务,将已接收的任务处理完毕后再关闭。
    void shutdown();
    //是否已经关闭,相当于回收了资源
    boolean isShutdown();
    //是否已经结束,相当于回收了资源
    boolean isTerminated();
    //可以提供线程执行后的返回值
    Future<T> submit(Callable<T> task);
    Future<?> submit(Runnable task);
  • ThreadPoolExecutor
    最常用的线程池,除了ForkJoinPool外,其他常用线程池底层都是使用它实现的。其中FixedThreadPool,CachedThreadPool,SingleThreadPool,ScheduledThreadPool等线程池都是由它实现的
如何使用线程池

ThreadPoolExecutor的构造方法

public ThreadPoolExecutor(int paramInt1, int paramInt2, long paramLong, TimeUnit paramTimeUnit,
            BlockingQueue<Runnable> paramBlockingQueue, ThreadFactory paramThreadFactory,
            RejectedExecutionHandler paramRejectedExecutionHandler) {
        this.ctl = new AtomicInteger(ctlOf(-536870912, 0));
        this.mainLock = new ReentrantLock();
        this.workers = new HashSet();
        this.termination = this.mainLock.newCondition();
        if ((paramInt1 < 0) || (paramInt2 <= 0) || (paramInt2 < paramInt1) || (paramLong < 0L))
            throw new IllegalArgumentException();
        if ((paramBlockingQueue == null) || (paramThreadFactory == null) || (paramRejectedExecutionHandler == null))
            throw new NullPointerException();
        this.corePoolSize = paramInt1;
        this.maximumPoolSize = paramInt2;
        this.workQueue = paramBlockingQueue;
        this.keepAliveTime = paramTimeUnit.toNanos(paramLong);
        this.threadFactory = paramThreadFactory;
        this.handler = paramRejectedExecutionHandler;
    }

核心参数:

  • corePoolSize:核心线程数
    线程池创建之后,线程池中的线程数为0,当任务过来就会创建一个线程去执行,直到线程数达到corePoolSize 之后,就会被到达的任务放在队列中。
    核心线程会一直存活,即使没有任务需要执行。
    当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理。
    设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭。
  • maxPoolSize:最大线程数
    当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务。
    当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常。
  • keepAliveTime:线程空闲时间
    当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize。
    如果allowCoreThreadTimeout=true,则会直到线程数量=0。
  • allowCoreThreadTimeout:允许核心线程超时
  • rejectedExecutionHandler:任务拒绝处理器
    两种情况会拒绝处理任务:
    当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务。
    当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务。
    线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常。
    ThreadPoolExecutor类有几个内部实现类来处理这类情况:
    AbortPolicy:丢弃任务并抛出RejectedExecutionException
    CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
    DiscardPolicy:丢弃任务,不做任何处理。
    DiscardOldestPolicy:丢弃队列中最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
    实现RejectedExecutionHandler接口,可自定义处理器。
submit方法

上面描述的各个参数能很容易的总结出来:

小于等于coreSize 直接new Worker
大于coreSize放入阻塞队列BlockQueue
队列满了在放入池子中直到达到maxSize
maxSize在放入就使用拒绝策略

但是你并没有真正了解线程池的工作方式!!!
**pool.submit();**00000000
我们从线程池的submit方法入手开始分析:
submit有两个重载方法

 public <T> Future<T> submit(Callable<T> task) {
     if (task == null) throw new NullPointerException();
     RunnableFuture<T> ftask = newTaskFor(task);
     execute(ftask);
     return ftask;
 }
public <T> Future<T> submit(Runnable task, T result) {
     if (task == null) throw new NullPointerException();
     RunnableFuture<T> ftask = newTaskFor(task, result);
     execute(ftask);
     return ftask;
 }

这两个方法中都执行了

  1. newTaskFor(task,val)
  2. execute(futureTask)

也就是Runnable和Callable都转换成了RunnableFuture

newTaskFor(task,val)
这个时候又出现了另个重要的类FutureTask

public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}
public static <T> Callable<T> callable(Runnable task, T result) {
  if (task == null)
         throw new NullPointerException();
     return new RunnableAdapter<T>(task, result);
 }

unnable被适配成了Callable是通过RunnableAdapter–实现了callable,引用了Runnable!!!!

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

上面描述的关系可以用类图表示出来
在这里插入图片描述

  • submit方法接受Runnable和Callable并把他们通过FutureTask方法调用execute方法
  • FutureTask接收runnable时候会通过适配器把他们转换为实现callable,执行run方法借助callable实现的,这样FutureTask就有了返回值

线程submit(callable)和submit(runnable)的区别与联系

  • runnable是没有返回值所以不会阻塞
  • callable有返回值所以会阻塞
  • 不管调用哪个,submit提交给Future,最终统一适配为callable,最终执行execute(future Task)
execute(futureTask)

直接上构造函数

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException()int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {//③
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}
if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }

首先,整个exectute方法是并发的,所有线程都可能同时进入。这里的处理逻辑就是,如果worker的数量小于核心线程数量,直接addWorker;
那么我们可以看下addWorker方法,它有4种传参的方式:

入参方式说明
addWorker(firstTask, true)线程数小于corePoolSize时,放一个需要处理的task进Workers Set。如果Workers Set长度超过corePoolSize,就返回false
addWorker(firstTask, false)当阻塞队列被放满时,就尝试将这个新来的task直接放入Workers Set,而此时Workers Set的长度限制是maximumPoolSize。如果线程池也满了的话就返回false
addWorker(null, false)放入一个空的task进workers Set,长度限制是maximumPoolSize。这样一个task为空的worker在线程执行的时候会去阻塞任务队列里拿任务,这样就相当于创建了一个新的线程,只是没有马上分配任务
addWorker(null, true)这个方法就是放一个null的task进Workers Set,而且是在小于corePoolSize时,如果此时Set中的数量已经达到corePoolSize那就返回false,什么也不干。实际使用中是在prestartAllCoreThreads()方法,这个方法用来为线程池预先启动corePoolSize个worker等待从workQueue中获取任务执行

这里我们关注的就是主流程addWoker(futureTask);

1)独占锁锁住,构建一个Worker对象
2)判断线程池状态,未关闭任务不为空add到HashSet< Worker>中
3)释放锁,启动Worker。

所以,线程池的池子结构就是HashSet
因为HashSet是线程不安全的,所以对他加锁ReentrantLock
Worker本身也是Runnable,他有个成员变量叫做firstTask就是通过构造方法复制的传入的Runnable就是我们上文的FutureTask,构建一个Thread也是通过构造方法构建的(Runnable只是任务,Thread是才能启动任务)。
Worker.Thread.start();所以会执行run方法

runWorker(this)

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        //①获取任务(自身或者从队列取出)
        while (task != null || (task = getTask()) != null) {
            w.lock();
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);//②执行之前
                Throwable thrown = null;
                try {
                    task.run();//③执行真正的任务
                }catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);//④执行之后
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);//⑤
    }
}
  • 线程启动,执行FutureTask.run()
  • 执行完毕后下次再getTask()为空,执行processWorkerExit(worker,false)
  • 执行完毕后下次再getTask()不为空,从阻塞队列中获取

到这里基本上线程池的主要流程我们已经梳理完了,下面来总结一下:

  1. 当一个submit(callable)的请求进入时候,在AbstractExecutorService中FutureTask中执行newTaskFor和execute;
  2. execute去addWorker中进行判断,本质是一个HashSet,HashSet.add(new Worker())
  3. Worker本身也是Runnable,他有一个参数firstTask负责传入FutureTask中的参数,然后构建一个Thread runWorker
  4. runWorker中的run方法是主体,基于AOP,在worker.thread.start时候执行;
常见的四种线程池

1.newFixedThreadPool
固定大小的线程池,该线程池corePoolSize和maximumPoolSize相等,阻塞队列使用的是LinkedBlockingQueue,大小为整数最大值。

该线程池中的线程数量始终不变,当有新任务提交时,线程池中有空闲线程则会立即执行,如果没有,则会暂存到阻塞队列。对于固定大小的线程池,不存在线程数量的变化。同时使用无界的LinkedBlockingQueue来存放执行的任务。当任务提交十分频繁的时候,LinkedBlockingQueue迅速增大,存在着耗尽系统资源的问题。而且在线程池空闲时,即线程池中没有可运行任务时,它也不会释放工作线程,还会占用一定的系统资源,需要shutdown。

2.newSingleThreadExecutor
单个线程线程池,只有一个线程的线程池,阻塞队列使用的是LinkedBlockingQueue,若有多余的任务提交到线程池中,则会被暂存到阻塞队列,待空闲时再去执行。按照先入先出的顺序执行任务。

3.newCachedThreadPool
缓存线程池,缓存的线程默认存活60秒。线程的核心池corePoolSize大小为0,核心池最大为Integer.MAX_VALUE,阻塞队列使用的是SynchronousQueue。是一个直接提交的阻塞队列, 他总会迫使线程池增加新的线程去执行新的任务。在没有任务执行时,当线程的空闲时间超过keepAliveTime(60秒),则工作线程将会终止被回收,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销。如果同时又大量任务被提交,而且任务执行的时间不是特别快,那么线程池便会新增出等量的线程池处理任务,这很可能会很快耗尽系统的资源。

4.newScheduledThreadPool
定时线程池,该线程池可用于周期性地去执行任务,通常用于周期性的同步数据。
scheduleAtFixedRate:是以固定的频率去执行任务,周期是指每次执行任务成功执行之间的间隔。
schedultWithFixedDelay:是以固定的延时去执行任务,延时是指上一次执行成功之后和下一次开始执行的之前的时间。

如何选择线程池数量
  • cpu密集型的任务:

    线程数=核心数N+1;

  • io密集型的任务:

    线程数=核心数N*2+1;

  • 实际应用:

    线程数=((cpu线程时间+线程等待时间)/cpu时间)*核心数N

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值