1、死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。
死锁产生的四个必要条件?
- 1、互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 2、请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 3、不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 4、环路等待条件:在发生死锁时,必然存在一个进程—资源的环形链。
解决死锁的基本方法?
预防死锁:
- 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
- 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
- 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
- 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
避免死锁:
-
银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。
当进程请求资源时,假设同意该请求,看系统的状态,安全,同意,不安全,阻塞
Resource(系统每种资源的总量) Available (未分配的每种资源总量) Claim (该请求对资源的请求) Allocation(当前分配给进程的资源)
检测死锁:
1、每个进程和资源都有唯一号码
2、建立资源分配表和进程等待表
解除死锁:
1、剥夺资源:从其他进程种,抽取资源
2、撤销进程:撤销死锁进程或代价最小的进程
死锁检测:
1、Jstack
2、Jconsole JDK自带检测
2、线程和进程、协程的区别
进程是程序的一次执行过程,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间,至少有 5 种基本状态,初始态,执行态,等待状态,就绪状态,终止状态。
线程是cpu资源分配和调度的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
协程被操作系统内核所管理,而完全是由程序所控制,也就是用户态。协程的开销要远远比线程小。
区别:
-
进程是系统资源分配和调度的最小单位,线程是cpu资源分配和调度的基本单位。
-
开销方面: 每个进程都有独立的代码和数据空间(程序上下文),进程之间切换开销大;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
-
系统为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源
-
进程中至少包含一个线程
线程创建方式
-
通过继承Thread创建线程
- 定义Thread类的子类,重写run()方法,run()方法里面是线程的方法体。
- 创建子类对象,即线程对象
- 线程对象调用start()方法启动线程。
-
通过实现Runnable接口创建线程
- 定义Runnable接口的实现类,重写run()方法,run()方法里面是线程的方法体。
- 创建Runnable实现类对象,依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 线程对象调用start()方法启动线程。
注意:run()只是线程想要执行的内容,没有返回值。
- 通过实现Callable和FutureTask创建线程
- 定义Callable接口的实现类,重写call()方法,call()方法里面是线程的方法体,有返回值。
- 创建Callable实现类对象,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
注意:call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
线程几种状态
- 新建状态(New):
用new语句创建的线程处于新建状态,此时它和其他Java对象一样,仅仅在堆区中被分配了内存。 - 就绪状态(Runnable):
当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态,Java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得CPU的使用权。 - 运行状态(Running):
处于这个状态的线程占用CPU,执行程序代码。只有处于就绪状态的线程才有机会转到运行状态。 - 阻塞状态(Blocked):
阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会转到运行状态。-
等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。
-
同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态。
-
其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或超时、或者I/O处理完毕时,线程重新转入就绪状态。
-
- 死亡状态(Dead):
当线程退出run()方法时,就进入死亡状态,该线程结束生命周期。
线程方法
-
1、Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
-
2、Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
-
3、thread.join()/thread.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)。
-
4、obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。 -
5、LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒。
3、用户态、内核态
内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。
所有用户程序都是运行在用户态的, 但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据, 或者从键盘获取输入等. 而唯一可以做这些事情的就是操作系统, 所以此时程序就需要先操作系统请求以程序的名义来执行这些操作.
用户态切换到内核态的方式
a. 系统调用
b. 异常
c. 外围设备的中断
4、悲观锁、乐观锁
5、线程池
工作原理:1、< corepoolsize 直接加入线程池工作
2、> corepoolsuze 加入队列 ,成功等待,失败 <= maximumpoolsize 进入线程池,> maximunpoolsize 拒绝策略
ThreadPoolExecutor的有哪些参数
corepoolsize, 线程数
MaximumPoolSize,最大线程数
BlockQueue,阻塞队列(
1、无界队列 (LinkedBlockingQueue)一直往后加
2、有界对列 FIFO (ArrayBlockingQueue), 优先对列(PriorityBlockingQueue)
3、同步移交对列 SynchronizedQueue
ArrayBlockingQueue中入队和出队操作,使用同一个lock,所以无法并发。
LinkedBlockingQueue读和写有两把锁ReentrantLock takeLock和putLock,它们之间的操作互相不受干扰,因此两种操作可以并行完成。
LinkedBlockingQueue的吞吐量要高于ArrayBlockingQueue。
)
handle,队列满后拒绝策略
AbortPolicy 抛出RejectedExecutionException
DiscardPolicy 什么也不做,直接忽略
DiscardOldestPolicy 丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置
CallerRunsPolicy 直接由提交任务者执行这个任务
keepAliveTime,空闲线程等待时间
unit,时间单位
threadFactory 创建线程的工厂类
线程池种类
(single,scheme,fixed,cache),初始化线程种类、拒绝策略、线程池执行。
线程池引发的故障到底怎么排查
jps -l找到项目的进程号(jps -l)
jstack dump线程信息,如下会将线程信息dump到一个名为thread.txt的文件中(jstack 16555 > thread.txt)
6、进程间通信方式有哪些,优缺点呢
-
管道
在内核中申请一块固定大小的缓冲区,程序拥有写入和读取的权利。1,面向字节流 2,生命周期随内核 3,自带同步互斥机制 4,半双工,单向通信,两个管道实现双向通信
-
消息队列
在内核中创建一队列,队列中每个元素是一个数据报,不同的进程可以通过句柄去访问这个队列。 -
共享存储
将同一块物理内存一块映射到不同的进程的虚拟地址空间中,实现不同进程间对同一资源的共享。 -
信号
-
信号量
在内核中创建一个信号量集合(本质是个数组),数组的元素(信号量)都是1,使用P操作进行-1,使用V操作+1 -
套接字
7、并发与并行?
并发是指多个事件在同一时间段内发生,因此这多个事件是发生在同一实体上的。并发的本质是一个cpu在多个的程序之间进行多路复用,也就是多个用户共享同一个物理资源。
并行是指多个事件在同一时刻发生,因此这多个事件是发生多个实体上。并行的本质是不同的程序在不同的cpu上同时 运行。
并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。而并发编程可能会遇到很多问题,比如 :内存泄漏、上下文切换、线程安全、死锁等问题。
并发编程三要素:
- 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
- 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
- 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
(原子性(单一线程)、可见性 (内存强制刷新)、有序性(as-id-serial、happens-before))
出现线程安全问题的原因:
- 线程切换带来的原子性问题
- 缓存导致的可见性问题
- 编译优化带来的有序性问题
解决办法:
- JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
- synchronized、volatile、LOCK,可以解决可见性问题
- Happens-Before 规则可以解决有序性问题
8、多线程
多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。
多线程的好处:
- 可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
多线程的劣势:
-
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
-
多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
-
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
9、线程同步和互斥?
同步就是协同步调,按预定的先后次序进行运行。这里的同步千万不要理解成那个同时进行,应是指协同、协助、互相配合。
线程同步是指多线程 通过特定的设置(如互斥量,事件对象,临界区)来控制线程之间的执行顺序(即所谓的同步) 也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间是各自运行各自的!
线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步(下文统称为同步)。
10、IO网络模型
网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作
-
阻塞IO
blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。没有数据你就一直等着。 -
非阻塞IO(non-blocking IO)
在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有,这种方式称为轮询(polling)。。没有数据,直接和我说没有,我等会再来。 -
多路复用IO(IO multiplexing)
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。监视多个连接,谁有数据谁就给我。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。 -
select,poll,epoll都是IO多路复用的机制。
-
在IO多路复用中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的进程其实是一直被block的。只不过进程是被select这个函数block,而不是被socket IO给block。所以IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。
-
异步IO
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
同步 I/O:应用进程在调用 recvfrom 操作时会阻塞。
异步 I/O:不会阻塞。
select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。
传统意义上,进程的开销要远远大于线程,所以如果需要同时为较多的客户机提供服务,则不推荐使用多进程;
如果单个服务执行体需要消耗较多的CPU资源,譬如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。
在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程, 即父进程永远无法预测子进程 到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
孤儿进程: 一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程: 一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程控制块(PCB)仍然保存在系统中。这种进程称之为僵尸进程。
并发编程面试题(2020最新版)
网络io模型