0
点赞
收藏
分享

微信扫一扫

【操作系统】面试

一叶随风_c94d 2022-04-06 阅读 115
java

文章目录

线程

线程、进程、协程

线程是程序执行流的最小单位,进程是系统进行资源分配和调度的独立单位。
进程:程序的一次动态执行过程,经历代码从加载,执行到完毕的完整过程
线程:进程在执行过程中产生的更小的执行单元。
协程:比线程更轻量级,一个线程可以有多个协程,是串行运行。

进程切换和线程切换的区别

进程切换涉及到虚拟地址空间的切换,而线程切换则不会,因为每个进程都有自己的虚拟地址空间,
线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。

为什么进程切换比线程切换快

关于这个问题,需要从虚拟地址和物理地址说起

物理地址就是真实的地址嘛,这种寻址方式很容易破坏操作系统,而且使得操作系统中同时运行两个或以上的程序几乎是不可能的(此处可以举个例子,第一个程序给物理内存地址赋值 10,第二个程序也同样给这个地址赋值为 100,那么第二个程序的赋值会覆盖掉第一个程序所赋的值,这会造成两个程序同时崩溃)。

当然,也不是完全不可能,有一种方式可以实现比较粗糙的并发

就是说,我们将空闲的进程存储在磁盘上,这样当它们不运行时就不会占用内存,当进程需要运行的时候再从磁盘上转到内存上来,不过很显然这种方式比较浪费时间。

于是,我们考虑,把所有进程对应的内存一直留在物理内存中,给每个进程分别划分各自的区域,这样,发生上下文切换的时候就切换到特定的区域

那问题还是很明显的,就是仍然没法避免破坏操作系统,因为各个进程之间可以随意读取、写入内容。

所以,我们需要一种机制对每个进程使用的地址进行保护,因此操作系统创造了一个新的内存模型,那就是虚拟地址空间

就是说,每个进程都拥有一个自己的虚拟地址空间,并且独立于其他进程的地址空间,然后每个进程包含的栈、堆、代码段这些都会从这个地址空间中被分配一个地址,这个地址就被称为虚拟地址。底层指令写入的地址也是虚拟地址。

有了虚拟地址空间后,CPU 就可以通过虚拟地址转换成物理地址这样一个过程,来间接访问物理内存了。

地址转换需要两个东西,一个是 CPU 上的内存管理单元 MMU,另一个是内存中的页表,页表中存的虚拟地址到物理地址的映射

但是呢,每次访问内存,都需要进行虚拟地址到物理地址的转换,对吧,这样的话,页表就会被频繁地访问,而页表又是存在于内存中的。所以说,访问页表(内存)次数太多导致其成为了操作系统地一个性能瓶颈。

于是,引入了转换检测缓冲区 TLB,也就是快表,其实就是一个缓存,把经常访问到的内存地址映射存在 TLB 中,因为 TLB 是在 CPU 的 MMU 中的嘛,所以访问起来非常快。

然后,正是因为 TLB 这个东西,导致了进程切换比线程切换慢。

由于进程切换会涉及到虚拟地址空间的切换,这就导致内存中的页表也需要进行切换,一个进程对应一个页表是不假,但是 CPU 中的 TLB 只有一个,页表切换后这个 TLB 就失效了。这样,TLB 在一段时间内肯定是无法被命中的,操作系统就必须去访问内存,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢。

而线程切换呢,由于不涉及虚拟地址空间的切换,所以也就不存在这个问题了。

为什么虚拟地址空间切换会比较耗时

虚拟地址空间的切换,这就导致内存中的页表也需要进行切换,一个进程对应一个页表是不假,但是 CPU 中的 TLB 只有一个,页表切换后这个 TLB 就失效了。这样,TLB 在一段时间内肯定是无法被命中的,操作系统就必须去访问内存,那么虚拟地址转就换为物理地址就会变慢。

线程三要素

原子性:原子操作,要么执行完,要么不执行
可见性:一个线程对一个变量修改时,别的线程能看到
有序性:程序执行顺序按照代码先后顺序

如何实现线程的可见性

Synchronized,volatile,final关键字
Final修饰为常量,不允许修改

线程的操作方法:

强制运行,join():其他线程无法运行,必需等此线程完成后才能运行
休眠,sleep():Thread.sleep(500)即可休眠500ms
中断,interrupt():
优先级:setPriority(Thread.MIN_PRIORITY)//优先级最低,并非优先级最高就一定先执行。
礼让,yield():将线程操作暂让。

Thread类和Runable接口区别:

Thread类也是runnable接口子类,但并没有完全实现其中run方法,其run方法也是调用runnable中的run方法,所以如果通过继承thread类实现多线程,必须复写run方法。

区别:runnable适合多个线程共享资源,thread不适合。

线程的start()和run()方法区别

1.调用start时启动线程,在执行时才自动调用run
2.一个线程只能调用一次start,否则会抛出异常,run方法无限制

wait和sleep的区别

Java怎么实现线程安全的单例(饿汉式、枚举、双重检验锁)

如何实现线程安全

进程间通信的方式

管道、消息队列、共享内存、套接字,信号量。

创建线程的方式,callable和runnable的区别

多线程

实现多线程的方式有几种:

两种:继承Thread类;实现Runable接口
继承Thread类

  1. class MyThread extends Thread{//继承thread类,作为线程实现类
  2. 复写run方法
  3. MyThread mt1=new MyThread(“线程a”)//实例化对象
  4. mt1.start()//调用线程主体
    实现runnable接口:
  5. class MyThread implements Runnable{ // 实现Runnable接口,作为线程的实现类
  6. 复写run方法作为线程的操作主体
  7. MyThread mt1 = new MyThread("线程A ") ; // 实例化对象
  8. Thread t1 = new Thread(mt1) ; // 实例化Thread类对象
  9. t1.start() ; // 启动多线程

如果一个系统需要引入多线程,需要考虑哪些方面?

java运行时至少启动两个线程:

每当java执行一个类时,都会启动一个jvm,每个jv就是在操作系统中启动一个线程,此外java本身具备垃圾收集机制,就是另外还有一个垃圾收集线程。

同步:多个线程操作同一资源时出现的资源同步问题

解决方法:

  1. 同步代码块:被synchronized关键字修饰,
    1, synchronized(同步对象){需要同步的代码}
    2, 使用synchronized关键字将一个方法声明为同步方法
    Synchronized 方法返回值 方法名称(参数列表){}
    3, Lock接口:Lock lock=new ReentranLock();Lock.lock();
    try{ }finally{ Lock.unlock(); }

多线程的时候为什么会出现线程不安全的情况

线程池

线程池概念

概念:首先创建一些线程供给任务,线程结束后会变成空闲状态,等待执行下个任务,
1.避免类频繁创建、销毁线程,资源消耗和影响响应速度
2. 避免线程并发数量过多,抢占资源从而造成阻塞
3. 可以管理线程,如:延时执行,定时循环执行的策略
构造:Executor接口或ThreadPoolExecutor类,

  1. Executor
    .newFixedThreadPolol:固定大小
    .newCachedThreadPool:可缓存
    .newSingleThreadPool单个线程数
  2. ThreadPoolExecutor:七个参数:
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));

线程池的常用参数

  1. corePoolSize:核心线程数
  2. maximumPoolSize:允许最大线程数
  3. keepAliveTime最大线程数可存活时间
  4. unit:设定存活时间
  5. workQueue:阻塞队列,存储等待执行的任务(Linked/ArrayBlockingQueue链表/数组组成)
  6. tjreadFactory线程工厂用来创建线程
  7. handler:拒绝策略(AbortPolicy拒绝并抛出异常)

线程池淘汰策略

线程数达到线程池的上限,有哪些策略来处理

拒绝策略:

  1. AbortPolice:中止策略:抛出异常并中止执行
  2. CallerRunsPolicy:把任务交给主线程执行
  3. DiscardPolicy:忽略最新任务
  4. DiscardOldestPolicy::忽略最早任务
  5. New RejectedExecutionHandler重写rejectedExecution实现重写自定义拒绝策略

线程池执行流程:

1.判断当前线程数和核心线程数、判断当前任务队列是否已满、判断当前线程是否达到最大线程数。得到结果都为true则会执行拒绝策略

IO密集和CPU密集两种情况下,线程池里的线程数应该怎么设置

死锁形成必要条件,避免死锁方法

1.一个资源之内被一个线程占有
2.当一个线程阻塞时,对已有资源保持不放(一次性申请完)
3.线程已获得的资源在未使用完不能被剥夺(未申请到别的资源,主动放弃)
4.循环等待(按序申请预防死锁)

synchronized与lock的区别,使用场景。看过synchronized的源码没

1.synchronized是一个关键字,一次只允许一个线程进入代码块,其他线程之内排队等
2.Lock时接口,需要手动加锁,解锁。使用时必需在try,catch块中进行,释放锁在finally块中,保证一定释放锁。

synchronized和volatile关键字的作用?

Synchornized,解决执行控制问题。是一个重量级操作,对系统性能影响大,
volatile:解决内存可见性问题:修改该变量时会强制将修改后的值写入主存中,且导致其他线程内存中对应的值实效,因此需要重新从主存中读取值,
区别:
volatile:只能使用在变量级别,仅实现内存可见性不能保证原子性,不会造成线程阻塞,不能被编译器优化
Synchronized:可以使用在变量,方法,类级别,保证可见性和原子性,会造成线程阻塞,标记的变量可以被编译器优化

Java锁机制?应用场景,和具体的实现方式?

1.独享锁一次只能被一个线程所有
共享锁:可被多个线程共有
2.公平锁:按照申请顺序取锁
非公平锁:不按
3.可重入锁:同一个线程在外层方法中获得到的锁,在进入方法内会自动得到。
4.互斥锁:是具体的实现reentranlock
读写锁readwritelock
5.乐观锁:不加锁,儿说不断更新获取数据
悲观锁:对同一个数据的并发操作会采取加锁
6.分段锁:细化锁的颗粒,当仅针对数组中一项数据操作时,只对这一项加锁
7.偏向锁:一段同步代码一直被一个线程所访问时该线程会自动获取锁,降低获取锁的代价
轻量级锁:当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁:当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
8.自旋锁:自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

分布式锁怎么实现?

基于缓存(redis);基于数据库;基于Zookeeper。

讲下乐观锁,乐观锁提交时怎么判断是否冲突

Segment”分段锁中每个Segment中如果叫你来设计,你会怎么设计?

ThreadLocal

ThreadLocal

本地线程变量,填充的是当前线程的变量,对其他线程封闭隔离,为每个变量在线程中创建了一个副本,这样每个线程可以访问自己内部的副本变量,
使用场景

  1. 在进行对象跨层传递时使用,避免多次传递,打破层次间的约束
  2. 线程间数据隔离
  3. 进行事务操作,用于存储线程事务信息
  4. 数据库连接,session会话管理。
  5. set方法:使用静态内部类ThreadLocalMap中Entry使用ThreadLocal作为key,value自己设置
  6. get

ThreadLocal内存泄漏问题;

当threadlocal为空时,就要进行垃圾回收,但是threadlocalmap生命周期和thread一样不会回收,这时threadlocalmap的key没了但是value还在。
解决方法:使用完threadlocal后执行remove操作。

并发

Java有哪些并发手段

同步代码块,锁,阻塞队列/令牌桶/信号量,CAS自旋
CAS自旋:比较并交换,直接用CPU层面指令保证原子性,性能高,

多并发怎么划分数组

并发包,

Java.util.concurrent.从jdk1.5开始新加入的一个包,致力于解决并发编程的线程安全问题,使用户能更快捷的编写程序,

  1. ConcurrentHashMap:线程安全的hashMap,
  2. CopyOnWriteArayList:线程安全的ArrayList

数据结构

conccurentHashmap原理

锁分段技术:容器中有多把锁,每把锁用于不同数据段,在多线程访问不同数据段时就不存在锁竞争,从而提高并发效率。
底层:Segment数组结构和HashEntry数组结构组成,
原理:Segment是重入锁,HashEntry存键值对,一个保护一个,在修改HashEntry时必须拿到Segment锁。HashEntry中除了value是volatile类型(确保读操作能得到最新值,避免了加锁),其他都是final类,因为不能修改next值,所有只能从hash链头部开始增删改。对于put,可加到头部,对于remove要从中间删除一个节点,并把前面节点都复制一遍,最好一个节点指向要删除的下一个节点。
定位操作:再哈希
Remove:先定位到段,再委托给段的remove操作,
Get:委托给段,不用锁,除非读到空值才会加锁重读,因为要用到的共享变量都是volatile,
Put:创建一个节点加到hash链头部,1.判断HashEntry是否要扩容,2.定位

CopyOnWriteArayList原理

写时加锁,读时不加

为什么并发操作会导致hashmap死循环?

在多线程环境下用HashMap进行put操作头插法会引起死循环,因为多线程导致HashMap的Entry链表形成环形数据结构,查找时会陷入死循环。

内存

操作系统的页式存储

虚拟内存

虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。

一个进程是怎么使用它的内存(不太了解,我就知道进程通信方式)

主机内存有16G,操作系统跑了100个进程,

每个进程都开的16G,该怎么去管理(我感觉可以用页面置换,虚拟内存)

操作系统里内存是怎么管理的

(讲了常见的几种内存管理机制:块式、页式、段式、段页式)

linux

linux你会啥命令,linux的文件管理

linux编译和执行

Linux下怎么去创建一个进程

Linux查看端口是否被占用命令

Linux管道的作用

把程序的输出直接连接到另一个程序的输入

happen-before原则

程序次序原则;volatile原则;传递原则;锁定原则;线程启动原则;线程终结原则;线程中断;对象终结;

时间局部性、空间局部性

优先级队列怎么实现的(给任务优先级)

同步、异步的使用的理解

举报

相关推荐

0 条评论