下面是多线程面试题的总结,后续碰到重点的新题型会不断继续更新。
单核:比如一个线程使用的时候CPU计算,IO空闲,IO操作时候CPU空闲,当CPU空闲时,执行另一个线程IO操作,这样提高CPU利用率。
多核,提高CPU利用率,多核分别执行多个线程。比如一个复杂任务,让多核并行,提高效率。
**问题:**内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
进程:是系统运行程序的基本单位比如一个迅雷应用程序,进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。一个进程中可以有多个线程,
线程: 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程是进程的执行的基本单位比如为在迅雷程序中开启多个下载任务下载就是多线程。
在线程里面可以开启协程,让程序在特定的时间内运行。也就是说一个线程执行不同的协程。
协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,**程序员必须自己承担调度的责任,**同时,协程也失去了标准线程使用多CPU的能力。
在单核上,CPU决定线程A和B的执行权。
在双核上,并行,一个核执行线程A一个核执行线程B。
而协程则是由程序员控制执行权,比如我可以让一个协程A同时运行在两个核上,执行完之后再使用双核同时运行协程B。
cpu执行一个线程时,而切换到另一个线程去执行。每个线程都是竞争CPU执行权的,比如CPU只有单核,那么不可能同时执行多个线程,CPU就会控制先执行谁后执行谁,当切换执行权的时候就叫做多线程上下文切换。
上下文切换通常是计算密集型的。也就是说,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
如何减少上下文切换:
各式各样的答案有很多,一个人给出的很好的解释是:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
这里可以举例说明不安全的一个实例,就是多线程处理共享数据比如仅剩一张票时,多线程操作会造成重复卖票情况。
同时持有资源又同时申请资源。
死锁四个必备条件:
互斥条件:该资源任意一个时刻只由一个线程占用。
不剥夺条件: 线程已获得的资源不能被其他线程强行剥夺,使用完毕后释放。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系。
破坏任意一个条件都可以防止死锁都产生。通常我们破坏循环等待。
破坏循环等待条件。
靠按序申请资源来预防。按某一顺序申请资源锁,释放资源则反序释放。破坏循环等待条件。
都造成线程阻塞。
两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。
Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。
sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
start方法会开启新线程然后调用run方法运行线程
而如果直接调用run方法,则只是在main线程中执行run方法的内容,并不会创建新的线程。
三种:
(1)继承Thread类
(2)实现Runnable接口
(3)通过Callable和Future创建线程JDK5升级
逐渐升级:
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
而Callable+Future/FutureTask却可以方便获取多线程运行的结果和线程运行状态等信息
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,所以他会开启新线程并执行run方法
当你调用run()方法的时候,只会是在原来的线程中调用方法,没有新的线程启动。所以还是单线程的。
阻塞状态也就是说主动放弃CPU执行权处于等待状态。
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
synchronized 他主要是在虚拟机层面实现而没有直接暴露给程序员。而ReentrantLock是API层面,比如说我们可以显示的调用Lock和unlock来实现。加锁和解锁。
synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,
ReentrantLock类借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。
在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
volatile 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序
(1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据
(2)现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下就会产生一些问题。使用volatile则会对禁止语义重排序避免问题,但是这也一定程度上降低了代码执行效率。
volatile与synchronized区别
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。
因为拿生产者消费者模型来说,如果只使用加锁同步synchronized,则我们生产者在生产的时候就不会释放锁一直生产,而消费者不能消费。使用等待唤醒,wait()方法来让线程等待,比如生产完一个就阻塞,然后暂时释放锁给消费者。
配合lock锁可以指定解锁对方线程。比如生产者消费者,可以指定解锁消费者线程。
如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放
Thread类提供了一个holdsLock(Object obj)
方法,当且仅当对象obj的监视器被当前线程持有的时候才会返回true,他一个static方法。
创建ThreadPoolExecutor 的方式,有三种类型的线程池,比如:创建只含单独线程的线程池,固定数量的线程池,创建可调整数量的线程池。
CountDownLatch, 一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
需求:解析一个文件下多个txt文件数据,可以考虑使用多线程并行解析以提高解析效率。每一个线程解析一个文件里的数据,等到所有数据解析完毕之后再进行其他操作。
CyclicBarrier
CyclicBarrier,让一组线程到达一个同步点后再一起继续运行,在其中任意一个线程未达到同步点,其他到达的线程均会被阻塞。
需求:多线程计算数据,merge计算结果,然后多个线程再一起运行。
也称后台线程,守护线程和前台线程一起运行,但是当所有前台线程执行完了,守护线程也就自动结束,守护线程最典型的例子就是GC线程
悲观锁:总是假设最坏的情况,每次拿到数据时都会上锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
适用场景:
像乐观锁适用于写比较少的情况下(多读情况)冲突会比较少。这样可以省去了锁的开销
一般如果冲突产生比较多,多写的场景下用悲观锁就比较合适。
ReentrantLock全程为可重入锁,可重入意思就是线程可以多次获取锁。ReentrantLock实现了Lock接口,加锁和解锁都需要显式写出。