面试之线程相关
1.什么是进程?
1.进程是系统中正在运行的一个程序,程序一旦运行就是进程。
2.进程可以看做程序执行的一个实例,进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间的通信,比如管道,文件,套接字
3.一个进程可以拥有多个线程,每个线程使用所属进程的栈空间。同一进程内多个线程会共享部分状态,多个线程可以读写一块内存。同时每一个线程还拥有自己的寄存器和栈,其他线程可以读写这些栈内存。
2.什么是线程?
1.线程是操作系统能够进行运算调度的最小单位。
2.线程是进程的一个实体,是进程的一条执行路径,当一个线程修改了资源,他的兄弟线程可以立即看到这种变化。
3.并发和并行和串行
1.并行指在同一时刻,有多条指令在多个处理器上同时执行;而并发是指两个或多个事件在同一时间间隔(宏观上)发生。
2.并行是在不同实体的多个事件,并发是在同一实体的多个事件
3.并发是指一个处理器同时处理多个任务。并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
4 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行
5.串行:多个任务,一个任务执行完成后再执行另外一个,例如 吃完饭再看球赛。串行即线程同步。
4.Volatile
1.volatile特性:
可见性
不保证原子性
禁止指令重排
2.volatile为什么能保证可见性?
内存屏障,是一条cpu指令,当使用变量volatile修饰时,将会在写操作后面加一条屏障指令,在读操作前面加一条屏障指令。这样的话一旦写入完成,可以保证其他线程可以读取到最新值,也就保证了可见性。
3 什么叫原子性?
所谓原子性,就是说一个操作不可被分割或加塞,要么全部执行,要么全不执行。
4.什么叫指令重排?
在多线程情况下,计算机为了提高执行效率,就会对这些步骤进行重排序,这就叫指令重排,添加volatile可以防止指令重排。
5.volatile的应用?
单例模式
5.什么叫线程安全?
1.线程安全指的是,在堆内存的数据由于可以被任何线程访问到,在没有限制的情况下存在被意外修改的风险。即在堆内存空间在没有保护机制的情况下,对多线程来说是不安全的地方,因为你放进去的数据,可能被其他数据破坏。
2.线程安全性问题出现的三个必要条件?
1.多线程环境下
2.多个线程共享一个资源
3.对资源进行非原子性操作
6.实现线程安全的方式有哪些?优缺点是什么?
1.同步代码块
2.同步方法
3.lock锁机制,通过创建Lock对象,采用lock()加锁,unlock()解锁,来保护指定代码块。
优缺点:由于synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否;而ReentrantLock是使用代码实现的,系统无法自动释放锁,需要在代码中的finally子句中显式释放锁lock.unlock()。
另外,在并发量比较小的情况下,使用synchronized是个不错的选择;但是在并发量比较高的情况下,其性能下降会很严重,此时ReentrantLock是个不错的方案。
7.创建多线程的方式有哪几种,常用哪一种,优缺点是什么?
1.继承Thread类
2.实现Runnable接口
3.实现Callable接口
优缺点:
1.采用实现Runnable、Callable接口的方式创建多线程的优缺点:
优势:(1)线程类只是实现了Runnable接口与Callable接口,还可以继承其他类。
(2)在这种方式下,多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势:编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
2.采用继承Thread类的方法创建多线程的优缺点:
劣势:因为线程类已经继承了Thread类,所以不能再继承其他父类。
优势:编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。
一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。
8.线程阻塞
1.什么是线程阻塞?
在某一时刻某一个线程在运行一段代码的时候,这时候另外一个线程也需要运行,但是运行过程中的那个线程执行完成之前,领一个线程是无法获取到cpu执行权的,这个时候就会造成线程阻塞。
2.导致线程阻塞的原因?
1.睡眠状态:当一个线程执行代码的时候调用了sleep方法时,线程处于睡眠状态,需要设置一个睡眠时间,此时如果有其他线程需要执行,就会造成线程阻塞,当休眠时间过了以后,线程不会释放锁,cpu执行权还在自己手里,该线程会进入就绪状态。
2.等待状态:当一个线程运行时调用了wait方法,此时该线程需要交出cpu执行权,也就是将锁释放掉,交给另外一个线程,需要执行notify方法或者notifyAll方法,否则不会醒来。
3**.礼让状态**:当一个线程正在运行的时候,调用了yield方法之后,该线程会将执行权礼让给同等级或者高自己一个等级的线程优先执行,此时线程可能只执行了一部分而此时的执行权交给了其他线程,这个时候也会进入阻塞状态,但是随时可能被分配到执行权。
4.自闭状态:当一个线程正在运行时,调用一个join方法此时,该线程就会进入阻塞状态,另外一个线程会运行,直到运行结束,原线程才会进入就绪状态。
9.线程异步
1.异步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制的存在,A线程仍然能请求到,A线程无需等待。
2.同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为有同步机制的存在,A线程仍然能请求到,A线程只能等待。
3.同步是最安全,最保险的,而异步不安全,容易导致死锁,这样一个线程死掉就会导致整个进程崩溃,但是没有同步机制的存在,性能会有所提升。
10.sleep和wait的区别
1.sleep继承的是Thread类,而wait继承的是Object类
2.sleep睡眠过后不会释放锁,而调用wait方法后会导致本线程释放对象锁。
3.调用sleep方法后,到达指定时间会自动苏醒,而调用wait方法只能采用notify或者notifyAll让其苏醒。
11.锁
锁:解决资源占用的问题;保证同一时间一个对象只有一个线程在访问;
锁机制的作用:有些业务逻辑在执行过程中要求对数据进行排他性的访问,于是需要通过一些机制保证在此过程中数据被锁住不会被外界修改,这就是所谓的锁机制。
饥饿:是指如果线程T1占用了资源R,线程T2又请求封锁R,于是T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求资源R,当T3释放了R上的封锁之后,系统又批准了T4的请求…,T2可能永远等待。(就好比食堂打饭,刷卡的优先打饭,付现金的要等刷卡的打完了才能打,可是拿着现金的很早就在那儿准备好了,可以刷卡的那条队伍却一直来了一个又一个,来个没完,拿现金的只好饿死。这也就是ReentrantLock显示锁里提供的不公平锁机制(当然了,ReentrantLock也提供了公平锁的机制,由用户根据具体的使用场景而决定到底使用哪种锁策略),不公平锁能够提高吞吐量但不可避免的会造成某些线程的饥饿。)
死锁:在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。尽管死锁很少发生,但一旦发生就会造成应用的停止响应(就像夫妻吵架,都等着对方先道歉,就会造成死锁)
活锁:是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。
互斥锁:对共享资源的访问必须是顺序的,也就是说当多个线程对共享资源访问的时候,只能有一个线程可以获得该共享资源的锁,当线程A尝试获取线程B的锁时,线程A必须等待或者阻塞,直到线程B释放该锁为止,否则线程A将一直等待下去,因此java内置锁也称作互斥锁,也即是说锁实际上是一种互斥机制。
死锁和饥饿的区别:
·死锁进程等待永远不会被释放的资源,饿死进程等待会被释放但却不会分配给自己的资源,表现为等待时限没有上界(排队等待或忙式等待);
·死锁一定发生了循环等待,而饿死则不然。这也表明通过资源分配图可以检测死锁存在与否,但却不能检测是否有进程饿死;
·死锁一定涉及多个进程,而饥饿或被饿死的进程可能只有一个。
·在饥饿的情形下,系统中有至少一个进程能正常运行,只是饥饿进程得不到执行机会。而死锁则可能会最终使整个系统陷入死锁并崩溃
可重入锁:
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。什么是可重入性?举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2.
method1和method2都是synchronized修饰的方法,在method1里面调用method2的时候,不需要重新申请锁,可以直接调用就行了(其实可以反过来想一想,如果synchronized不具有重入性,当我调用了method1的时候,得申请锁,申请好了之后那么method1就拥有了这个锁,那么调用method2的时候,又要重新申请锁,而锁在method1的手上,这时候又要重新申请锁,显然是不可能得到的,这不科学。所以,synchronize和lock都是具有可重入性的)
可中断锁:如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
非公平锁:刚刚讲到的食堂打饭的例子,就是一个不公平锁的例子;synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。这样就可能导致某个或者一些线程永远获取不到锁。
公平锁:公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。
读写锁:就是将一个资源的访问分成两个锁,一个读锁,一个写锁;正因为有了读写锁,才使得多个线程之间的读写操作不会发生冲突。ReadWriteLock就是读写锁,可以通过readLock()获取读锁,通过writeLock()获取写锁。
自旋锁:举个例子:获取到资源的线程A对这个资源加锁,其他线程比如B要访问这个资源首先要获得锁,而此时A持有这个资源的锁,只有等待线程A逻辑执行完,释放锁,这个时候B才能获取到资源的锁进而获取到该资源。这个过程中,A一直持有着资源的锁,那么没有获取到锁的其他线程比如B怎么办?通常就会有两种方式:
- 一种是没有获得锁的进程就直接进入阻塞(BLOCKING),这种就是互斥锁
- 另外一种就是没有获得锁的进程,不进入阻塞,而是一直循环着,看是否能够等到A释放了资源的锁,这种就是自旋锁
什么时候用自旋锁比较好?如果A线程占用锁的时间比较短,这个时候用自旋锁比较好,可以节省CPU在不同线程间切换花费的时间开销;如果A线程占用锁的时间比较长,那么使用自旋锁的话,B线程就会长时间浪费CPU的时间而得不到执行(要执行一个线程需要CPU,并且需要获得锁),这个时候不建议使用自旋锁;还有递归的时候尽量不要使用自旋锁,可能会造成死锁。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。这样可以保证每次都只有一个线程在访问这个数据;传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
12.线程池
1**.线程池**:一个池中配置 固定个数目的 线程
2.线程池的优势:
(1)、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
(4)提供更强大的功能,延时定时线程池。
13.线程的五种状态
1.新建状态(New):
当用new操作符创建一个线程时, 例如new Thread®,线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码
2.就绪状态(Runnable)
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
3.运行状态(Running)
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.
4. 阻塞状态(Blocked)
线程运行过程中,可能由于各种原因进入阻塞状态:
1>线程通过调用sleep方法进入睡眠状态;
2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
3>线程试图得到一个锁,而该锁正被其他线程持有;
4>线程在等待某个触发条件;
…
所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
5. 死亡状态(Dead)
有两个原因会导致线程死亡:
1) run方法正常退出而自然死亡,
2) 一个未捕获的异常终止了run方法而使线程猝死。
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.
来源:CSDN
作者:qq_45136592
链接:https://blog.csdn.net/qq_45136592/article/details/104733374