看了很多笔经面经,操作系统这块问的不多。说实话自己对于操作系统底层也不是特别了解,在这里稍微整理一下常见的内容吧
进程是CPU分配资源的最小单元,线程是CPU调度的基本单元、一个进程可以包含多个线程
1.进程
2.线程
可以使用管道或者共享内存。
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
新数据插入到链表头部;
每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
当链表满的时候,将链表尾部的数据丢弃。
相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。详细实现如下:
数据第一次被访问,加入到访问历史列表;
如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;
当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;
缓存数据队列中被再次访问后,重新排序;
需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。
LRU-K具有LRU的优点,同时能够避免LRU的缺点,实际应用中LRU-2是综合各种因素后最优的选择,LRU-3或者更大的K值命中率会高,但适应性差,需要大量的数据访问才能将历史访问记录清除掉。
当数据第一次访问时,2Q算法将数据缓存在FIFO队列里面,当数据第二次被访问时,则将数据从FIFO队列移到LRU队列里面,两个队列各自按照自己的方法淘汰数据。详细实现如下:
新访问的数据插入到FIFO队列;
如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;
如果数据在FIFO队列中被再次访问,则将数据移到LRU队列头部;
如果数据在LRU队列再次被访问,则将数据移到LRU队列头部;
LRU队列淘汰末尾的数据。
新插入的数据放入Q0;
每个队列按照LRU管理数据;
当数据的访问次数达到一定次数,需要提升优先级时,将数据从当前队列删除,加入到高一级队列的头部;
为了防止高优先级数据永远不被淘汰,当数据在指定的时间里访问没有被访问时,需要降低优先级,将数据从当前队列删除,加入到低一级的队列头部;
需要淘汰数据时,从最低一级队列开始按照LRU淘汰;每个队列淘汰数据时,将数据从缓存中删除,将数据索引加入Q-history头部;
如果数据在Q-history中被重新访问,则重新计算其优先级,移到目标队列的头部;
Q-history按照LRU淘汰数据的索引。
在这里补充一些线程池的内容吧
线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的。在jdk1.5之后这一情况有了很大的改观。Jdk1.5之后加入了java.util.concurrent包,这个包中主要介绍java中线程以及线程池的使用。为我们在开发中处理线程的问题提供了非常大的帮助。
为了避免重复的创建线程,线程池的出现可以让线程进行复用。通俗点讲,当有工作来,就会向线程池拿一个线程,当工作完成后,并不是直接关闭线程,而是将这个线程归还给线程池供其他任务使用。
//优雅关闭,不是强行关闭线程池,回收线程池中的资源,
//而是不再处理新的任务,将已接收的任务处理完毕后再关闭。
void shutdown();
//是否已经关闭,相当于回收了资源
boolean isShutdown();
//是否已经结束,相当于回收了资源
boolean isTerminated();
//可以提供线程执行后的返回值
Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
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;
}
核心参数:
上面描述的各个参数能很容易的总结出来:
小于等于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;
}
这两个方法中都执行了
- newTaskFor(task,val)
- 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(callable)和submit(runnable)的区别与联系
直接上构造函数
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);//⑤
}
}
到这里基本上线程池的主要流程我们已经梳理完了,下面来总结一下:
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