【操作系统】面试

文章目录

线程

线程、进程、协程

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

进程通信的方式

  1. 管道
  2. 消息队列
  3. 共享内存
  4. 信号量
  5. socket
  1. 管道:
  • 特点:
  1. 半双工的通信方式,数据只能单向流动,
  2. 只能在具有亲缘关系的进程间使用,
  3. 只存在于内存中。
  • 缺点:效率低(a 进程给 b 进程传输数据,只能等待 b 进程取了数据之后 a 进程才能返回。)
  • 优点:简单

(管道的通知机制类似于缓存,就像一个进程把数据放在某个缓存区域,然后等着另外一个进程去拿,并且是管道是单向传输的。)

  1. 消息队列
  • 特点:
  1. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
  2. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
  3. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
  • 缺点:如果 a 进程发送的数据占的内存比较大,并且两个进程之间的通信特别频繁的话,消息队列模型就不大适合了。因为 a 发送的数据很大的话,意味发送消息(拷贝)这个过程需要花很多时间来读内存。
  • 优点:无需等待其他进程来取就返回

(只需要把消息放在对应的消息队列里就行了,b 进程需要的时候再去对应的消息队列里取出来,这种通信方式也类似于缓存。)

  1. 共享内存
  • 特点
    共享内存是最快的一种,因为进程是直接对内存进行存取。
    因为多个进程可以同时操作,所以需要进行同步。
    信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
  • 缺点:线程安全问题
  • 优点:解决拷贝所消耗的时间。

系统加载一个进程的时候,分配给进程的内存并不是实际物理内存,而是虚拟内存空间。那么我们可以让两个进程各自拿出一块虚拟地址空间来,然后映射到相同的物理内存中,这样,两个进程虽然有着独立的虚拟内存空间,但有一部分却是映射到相同的物理内存,这就完成了内存共享机制了。

  1. 信号量
    信号量的本质就是一个计数器,用来实现进程之间的互斥与同步,而不是用于存储进程间通信数据。
  • 特点:
  1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
  2. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
  3. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
  4. 支持信号量组。
  • 缺点:在一台主机之间的通信,无法跨主机
  • 优点:解决了线程安全问题

(例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就会访问不了内存1。所以说,信号量也是进程之间的一种通信方式。

  1. socket
    跨主机通信,可用于不同机器间的进程通信。

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

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

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

虚拟地址和物理地址
物理地址就是真实的地址,这种寻址方式很容易破坏操作系统,而且使得操作系统中同时运行两个或以上的程序几乎是不可能的(此处可以举个例子,第一个程序给物理内存地址赋值 10,第二个程序也同样给这个地址赋值为 100,那么第二个程序的赋值会覆盖掉第一个程序所赋的值,这会造成两个程序同时崩溃)。
第一种方式:将空闲的进程存储在磁盘上,这样当它们不运行时就不会占用内存,当进程需要运行的时候再从磁盘上转到内存上来,不过这种方式比较浪费时间。
第二种方式:把所有进程对应的内存一直留在物理内存中,给每个进程分别划分各自的区域,这样,发生上下文切换的时候就切换到特定的区域。但是仍然没法避免破坏操作系统,因为各个进程之间可以随意读取、写入内容。

  • 所以,我们需要一种机制对每个进程使用的地址进行保护,因此操作系统创造了一个新的内存模型,那就是虚拟地址空间:每个进程都拥有一个自己的虚拟地址空间,并且独立于其他进程的地址空间,然后每个进程包含的栈、堆、代码段这些都会从这个地址空间中被分配一个地址,这个地址就被称为虚拟地址。底层指令写入的地址也是虚拟地址。有了虚拟地址空间后,CPU 就可以通过虚拟地址转换成物理地址这样一个过程,来间接访问物理内存了。
  • 地址转换需要两个东西,一个是 CPU 上的内存管理单元,另一个是内存中的页表,页表中存的虚拟地址到物理地址的映射。但是每次访问内存,都需要进行虚拟地址到物理地址的转换,这样页表就会被频繁地访问,而页表又是存在于内存中的。所以访问页表(内存)次数太多导致其成为了操作系统地一个性能瓶颈。
  • 于是,引入了转换检测缓冲区 TLB,也就是快表,其实就是一个缓存,把经常访问到的内存地址映射存在 TLB 中,因为 TLB 是在 CPU 的 内存管理单元的嘛,所以访问起来非常快。正是因为 TLB 这个东西,导致了进程切换比线程切换慢。
  • 由于进程切换会涉及到虚拟地址空间的切换,这就导致内存中的页表也需要进行切换,一个进程对应一个页表,但是 CPU 中的 TLB 只有一个,页表切换后这个 TLB 就失效了。这样,TLB 在一段时间内肯定是无法被命中的,操作系统就必须去访问内存,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢。
  • 而线程切换呢,由于不涉及虚拟地址空间的切换,所以也就不存在这个问题了。

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

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

线程三要素

原子性,可见性,有序性。

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

如何实现线程三要素

  • 原子性: final;synchronized中的 lock、unlock 中操作保证原子性;还有java.util.concurrent中实现的原子操作类。
  • 可见性: volatile、synchronized 和 final 。
  • 有序性:volatile 和 synchronized 。

final、synchronized和volatile关键字的作用?

1、final:保证原子性。

  • 原子性:修饰的变量一经初始化,就不能改变其值。

2、Synchornized:保证原子性、可见性和有序性。

  • 原子性:synchronized 在lock、unlock 中操作保证原子性。对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)。
  • 可见性:
  • 有序性:synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。

3、volatile:保证可见性和有序性:

  • 可见性:volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。修改该变量时会强制将修改后的值写入主存中,且导致其他线程内存中对应的值实效,因此需要重新从主存中读取值。所以对其他线程是可见的。
  • 有序性:volatile 是因为其本身包含“禁止指令重排序”的语义。

Java.util.concurrent中实现的原子操作类

Java.util.concurrent中实现的原子操作类包括:
AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference。
 其底层就是volatile和CAS 共同作用的结果:
首先使用了volatile 保证了内存可见性。
然后使用了CAS(compare-and-swap)算法 保证了原子性。
其中CAS算法的原理就是里面包含三个值:内存值A 预估值V 更新值 B 当且仅当 V == A 时,V = B; 否则,不会执行任何操作。

synchronized和volatile关键字的区别:

  1. volatile是线程同步的轻量级实现,其性能要比 synchronized关键字好。
    Synchronized是一个重量级操作,对系统性能影响大。
  2. volatile只能使用在变量级别。
    Synchronized可以使用在变量,方法,类级别。
  3. volatile拥有有序性,即禁止进行指令重排序
    synchronized 语义范围不但包括 volatile拥有的可见性,还包括volatile 所不具有的原子性,但不包括 volatile 拥有的有序性,即允许指令重排序。
  4. volatile不会造成线程阻塞,不能被编译器优化
    Synchronized会造成线程阻塞,标记的变量可以被编译器优化

多并发怎么划分数组

在JVM底层volatile是采用内存屏障来实现的,内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将缓存的修改操作立即写到主内存
  3. 写操作会导致其它CPU中的缓存行失效,写之后,其它线程的读操作会从主内存读。

volatile 原理

底层原理:内存屏障(写屏障、写屏障)

  • 对volatile变量写指令之后加入写屏障
  • 对volatile变量读指令之前加入读屏障

1.如何保证可见性

  • 写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中
  • 读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据。

1.如何保证有序性

  • 写屏障会确保指令重排时,不会将写屏障之前的代码排在写屏障之后。
  • 读屏障会确保指令重排时,不会将读屏障之后的代码排在读屏障之前。

volatile 的局限性

volatile 只能保证可见性,不能保证原子性写操作对其它线程可见,不能解决多个线程同时写的问题。

  • 有序性只保证了本线程内相关代码不被重排序。

volatile 使用场景

对于一个变量,只有一个线程执行写操作,其它线程都是读操作,这时候可以用 volatile 修饰这个变量。

Synchronized 原理

sychronized使用编程了monitorenter和monitorexit两个指令,每个锁对象都会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量:owner会保存获得锁的线程;recursions会保存线程获得锁的次数。当执行到monitorexit时recursions-1,当计数减到0时,这个线程就会释放锁。

执行同步代码块,首先会执行monitorenter指令,然后执行同步代码块中的代码,退出同步代码块的时候会执行monitorexit指令 。

使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就进入同步队列,线程状态变成BLOCK,同一时刻只有一个线程能够获取到monitor,当监听到monitorexit被调用,队列里就有一个线程出队,获取monitor。

每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一,所以只要这个锁的计数器大于0,其它线程访问就只能等待。

monitor
在这里插入图片描述
monitorexit
在这里插入图片描述
在这里插入图片描述

Synchronized 缺点

不能设置锁超时时间
不能通过代码释放锁
容易造成死锁

ReentrantLock

在多个条件变量和高度竞争锁的地方,用ReentrantLock更合适,ReentrantLock还提供了Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个Condition实例,所以更有扩展性。

线程的操作方法:

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

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

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

wait () 和sleep () 的区别

1、这两个方法来自不同的类:sleep来自Thread类,和wait来自Object类。
sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
2.最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。
3、使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
4、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

总结
两者都可以暂停线程的执行。
对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。线程不会自动苏醒。

优先级队列怎么实现的

给任务优先级

创建线程

创建线程有几种方式

继承 Thread 、实现 Runnable、线程池创建、Callable 创建
事实上创建线程本质只有一种方式,就是构造一个 Thread 类,这是创建线程的唯一方式,不同的只是 run 方法(执行内容)的实现方式。

继承Thread类

Thread 类实现了 Runnable 接口并定义了操作线程的一些方法,我们可以通过创建类时继承 Thread类来创建一个线程。
具体实现:
(1)创建一个继承Thread的类ThreadDemo
(2)重新run()方法
调用步骤:
(1)创建ThreadDemo 类的对象t1
(2)执行t1.start() 方法来启动线程

实现 Runnable

通过实现Runnable 接口来创建线程类 RThread,但是使用的时候,仍需要创建Thread 对象,把RThread的对象当成参数传入。
具体操作:
(1)实现Runnable 接口创建线程类 RThread
(2)重写run()方法
调用步骤:
(1)创建RThread 类的对象 rThread
(2)创建Thread类对象,并把rThread当成参数传入,相当于对rThread进行了封装。
(3)通过start()方法启动线程

Callable 创建

我们需要在主线程中开启多个线程去执行一个任务,然后收集各个线程的返回结果并将最终结果进行汇总,这是就需要用到 Callable 接口。
具体步骤:
(1)创建一个类实现Callable接口
(2)重写 call() 方法
调用步骤:
(1)创建线程池
(2)创建接收结果的列表集合
(3)创建线程对象
(4)将线程对象提交到线程池中,并将返回结果接收
(5)将返回结果加入结果集合
(6)关闭线程池

线程池创建线程

线程池创建线程本质上是默认通过 DefaultThreadFactory 线程工厂来创建的。最终还是需要通过 new Thread () 创建线程。

callable和runnable的区别

1、runnable没有返回值,而实现callable接口的任务线程能返回执行结果
2、Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

Thread类和Runable接口区别:

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

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

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

通过Thread实现线程时,一个线程只能启动一次,线程和线程所要执行的任务是捆绑在一起的。也就使得一个任务只能启动一个线程,不同的线程执行的任务是不相同的,所以没有必要,也不能让两个线程共享彼此任务中的资源。

通过Runnable方式实现的线程,一个任务可以启动多个线程,实际是开辟一个线程,将任务传递进去,由此线程执行。可以实例化多个 Thread对象,将同一任务传递进去,也就是一个任务可以启动多个线程来执行它。这些线程执行的是同一个任务,所以他们的资源是共享。
两种不同的线程实现方式本身就决定了其是否能进行资源共享。

实现线程执行的内容的两种写法:

实现 Runnable 接口从而实现 run () 的方式
继承 Thread 类重写 run () 方法的方式

哪种写法好?

答案是:Runnable 写法。

理由:

  1. 易于扩展
    这个不多说,Java 是单继承。如果使用继承 Thread 的写法。将不利于后续扩展。
  2. 解耦
    用 Runnable 负责定义 run () 方法(执行内容)。这种情况下,它与 Thread 实现了解耦。Thread 负责线程的启动以及相关属性设置。
  3. 性能
    在一些情况下可以提高性能。比如:线程执行的内容很简单,就是打印个日志。如果使用 Thread 实现,那它会从线程创建到销毁都要走一遍,需要多次执行时,还需要多次走这重复的流程,内存开销非常大。
    但是我们使用 Runnable 就不一样了。可以把它扔到线程池里面,用固定的线程执行。这样,显然是可以提高效率的。

线程安全

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

多线程并发时会不安全,多线程同时操作对象的属性或者状态时,会因为线程之间的信息不同步

如何实现线程安全

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

1.同步代码块
被synchronized关键字修饰

obj 称为同步监视器,也就是锁,
原理:当线程开始执行同步代码块前,必须先获得对同步代码块的锁定。并且任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
其中的锁,在非静态方法中可为this,在静态方法中为当前类本身(例如单例模式的懒汉式:Singleton.class)。

  synchronized(obj)
{
    //需要被同步的代码块
}

2.同步方法
使用synchronized关键字将一个方法声明为同步方法
不需要再指定同步监视器,这个同步方法(非static方法)无需显式地指定同步监视器,同步方法的同步监视器就是this,就是调用该方法的对象。

注意,synchronized可以修饰方法,修饰代码块,但是不能修饰构造器、成员变量等。

public synchronized void testThread()
{
    //需要被同步的代码块
}

3.同步锁
功能更为强大的线程同步机制,通过显式定义同步锁对象来实现同步,这里的同步锁由Lock对象充当。使用Lock与使用同步代码块有点类似,只是使用Lock时可以显示使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器。

其中,为了确保能够在必要的时候释放锁,代码中使用finally来确保锁的释放,来防止死锁!

lock.lock();
try{
    //需要被同步的代码块
    }catch(Exception e){
        e.printStackTrace();
    }finally{
        lock.unlock();
    }

Java怎么实现线程安全的单例

1.饿汉式单例
指在方法调用前,实例就已经创建好了

注意:要保证系统中不会有人意外创建多余的实例,便把构造函数设置为private,instance对象必须是private且是static的,如果不是private那么instance的安全性将无法保证,一个小小的意外可能使得instance变为null

存在问题:Singleton实例在什么时候创建是不受控制的,对于静态成员instance,它会在类第一次初始化的时候被创建,这个时刻并不一定是getInstance方法第一次被调用的时候

2.加入synchronized的懒汉式单例

优点:充分利用了延迟加载,只在真正需要时创建对象
缺点:并发环境下加锁,竞争激烈的场合对性能可能会产生一定的影响

3.双重校验锁

为什么用两个if判断这个对象是否是空?
因为当有多个线程同时创建对象的时候,多个线程有可能都停止在第一个if判断的地方,等待锁的释放,然后多个线程都创建了对象,这样就不是单例模式了.

4.使用枚举数据类型

多线程

多线程有什么用

1.发挥多核CPU的优势
2.防止阻塞
3.便于建模

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

如何控制线程的调度和阻塞

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

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

线程池

线程池概念

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

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

ThreadPoolExecutor线程池的常用参数

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

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

拒绝策略:

rejected = new ThreadPoolExecutor.AbortPolicy();//默认,抛出异常并中止执行
rejected = new ThreadPoolExecutor.DiscardPolicy();//忽略最新任务
rejected = new ThreadPoolExecutor.DiscardOldestPolicy();//将最早进入队列的任务删,之后再尝试加入队列
rejected = new ThreadPoolExecutor.CallerRunsPolicy();//把任务交给主线程执行

  1. New RejectedExecutionHandler重写rejectedExecution实现重写自定义拒绝策略

线程池执行流程

在这里插入图片描述

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要
    创建非核心线程立刻运行这个任务;
    d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池
    会抛出异常 RejectExecutionException。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小

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

(1)如果是CPU密集型应用,则线程池大小设置为 CPU的数量+1(或者是N).
(2)如果是IO密集型应用,则线程池大小设置为2 CPU的数量+1(或者是2N).
+1的原因是:即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费。

什么是死锁

死锁是指两个或两个以上的进程在运行过程中,由于争夺资源造成的阻塞现象,若无外力作用,条目将无法推进下去。

死锁形成必要条件

1.互斥条件:一个资源只能被一个线程占有
2.请求与保持条件:当一个线程阻塞时,对已有资源保持不放
3.不可剥夺条件:线程已获得的资源在未使用完不能被剥夺
4.循环等待条件:循环等待

避免死锁方法

1、死锁预防 ----- 确保系统永远不会进入死锁状态

  • 破坏“请求”条件 :一次性申请完
  • 破坏“保持”条件 : 只要一个资源得不到分配,也不给该进程分配其他资源
  • 破坏“不可剥夺”条件 :未申请到别的资源,主动放弃已有资源
  • 破坏“循环等待”条件:按序申请预防死锁

2、避免死锁 ----- 在使用前进行判断,只允许不会产生死锁的进程申请资源
银行家算法

synchronized与lock的区别,使用场景。

  1. Syncronized 是Java 中的一个关键字,Lock 是 Java 中的一个接口
  2. 锁的释放条件:1. Syncronized自动释放;2. Lock 必须在 finally 关键字中手动释放锁,不然容易造成线程死锁
  3. 锁的获取:
    Syncronized 中,假设线程 A 获得锁,B 线程等待。如果 A 发生阻塞,那么 B 会一直等待。在 Lock 中,会分情况而定,Lock 中有尝试获取锁的方法,如果尝试获取到锁,则不用一直等待。
  4. 锁的状态:Synchronized 无法判断锁的状态,Lock 则可以判断
  5. 锁的类型:Synchronized 是可重入,不可中断,非公平锁;Lock 锁则是 可重入,可中断,可公平锁
  6. 锁的性能:Synchronized 适用于少量同步的情况下,性能开销比较大。Lock 锁适用于大量同步阶段:
  7. 锁的使用场景:ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
    总体来说,Lock锁比synchronized更加灵活,提供了更加丰富的API进行同步操作,也可以结合条件实现比较复杂的线程间同步通信。

synchronized与ReentreLock的区别

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

独享锁/共享锁

独享锁:一次只能被一个线程所有

共享锁:可被多个线程共有

对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。

公平锁/非公平锁

公平锁:按照申请顺序取锁
非公平锁:不按。有可能后申请的线程比先申请的线程优先获取锁。有可能造成优先级反转或者饥饿现象。

  • 对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
  • 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

可重入锁

可重入锁:同一个线程在外层方法中获得到的锁,在进入方法内会自动得到。可一定程度避免死锁

互斥锁/读写锁

是具体的实现

互斥锁:在Java中的具体实现就是reentranlock
读写锁:在Java中的具体实现就是readwritelock

乐观锁/悲观锁

是指看待并发同步的角度

乐观锁:在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

分段锁

是一种锁的设计

分段锁:细化锁的颗粒,当仅针对数组中一项数据操作时,只对这一项加锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。

自旋锁

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
自旋锁的目的是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。
如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,忙循环就会变成浪费系统资源的操作,反而降低了整体性能。因此自旋锁是不适应锁占用时间长的并发情况的。

偏向锁

Java偏向锁(Biased Locking)是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。

偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。

轻量级锁

轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。
轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”这一经验法则。如果没有竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销;但如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下, 轻量级锁反而会比传统的重量级锁更慢。

重量级锁

如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,升级到重量级锁其实就是互斥锁了,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。

在 Java 中,synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。这一过程在后续讲解 synchronized 关键字的原理时会详细介绍。

分布式锁怎么实现?

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

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

CAS机制和版本号机制

1、CAS(Compare And Swap)(比较并且交换)
CAS操作包括了3个操作数:
需要读写的内存位置(V)
进行比较的预期值(A)
拟写入的新值(B)
如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。

2.版本号机制
在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。

乐观锁功能限制

与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。
例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。

CAS有哪些缺点

1、高竞争下的开销问题
在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。当然,更重要的是避免在高竞争环境下使用乐观锁。

2、功能限制
CAS的功能是比较受限的,例如CAS只能保证单个变量操作的原子性.
解决方法:

  • 如果需要对多个共享变量进行操作,可以使用加锁方式(悲观锁)保证原子性,
  • 可以把多个共享变量合并成一个共享变量进行CAS操作。

3、ABA问题
解决方法:

  • AtomicStampedReference 带有时间戳的对象引用来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
  • 在变量前面加上版本号,每次变量更新的时候变量的版本号都+1,即A->B->A就变成了1A->2B->3A

CAS适用场景

1、对于资源竞争较少的情况:CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

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

ThreadLocal

ThreadLocal是什么

多线程访问同一个共享变量的时候容易出现并发问题,ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

ThreadLocal使用场景

  1. 在进行对象跨层传递时使用,避免多次传递,打破层次间的约束
  2. 线程间数据隔离
  3. 进行事务操作,用于存储线程事务信息
  4. 数据库连接,session会话管理。

set方法

 public void set(T value) {
        //首先获取当前线程对象
        Thread t = Thread.currentThread();
        //获得当前线程对应的threadLocals
        ThreadLocalMap map = getMap(t);
        //如果不为空,
        if (map != null)
            map.set(this, value);
        else
            //如果为空,初始化该线程对象的map变量,其中key 为当前的threadlocal 变量
            createMap(t, value);
    }

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
}
    
//创建threadLocals ,key 为当前 threadlocal
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
     table = new Entry[INITIAL_CAPACITY];
     int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
     table[i] = new Entry(firstKey, firstValue);
     size = 1;
     setThreshold(INITIAL_CAPACITY);
}

Entry是继承的弱引用。使用静态内部类ThreadLocalMap中Entry使用ThreadLocal作为key,value自己设置。
对于每个线程内部有个名为threadLocals的成员变量,该变量的类型为ThreadLocal.ThreadLocalMap ,类似于一个HashMap,存取值的时候,也是从这个容器中来获取。

get

首先获取当前线程,然后通过key threadlocal 获取 设置的value 。

remove

remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量

每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。

public void remove() {
    //获取当前线程绑定的threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
     //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
     if (m != null)
         m.remove(this);
 }

ThreadLocalMap内部实现

内部实际上是一个Entry数组
是继承自WeakReference的一个类,该类中实际存放的key是指向ThreadLocal的弱引用和与之对应的value值(该value值就是通过ThreadLocal的set方法传递过来的值)
由于是弱引用,当get方法返回null的时候意味着坑能引用

当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。
考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

ThreadLocal内存泄漏问题;

ThreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的value不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这就造成了内存泄漏。
解决方法:使用完毕及时调用remove方法。

如果不remove 当前线程对应的VALUE ,就会一直存在这个值。
使用了线程池,可以达到“线程复用”的效果。但是归还线程之前记得清除ThreadLocalMap,要不然再取出该线程的时候,ThreadLocal变量还会存在。这就不仅仅是内存泄露的问题了,整个业务逻辑都可能会出错。

为什么key使用弱引用

如果使用强引用,当ThreadLocal 对象的引用(强引用)被回收了,ThreadLocalMap的Entry本身依然还持有ThreadLocal的强引用,如果没有手动删除这个key ,则ThreadLocal不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收, 可以认为这导致Entry内存泄漏。

强引用-软引用-弱引用

强引用:普通的引用,具有强引用的对象,只要这种引用还存在就不会被GC;
软引用:仅有软引用指向的对象,只有发生gc且内存不足,才会被回收;
弱引用:仅有弱引用指向的对象,只要发生gc就会被回收。

并发

Java有哪些并发手段

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

阻塞队列

比如队列中初始化n个元素,每次消费从队列中获取一个元素,如果拿不到则阻塞;执行完毕之后,重新塞入一个元素,这样就可以实现一个简单版的并发控制

AtomicInteger cnt = new AtomicInteger();

private void consumer(LinkedBlockingQueue<Integer> queue) {
    try {
        // 同步阻塞拿去数据
        int val = queue.take();
        Thread.sleep(2000);
        System.out.println("成功拿到: " + val + " Thread: " + Thread.currentThread());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        // 添加数据
        System.out.println("结束 " + Thread.currentThread());
        queue.offer(cnt.getAndAdd(1));
    }
}

@Test
public void blockQueue() throws InterruptedException {
    LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>(2);
    queue.add(cnt.getAndAdd(1));
    queue.add(cnt.getAndAdd(1));


    new Thread(() -> consumer(queue)).start();
    new Thread(() -> consumer(queue)).start();
    new Thread(() -> consumer(queue)).start();
    new Thread(() -> consumer(queue)).start();

    Thread.sleep(10000);
}

多并发怎么划分数组

并发包

通过上面分析,并发严重的情况下,使用锁显然效率低下,因为同一时刻只能有一个线程可以获得锁,其它线程只能乖乖等待。Java提供了并发包解决这个问题

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

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

多线程并发问题总结:

  1. 当只有一个线程写,其它线程都是读的时候,可以用volatile修饰变量
  2. 当多个线程写,那么一般情况下并发不严重的话可以用Synchronized,Synchronized并不是一开始就是重量级锁,在并发不严重的时候,比如只有一个线程访问的时候,是偏向锁;当多个线程访问,但不是同时访问,这时候锁升级为轻量级锁;当多个线程同时访问,这时候升级为重量级锁。所以在并发不是很严重的情况下,使用Synchronized是可以的。不过Synchronized有局限性,比如不能设置锁超时,不能通过代码释放锁。
  3. ReentranLock 可以通过代码释放锁,可以设置锁超时。
  4. 高并发下,Synchronized、ReentranLock 效率低,因为同一时刻只有一个线程能进入同步代码块,如果同时有很多线程访问,那么其它线程就都在等待锁。这个时候可以使用并发包下的数据结构,例如ConcurrentHashMap,LinkBlockingQueue,以及原子性的数据结构如:AtomicInteger。

数据结构

conccurentHashmap原理

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

ConcurrentHashMap底层实现原理

  1. ConcurrentHashMap的整体架构
  2. ConcurrentHashMap的基本功能
  3. ConcurrentHashMap在性能方面的优化

1.ConcurrentHashMap整体架构

  • ConcurrentHashMap在JDK1.8中的存储结构,是由数组、单向链表、红黑树组成。
  • 当我们初始化一个ConcurrentHashMap实例时,默认会初始化一个长度为16的数组。由于ConcurrentHashMap它的核心仍然是hash表,所以必然会存在hash冲突问题。
  • ConcurrentHashMap采用链式寻址法来解决hash冲突。当hash冲突比较多的时候,会造成链表长度较长,这种情况会使得ConcurrentHashMap中数据元素的查询复杂度变成O(n)。因此在JDK1.8中,引入了红黑树的机制。
  • 当数组长度大于64并且链表长度大于等于8的时候,单项链表就会转换为红黑树。另外,随着ConcurrentHashMap的动态扩容,一旦链表长度小于6,红黑树会退化成单向链表。

2.ConcurrentHashMap基本功能
ConcurrentHashMap本质上是一个HashMap,因此功能和HashMap一样,但是在hashmap基础上提供了并发安全的实现:通过node节点去加锁,来保障数据更新的安全性。

3.ConcurrentHashMap在性能方面的优化
如果在并发性能和数据安全性之间做好平衡,在很多地方都有类似的设计,比如cpu的三级缓存、mysql的buffer_pool、Synchronized的锁升级等等。ConcurrentHashMap也做了类似的优化,主要体现在以下几个方面:

  1. 在JDK1.8中,ConcurrentHashMap锁的粒度是数组中的某一个节点,而在JDK1.7,锁定的是Segment,锁的范围要更大,因此性能上会更低。
  2. 引入红黑树,降低了数据查询的时间复杂度,红黑树的时间复杂度是O(logn)。
  3. 当数组长度不够时,ConcurrentHashMap需要对数组进行扩容,在扩容的实现上,ConcurrentHashMap引入了多线程并发扩容的机制,简单来说就是多个线程对原始数组进行分片后,每个线程负责一个分片的数据迁移,从而提升了扩容过程中数据迁移的效率。
  4. ConcurrentHashMap中有一个size()方法来获取总的元素个数,而在多线程并发场景中,在保证原子性的前提下来实现元素个数的累加,性能是非常低的。ConcurrentHashMap在这个方面的优化主要体现在两个点:
  1. 当线程竞争不激烈时,直接采用CAS来实现元素个数的原子递增。
  2. 如果线程竞争激烈,使用一个数组来维护元素个数,如果要增加总的元素个数,则直接从数组中随机选择一个,再通过CAS实现原子递增。它的核心思想是引入了数组来实现对并发更新的负载。

JDK1.7的实现

ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成。
Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样
在这里插入图片描述

JDK1.8的实现

JDK1.8的实现已经摒弃了Segment的概念,而是直接用数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本
在这里插入图片描述

总结

JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言,总结如下思考

  1. JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
  2. JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
  3. JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
  4. JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点
  1. 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
  2. JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
  3. 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据

CopyOnWriteArayList原理

写时加锁,读时不加

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

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

内存

操作系统的页式存储

分区式存储管理最大的缺点是碎片问题严重,内存利用率低。究其原因,主要在于连续分配的限制,即它要求每个作用在内存中必须占一个连续的分区。
如果允许将一个进程分散地装入到许多不相邻的分区中,便可充分地利用内存,而无需再进行“紧凑”。
基于这一思想,产生了“非连续分配方式”,或者称为“离散分配方式”。
非连续分配:为用户进程分配的可以是一些分散的内存空间。

分页存储管理的思想:把内存分为一个个相等的小分区,再按照分区大小把进程拆分成一个个小部分。
分页存储管理分为:实分页存储管理和虚分页存储管理

实分页式存储基本原理

操作系统以页框为单位为各个进程分配内存空间。系统自动地将作业的地址空间分页,将系统的主存空间分块,页与块等大小,在作业运行时,一次性把作业的全部页面装入内存,各个页所占的内存块可以不连续,也不必按先后顺序,可以放到不相邻的各个页框中。
这实际是个把作业从地址空间映射到存储空间的过程

分页存储管理方案的评价

优点:
较好地解决了碎片问题
打破了存储分配的连续性要求
提高了主存的利用率

缺点:
页内碎片
动态地址变换、方案实施需耗用额外的系统资源
存储扩充问题没有解决——作业大小受到限制,可用块数小于作业需求时需等待

页表

在这里插入图片描述

时间局部性、空间局部性

时间局部性:一条指令的一次执行和下次执行,一个数据的一次访问和下次访问都集中在一个较短时期内;

空间局部性:当前指令和邻近的几条指令,当前访问的数据和邻近的数据都集中在一个较小区域内。

局部性原理的具体体现:

程序在执行时,大部分是顺序执行的指令,少部分是转移和过程调用指令。
过程调用的嵌套深度一般不超过5,因此执行的范围不超过这组嵌套的过程。
程序中存在相当多的循环结构,它们由少量指令组成,而被多次执行。
程序中存在相当多对一定数据结构的操作,如数组操作,往往局限在较小范围内。

引入虚拟存储技术的好处

大程序:可在较小的可用内存中执行较大的用户程序;
大的用户空间:提供给用户可用的虚拟内存空间通常大于物理内存(real memory)
并发:可在内存中容纳更多程序并发执行;
易于开发:与覆盖技术比较,不必影响编程时的程序结构

虚拟存储技术的特征

不连续性:物理内存分配的不连续,虚拟地址空间使用的不连续(数据段和栈段之间的空闲空间,共享段和动态链接库占用的空间)
部分交换:与交换技术相比较,虚拟存储的调入和调出是对部分虚拟地址空间进行的;
大空间:通过物理内存和快速外存相结合,提供大范围的虚拟地址空间

虚拟存储技术的种类

虚拟页式
虚拟段式
虚拟段页式

虚拟内存

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

一个进程是怎么使用它的内存

不太了解,我就知道进程通信方式)

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

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

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

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

linux

linux你会啥命令

cd [目录名]
切换文件路径

pwd
显示当前工作目录

rm 删除给定的文件

cp 对文件进行复制

mkdir t
当前工作目录下创建名为 t的文件夹

grep 在给定的文件中搜寻指定的字符串

exit 结束当前的终端会话。

ping 通过发送数据包ping远程主机(服务器),常用与检测网络连接和服务器状态。

su 用于切换不同的用户

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

netstat -anp |grep 端口号

Linux管道的作用

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

happen-before原则

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

【原则一】程序次序规则

在一个线程中,按照代码的顺序,前面的操作Happens-Before于后面的任意操作。

这个规则比较符合单线程的思维:在同一个线程中,程序在前面对某个变量的修改一定是对后续操作可见的。

【原则二】volatile变量规则

对一个volatile变量的写操作,Happens-Before于后续对这个变量的读操作。

也就是说,对一个使用了volatile变量的写操作,先行发生于后面对这个变量的读操作。这个需要大家重点理解。

【原则三】传递规则

如果A Happens-Before B,并且B Happens-Before C,则A Happens-Before C。

其实,Java 1.5版本的 java.util.concurrent并发工具就是靠volatile语义来实现可见性的。

【原则四】锁定规则

对一个锁的解锁操作 Happens-Before于后续对这个锁的加锁操作。

例如,下面的代码,在进入synchronized代码块之前,会自动加锁,在代码块执行完毕后,会自动释放锁。

【原则五】线程启动规则

如果线程A调用线程B的start()方法来启动线程B,则start()操作Happens-Before于线程B中的任意操作。

我们也可以这样理解线程启动规则:线程A启动线程B之后,线程B能够看到线程A在启动线程B之前的操作。

【原则六】线程终结规则

线程A等待线程B完成(在线程A中调用线程B的join()方法实现),当线程B完成后(线程A调用线程B的join()方法返回),则线程A能够访问到线程B对共享变量的操作。

【原则七】线程中断规则

对线程interrupt()方法的调用Happens-Before于被中断线程的代码检测到中断事件的发生。

【原则八】对象终结原则

一个对象的初始化完成Happens-Before于它的finalize()方法的开始。

同步、异步

同步、异步定义:

进程同步:这是进程间的一种运行关系。“同”是协同,按照一定的顺序协同进行(有序进行),而不是同时。即一组进程为了协调其推进速度,在某些地方需要相互等待或者唤醒,这种进程间的相互制约就被称作是进程同步。这种合作现象在操作系统和并发式编程中属于经常性事件。具有同步关系的一组并发进程称为合作进程。

进程异步: 是指进程以不可预知的速度向前推进。内存中的每个进程何时执行,何时暂停,以怎样的速度向前推进,每道程序总共需要多少时间才能完成等,都是不可预知的。

同步、异步理解:

同步: 两个进程的运行是相关的,其中一个进程要阻塞等待另外一个进程的运行。

异步: 两个进程毫无相关(不用互相等待),自己运行自己的。

补充:并行性是指两个或多个事件在 同一时刻 发生;而并发性是指两个或多个事件咋 同一时间间隔内 发生。

同步、异步区别:

同步是阻塞模式,异步是非阻塞模式。
进程同步/异步指的是进程之间的运行关系,但是阻塞和非阻塞是访问资源的一种运行状态

伪共享

缓存行cache line

CPU缓存是由缓存行组成的,一个缓存行一般是64个字节,CPU读取数据是以缓存行为单位的读取,这意味着即使是读1个字节的数据,CPU也要读取这个数据所在的连续的64个字节的数据,如果使用的数据结构中的数据项不是彼此相邻连续的,如链表,那么读数据的时候就得不到免费缓存带来的好处,在java中,数组中的数据通常是连续的(数组的连续存储不是jvm规范中的要求,在某些jvm中,大中型数据项不是分配在连续的空间),所以数组的访问速度比链表要快。

缓存失效

在java中,long类型占8个字节,这就意味着,当读一个long类型的变量,也会读取其相邻的7个long类型变量(不是long类型的变量按占用的字节数计算个数,如int类型的变量占4个字节,那么就64-8个字节,就可以存储14个int类型的变量),在基于mesi协议下,其它的线程此时再读取其中的一个long类型变量,那么这个long类型变量所在的缓存行就会失效,需要重新读取缓存,这就是缓存失效。

伪共享

CPU在读取数据时,是以一个缓存行为单位读取的,假设这个缓存行中有两个long类型的变量a、b,当一个线程A读取a,并修改a,线程A在未写回缓存之前,另一个线程B读取了b,读取的这个b所在的缓存是无效的(前面说的缓存失效),本来是为了提高性能是使用的缓存,现在为了提高命中率,反而被拖慢了,这就是传说中的伪共享。

如何消除伪共享

  1. 在变量的后背凑齐64个字节的变量
  2. 使用消除了伪共享结构的类
  3. 使用@sun.misc.Contended 注解
  1. 在变量的后背凑齐64个字节的变量
class Pointer {
    //在一个缓存行中,先会存储a
    volatile long a;  //需要volatile,保证线程间可见并避免重排序
//    放开下面这行,解决伪共享的问题,提高了性能
    long p1, p2, p3, p4, p5, p6, p7;
    volatile long b;   //需要volatile,保证线程间可见并避免重排序
}
  1. 使用消除了伪共享结构的类
class Pointer2 {
    MyLong a = new MyLong();
    MyLong b = new MyLong();
}

class MyLong {
    volatile long value;
    long p1, p2, p3, p4, p5, p6, p7;
}
  1. 使用@sun.misc.Contended 注解
@sun.misc.Contended
class MyLong {
    volatile long value;
}

或者:
class Pointer {
    volatile long a;
    @Contended
    volatile long b;
}

使用了伪共享的大牛例子

  1. jdk中的ConcurrentHashMap类

小结

  1. CPU具有多级缓存,越接近CPU的缓存越小也越快;
  2. CPU缓存中的数据是以缓存行为单位处理的;
  3. CPU缓存行能带来免费加载数据的好处,所以处理数组性能非常高;
  4. CPU缓存行也带来了弊端,多线程处理不相干的变量时会相互影响,也就是伪共享;
  5. 避免伪共享的主要思路就是让不相干的变量不要出现在同一个缓存行中;
  6. 一是每两个变量之间加七个 long 类型;
  7. 二是创建自己的 long 类型,而不是用原生的;
  8. 三是使用 java8 提供的注解;
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值