java面试题:多线程与并发

多线程

关键词:线程,锁

Q:如何新建一个线程?
继承Thread,或者实现Runnable接口,或者通过Callable接口实现。
Q:Callable有什么区别?
Callable接口,有一个call()方法,可以返回值。
Q:讲一下Callable接口、Future接口、FutureTask类
Callable可以作为FutureTask的方法参数。
而FutureTask类间接实现了Future接口。
FutureTask进行多线程操作时,可以通过Future接口的get()获取返回结果,也就是通过FutureTask实现异步。
Q:在T1线程中A B C三个方法依次执行,假如B方法通过Future接口的get()方法获取异步结果,那么T1线程是否会阻塞
Future实现类的get()方法会导致主线程阻塞,直到Callable任务执行完成;
Q:线程有哪些状态?
新建,就绪,运行,阻塞,停止
阻塞可以是sleep(),wait(),或者join()
Q:如何查看线程的运行状态?
使用Thread类的getState()方法可以获得线程的状态,该方法的返回值是Thread.state,他是线程状态的枚举。
分别是NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED。
Q: sleep() 和 wait() 的区别?
所属的类不一样。Thread.sleep(1000); 而wait()是属于Object的。
sleep()不会释放锁,而wait()会释放锁。
sleep()可以在任何地方使用。而wait,notify,notifyAll只能在同步控制方法或者同步控制块中使用。
sleep()必须捕获异常,而wait,notify,notifyAll的不需要捕获异常。
Q: java多线程之间,是如何进行通信的?
wait,notify,join,yield,intercept。
Q:死锁是怎么回事?
死锁,就是两个(或多个)线程对彼此加锁的资源进行加锁,导致彼此等待而永远阻塞。
比如,如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。线程1永远得不到B,线程2也永远得不到A,并且它们永远也不会知道发生了这样的事情。为了得到彼此的对象(A和B),它们将永远阻塞下去。这种情况就是一个死锁。
Q:如何避免死锁?如何解决死锁?
破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

Q:在实践中,有没有使用过多线程?
可以使用多线程同时执行多个任务,提高程序运行的效率。
还可以通过异步来处理耗时操作。比如一个线程做为主线程,新建另一个线程进行下载任务,这样主线程就不需要等待耗时操作。
Q:有三个线程,如何使它们顺序执行?
使用join,可以让某个线程在另一线程之前执行。
Q:假如有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现?
使用ForkJoinPool 。可以进行fork()分而治之,将任务进行分解,然后合并所有的结果。
Q:如何中断线程?中断线程意味着什么?
interrupt() 方法只是改变中断状态而已,它不会中断一个正在运行的线程。
如果线程被wait, join和sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。
如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException。

线程池

Q:线程池有没有了解过?为什么要用线程池?
为了避免频繁的创建和销毁线程,让创建的线程进行复用,就有了线程池的概念。
线程池里会维护一部分活跃线程,如果有需要,就去线程池里取线程使用,用完即归还到线程池里,免去了创建和销毁线程的开销,且线程池也会线程的数量有一定的限制。
Q:线程池的submit()和execute()方法区别?
submit()有返回值,而execute()没有。
Q:线程池的参数有哪些?

  • 参数如下:
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
  • corePoolSize:核心线程池里面的线程数量
  • maximumPoolSize:线程池允许的最大线程数,它表示在线程池中最多能创建多少个线程;
  • keepAliveTime:表示当线程池的个数大于核心数量corePoolSize时,线程的空闲时间达到指定的时间时会被销毁。
  • unit:参数keepAliveTime的时间单位
  • workQueue:一个阻塞队列,用来存储等待执行的任务。当请求的线程数大于corePoolSize时,线程会进入这个BlockingQueue阻塞队列。
  • handler:执行拒绝策略的对象。当阻塞队列workQueue的任务缓存区到达上限,并且活动的线程数大于maximumPoolSize的时候,线程池会执行拒绝策略。
  • threadFactory: 定义如何启动一个线程,可以设置线程的名称,并且可以确定是否是后台线程等。
    Q:假设我们有一个线程池,核心线程数为10,最大线程数也为20,任务队列为100。现在来了100个任务,线程池里现在有几个线程运行?"
    有两种情况:
    1.核心线程数用完了,先进队列,到最大值,再起线程
    JDK中的线程池,也就是ThreadPoolExecutor就是这种机制的。
    核心线程数10用完了,剩下的90个进入了任务队列。因此现在线程池里有10个线程运行。
    2.核心线程数用完了,先起线程,到最大值,再进队列
    在dubbo中,有一种线程池叫EagerThreadPoolExecutor线程池。在Tomcat里面也有类似的线程池。
    核心线程数10用完了,剩下的线程先达到最大值20,然后再剩下的才会进任务队列。因此现在线程池里有20个线程运行。
    Q:拒绝策略有哪些?
    拒绝策略有以下几种:
    ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。 (默认)
    ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
    ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列中等待最久的任务,然后把当前任务加入队列。
    ThreadPoolExecutor.CallerRunsPolicy:由调用任务的run()方法绕过线程池执行此线程。
    Q:阻塞队列有哪些?
    Q:阻塞队列的默认值是多少?
    ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列
    LinkedBlockingQueue:一个基于链表结构的阻塞队列。LinkedBlockingQueue如果不指定大小,就是一个无界队列。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
    Q:线程池有哪些类型?有什么不同?
    1.newCachedThreadPool:
    new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory);
    newCachedThreadPool里面的corePoolSize核心线程数为0,最大线程数设置为最大的Integer.MAX_VALUE。
    由于newCachedThreadPool是任意伸缩的线程池,如果最大线程数maximumPoolSize达到最大,那么会导致OOM异常。

2.newScheduledThreadPool:
newScheduledThreadExecutor可以定时或者周期性执行任务。
如果最大线程数maximumPoolSize达到最大,那么会导致OOM异常。

3.newFixedThreadPool:
ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
newFixedThreadPool是 具有线程池提高程序效率和节省创建线程时所耗的开销的优点。
但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
由于使用的LinkedBlockingQueue()是一个无界队列,队列长度可达到Integer.MAX_VALUE,如果瞬间请求非常大,会有OOM的风险。

4.newSingleThreadExecutor:
new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory));
newSingleThreadExecutor创建一个单线程的线程池,相当于串行地执行所有任务,能保证任务的提交顺序依次执行。

5.newWorksStealingPool:这个是在jdk8引入的,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争。

Q:怎么手动实现一个线程池?

并发基础

关键词:线程安全、synchronized同步锁、Lock锁、volatile可见性、AtomicInteger原子操作类、CAS、AQS、ThreadLocal、CountDownLatch、Semaphore、ReentrantLock、Carrier

Q:线程安全是什么?
多个线程操作同一共享变量时,需要保证数据的安全性。
Q:如何保证线程安全?
1.加锁,synchronized同步锁或者ReentrantLock可重入锁。
2.使用AtomicInteger原子操作类,代替基本数据类型。
3.如果进行的是原子操作,可以使用volatile关键字修饰。
4.使用ThreadLocal对各个线程进行隔离
Q:同步有哪些?
synchronized关键字和Lock锁。
Q:synchronized的底层实现?
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
详情参见: https://www.cnblogs.com/paddix/p/5367116.html
Q:锁有哪些?
可重入锁ReentrantLock。可一个线程获得某个对象的方法的锁时,想要访问该对象的其他方法时无需再加锁。
ReentrantLock还可以设置为公平锁还是非公平锁。公平锁,会保证各个线程尽可能公平地拿到锁,不会导致某个线程一直拿不到锁的情况。
Q:ReentrantReadWriteLock是什么?读的时候可以写吗?读的时候可以读吗?写的时候可以写吗?
可重入的读写锁。读写锁分为读锁和写锁。
“读读共存,写写不共存,读写不共存”。
https://www.cnblogs.com/expiator/p/9374598.html
Q:AQS有没有了解过
AbstractQueueSynchronizer。抽象队列同步器。
CountDownLatch、Semaphore、Carrier这些并发工具类都是基于AQS实现的。线程池的内部也有继承自AQS的类。
Q:讲一下并发工具类是怎么使用的?
CountDownLatch是闭锁(阀门)。 更具体的待补充。。
Semaphore是信号量。
Q:简单讲下AQS的源码
在AQS内部会保存一个状态变量state,通过CAS修改该变量的值,修改成功的线程表示获取到该锁,没有修改成功,或者发现状态state已经是加锁状态,则通过一个Waiter对象封装线程,添加到等待队列中,并挂起等待被唤醒…
Carrier是 栅栏。
Q:CAS是什么?
CAS就是Compare And Swap,比较和替换。
比如说,比较当前状态是否为开启状态,如果不是开启状态,就将其改为开启状态。
需要读写的内存值: V,进行比较的预估值: A,拟写入的更新值: B。
当且仅当 V == A 时, V = B。
Q:CAS原理是什么?CAS是怎么实现的?
比较和替换。相当于有一个版本号,运用了乐观锁的机制。
CAS其实是通过Unsafe类的compareAndSwap实现的,Unsafe可以用来直接访问系统内存资源并进行自主管理。
Q:CAS有什么缺点?
CAS的缺点是存在ABA问题。
Q:什么是ABA?
就是一个变量V,如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?如果在这段期间它的值曾经被改成了B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。
Q:怎么解决ABA问题?
java并发包中提供了一个带有标记的原子引用类"AtomicStampedReference",它可以通过控制变量值的版本来保证CAS的正确性。

Q:volatile关键字有什么用?
可以保证线程的可见性,线程修改共享变量时会被其他线程发现。还可以禁止指令重排序。
Q:volatile是怎么实现的?
这个涉及到Java内存模型。JMM。
详情见: https://www.cnblogs.com/xrq730/p/7048693.html
Q:volatile能保证线程安全吗?
不能。volatile只有进行原子操作时,才是线程安全的
Q:使用volatile操作i++,是否线程安全?如果不安全,应该怎么处理?
线程不安全。因为i++不是原子操作。
可以使用AtomicInteger原子操作类进行操作。
Q:volatile和全局变量有什么区别?
Q:AtomicInteger是怎么实现的?
CAS乐观锁机制。比较和替换实现。
Q:讲一下ThreadLocal
每个线程都有一个自己的副本变量。
Q:讲一下ThreadLocal的底层实现。
Q:ThreadLocal是如何为每个线程创建变量的副本的?
Q:ThreadLocal是如何做到在不同线程set()、get()的值不被其它线程访问的;
各线程对共享的ThreadLocal实例进行操作,实际上是以该实例为键对内部持有的ThreadLocalMap对象进行操作。
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

Q:ThreadLocal可能会导致哪些问题?怎么解决?
内存泄露。用完记得将没用的ThreadLocal运行remove移除。
未完待续
更详细的资料参见 :
想进大厂需要懂的50个多线程面试题

原文地址:https://www.cnblogs.com/expiator/p/10193315.html