Java多线程面试题

1.说说你对volatile的理解。

volatitle在多线程情况下,可以保证数据数据的可见性。禁止指令重排优化,从而避免多线程环境下程序出现乱序执行导致执行结果不一致的问题,它不支持原子性(使用AutomicInteger来保证原子性)。

2.你在哪些地方使用过volatitle

在单例模式DCL中使用过。

3.说说在创建线程时使用Runable接口比Thread类的好处:

(1)Thread类继承存在单继承的局限性,而接口不会。
(2)Runable可以体现数据共享的概念(JMM内存模型图),代码可以被多个线程共享,代码和数据独立。
(3)线程池只能放入实现Runnable或callable类的线程,不能直接放入继承Thread的类
(3)Runnable实现线程可以对线程进行复用,因为runnable是轻量级对象,而Thread不行,它是重量级对象。

4.在多线程中为什么static和volatile一起使用修饰变量,好处是什么有何区别?

(1)static保证唯一性,就是在主内存中是唯一的变量。各个线程创建时需要从主内存同一个位置拷贝到自己工作内存中去,也就是说只能保证线程创建时,变量的值是相同来源的,运行时还是使用各自工作内存中的值,依然会有不同步的问题。
(2)volatile是保证可见性,就是指在工作线程和主内存的数据的一致性,如果改变了工作线程中volatile修饰的变量,那么主内存也要发生更新。所以,volatile和static一起使用不矛盾。因为static修饰只能保证在主内存的唯一性,如果涉及到其他工作线程,改变参数可能就会导致static修饰的变量的内容无法同步,所以static和volatile可以一起使用,因为他们管的地方是不一样的,互不影响。

5.说说你对CAS的理解?

CAS即比较并且交换,CPU去更新一个值,如果初始值与原来值不相同操作就失败,但如果初始值与原来值相同就操作就成功。

CAS应用:CAS有3个操作数,V:要更新的变量;E:预期值;N:新值。如果V值等于E值,则将V值设为N值;如果V值不等于E值,说明其他线程做了更新,那么当前线程什么也不做。(放弃操作或重新读取数据)

6.说说CAS的实现原理

CAS是通过Unsafe实现,Unsafe是CAS的核心类,由于Java无法直接访问底层系统,需要通过本地native来访问,Unsafe相当于
一个后门,基于该类可以直接操作特定的内存数据。Unsafe类存在于sun.misc包中,其内部方法操作
可以像C语言的指针一样直接操作内存。因为Java中CAS的操作的执行依赖于Unsafe类的方法。

底层代码如下

public final int getAndIncrement() { 
    return unsafe.getAndAddInt(this, valueOffset, 1);   
}

//Unsafe类中的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {        
    int v;        
    do {            
        v = getIntVolatile(o, offset);        
    } while (!compareAndSwapInt(o, offset, v, v + delta));        
    return v;
}

CAS的缺点
1.循环时间长,开销很大。
2.只能保证一个共享变量的原子操作

7. 谈谈AtomicInteger的ABA问题,原子更新引用知道吗?

假如有A,B两条线程,线程A,B都拷贝主内存的数据到自己的内存空间。这时候B线程将自己内存中的数据更改了并更新到主内存中,然后又将自己内存中的数据改回了原来的初始值。现在A线程在做修改的时候,将自己内存中的数据与主内存中的数据做对比发现是一样的并做更新。对与A线程而言它并不知道B线程已经做了一些相应的更改,这时候就产生了ABA问题。通过原子更新引用来解决ABA问题。

8. 如何解决ABA问题?

每修改一次值都对其添加版本号,对值和版本号都做CAS操作。在值和版本号都一致的情况下才能做修改。AtomicStampedReference是一个带有时间戳的对象引用,能很好的解决CAS机制中的ABA问题。
其实除了AtomicStampedReference类,还有一个原子类也可以解决就是AtomicMarkableReference,它不是维护一个版本号,而是维护一个boolean类型的标记,用法没有AtomicStampedReference灵活。因此也只是在特定的场景下使用。

 9. Collection和Collections的区别

(1)Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。

(2)Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。

 10.说说你遇到的线程不安全问题,以及解决方式

导致原因:线程不安全问题,基本都是由于多线程并发争抢修改数据导致的。
故障现象:会抛出java.util.ConcurrentModificationException异常

(1)ArrayList线程不安全解决方案

a. 用Vector来解决,
b. 用Collections.synchronizedList(new ArrayList<>())
c. new CopyOnWriteArrayList<>()

(2)HashSet线程不安全解决方案

a. 用Collections.synchronizedSet(new HashSet<>())来解决
说下HashSet的底层实现
HshSet底层是HashMap,既然是HashMap为什么我们add的时候只添加一个值,而不以key-value的形式添加呢。因为底层的HashMap已经put了Value值,这个Value值其实是一个Object对象。我们在调用HashSet的add方法时等于只是添加了一个key值。这也是set集合不重复的原因。

(3)HashMap线程不安全解决方案

a. 用Collections.synchronizedMap(new HashMap<>())
b. new ConcurrentHashMap<>()

11 . 说一下公平锁与非公平锁,两者之间的区别

公平锁:指多个线程按照申请锁的顺序来获取锁,类似排队购票,先到先得。
非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发情况下,有可能造成优先级反转或者饥饿现象。

并发包中的ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁。

关于两者的区别:
公平锁:就是很公平,在并发环境中,每个线程获取锁时会先看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁。否则就会加入到等待队列中,后面按照FIFO的规则从队列中取到自己。 

非公平锁:比较粗鲁,上来就尝试直接占有锁,如果尝试失败,就采用类似公平锁的方式。相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

说明:

对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

对于Synchronized而言,也是一种非公平锁。

12. 说说你对可重入锁的理解

可重入锁又名递归锁,一个线程拿到一个方法的一把锁可以访问其内部代码,但是其内部还嵌套了另外一个方法,这个方法也上了锁。因为线程拿到了外层方法的锁,这把锁和嵌套方法的锁是同一把锁,所以可以直接访问嵌套方法内部。像ReentrantLock,Synchronized就是典型的非公平的可重入锁。可重入锁最大的作用就是可以避免死锁。

可重入锁代码实例

package com.example.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentryLock{
    /**
     * 对于 ReentrantLock 可重入锁
     */
    private Lock lock=new ReentrantLock();
    
    private void getInfo(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"==获取信息");
            getDetails();
        } finally{
            lock.unlock();
        }
    }
    
    private void getDetails(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"==获取详情");
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
    
    /**
     * 对于synchronized可重入锁
     */
    private synchronized void getSMS(){
        System.out.println(Thread.currentThread().getName()+"==开始发送短信!");
        getEmail();
    }
    
    private synchronized void getEmail(){
        System.out.println(Thread.currentThread().getName()+"==开始发送邮件!");
    }
    
    
    
    public static void main(String[] args) {
        ReentryLock reentryLock=new ReentryLock();
        new Thread(()->{
            reentryLock.getSMS();
        },"t1").start();
        
        new Thread(()->{
            reentryLock.getInfo();
        },"t2").start();
    }

}

13. 说说你对自旋锁(Unsafe+CAS思想)的理解

是指读取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗。
缺点是循环会消耗CPU 

package com.example.thread;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class SpinLock {

    private AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock() {
        Thread current = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t into lock");
        while (!atomicReference.compareAndSet(null, current)) {
        }
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        atomicReference.compareAndSet(current, null);
        System.out.println(Thread.currentThread().getName()+"\t leave lock");
    }
    public static void main(String[] args) throws InterruptedException {
        SpinLock spinLock = new SpinLock();
        new Thread(()->{
            spinLock.lock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            }finally{
                spinLock.unlock();
            }
        },"thread1").start();
        
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            spinLock.lock();
            spinLock.unlock();
        },"thread2").start();
    }
}

14 .独占锁与共享锁,两者有什么区别?

独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁
共享锁:指该锁可被多个线程锁持有

15. CountDownLatch/CyclicBarrier/Semaphore 使用过吗?介绍一下。

CountDownLatch:当一个或多个线程通过调用await方法进入阻塞状态,等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现,计数器初始值为线程的数量。当每一个线程调用countDown方法完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后唤醒哪些因为调用了await而阻塞的线程去执行接下来的任务。

CyclicBarrier:它的功能是:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障,这时所有被屏障拦截的线程才会继续执行。它通过调用await方法让线程进入屏障。

Semaphore:信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程的控制(就是控制同时得到共享资源的线程数量)。

16. 说说CountDownlatch和CyclicBarrier以及Semaphor的区别

(1)CountDownLatch是做减法,CyclicBarrier是做加法,Semaphor的临界资源可以反复使用。

(2)CountDownLatch不能重置计数,CycliBarrier提供的reset()方法可以重置计数,不过只能等到第一个计数结束。Semaphor可以重复使用。

(3)CountDownLatch和CycliBarrier不能控制并发线程的数量,Semaphor可以实现控制并发线程的数量。

17 .阻塞队列知道吗?介绍一下。

阻塞队列首先是一个队列。
(1)当阻塞队列是空的时候,从队列中获取元素的操作将被阻塞。
(2)当队列是满的时候,往队列里添加元素的操作会被阻塞。

18. 阻塞队列的优点是什么?它主要用在哪里?

优点:我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。
在concurrent包发布以前。在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程
安全,而这会给我们的程序带来不小的复杂度。

用途:a.生产者消费者模式  b.线程池   c.消息中间件

19.多线程为什么要用 while 来判断而不用 if 呢?

用if会造成虚假唤醒 , 详情可以百度。

20 . 使用condition做线程通信,比传统的Object的wait()、notify()实现线程间的协作有什么好处?

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。

21 . Synchronized 和lock有什么区别?用新的lock有什么好处?你举例说说

(1)原始构成:synchronized是JVM层面的,底层通过monitorenter和monitorexit来实现的。Lock是JDK API层面的。(synchronized一个enter会有两个exit,一个是正常退出,一个是异常退出(保证肯定可以退出))
(2)使用方法:synchronized不需要手动释放锁,而Lock需要手动释放,若没有主动释放锁就可能导致出现死锁现象。
(3)是否可中断:synchronized不可中断,除非抛出异常或者正常运行完成。Lock是可中断的。
  a.设置超时方法tryLock(long timeout,TimeUnit unit);
  b. lockInterruptibly()方法放代码块中,调用interrupt()
(4)是否为公平锁:synchronized只能是非公平锁,而ReentrantLock既能是公平锁,又能是非公平锁。构造方法传入false/true,默认是非公平锁false。
(5)绑定多个条件Condition:synchronized不能,只能随机唤醒。而Lock可以通过Condition来绑定多个条件,精确唤醒。

22. 线程池的使用及优势,为什么要使用线程池?

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,则超出数量的线程排队等候,等待其他线程执行完毕,再从队列中取出任务来执行。
特点:线程复用;控制最大并发数;管理线程。

优势:

1.降低资源消耗。通过重复利用已经重建的线程降低线程创建和销毁造成的消耗。
2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
3.提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

23 .说出你知道的创建线程池的几种方式

Java中线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。 
Executors.newFixedThreadPool(int) 创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。
Executors.newSingleThreadPool() 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
Executors.newScheduledThreadPool(int n) 创建一个定长线程池,支持定时及周期性任务执行。
Executors.newCachedThreadPool() 可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务。

24 .说说线程池的7大参数和底层原理   

7大参数
int corePoolSize:线程池中核心线程数的最大值
int maximumPoolSize:线程池中能拥有最多线程数
long keepAliveTime:表示空闲线程的存活时间。当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。
TimeUnit unit:表示keepAliveTime的单位
BlockingQueue<Runnable> workQueue:用于缓存任务的阻塞队列,被提交但尚未被执行的任务
ThreadFactory threadFactory:用于生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
RejectedExecutionHandler handler: 拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数时如何来拒绝。

底层工作原理
1.当通过线程池创建好线程之后,当任务请求过来时会被核心线程去处理。
2.随着请求的增多,如果核心线程已经被占满了没有时间去处理过来的请求,这时候会将这些请求放到阻塞队列中等待。
3.如果请求还在进一步增多,阻塞队列的空间都已经被占满了。这时候会开启新的线程直到线程数达到线程池中能拥有最多线程数,去处理请求。
4.如果请求还是进一步增多,阻塞队列也满了。并且工作线程等于线程池的最大线程数,这时候会启用拒绝策略。

25 .谈谈线程池的拒绝策略

等待队列已经满了再也塞不下新任务了,同时线程池中的最大线程数也达到了,无法继续为新任务服务,这时候我们就需要拒绝策略机制合理的处理这个问题。
四种拒绝策略:
AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。
必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
CallerRunsPolicy - "调用者运行"一种调用机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
DiscardPolicy - 直接丢弃,不予任何处理也不抛出异常。
DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入队列中尝试再次提交新任务。

26. 你在工作中哪种线程池用的最多?Executors已经提供了线程池为什么不用?

JDK 提供的线程池一个都不用。
1.newFixedThreadPool()、newSingleThreadExecutor() 底层代码 中 LinkedBlockingQueue 没有设置容量大小,默认允许的请求队列长度是 Integer.MAX_VALUE, 可以认为是无界的。线程池中 多余的线程会被缓存到 LinkedBlockingQueue中,最终内存撑爆。

2.newCachedThreadPool()、newScheduledThreadPool() 的 底层代码中的最大线程数(maximumPoolSize) 是Integer.MAX_VALUE,可以认为是无限大,如果线程池中,执行中的线程没有及时结束,并且不断地有线程加入并执行,最终会将内存撑爆。
建议通过ThreadPoolExecutor去创建线程池。

27. 如何考虑配置一个合理的线程池,或者说线程池如何设置合理的参数?

我会根据我的业务是CPU密集型还是IO密集型来做决定。
CPU密集型:意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程)。而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。CPU密集型任务配置尽可能少的线程数量。
一般公式: CPU核数+1个线程的线程池

IO密集型:即任务需要大量的IO,即大量的阻塞。IO密集型时,大部分线程都阻塞,故需要都配置线程数:

参考公式: CPU核数/1-阻塞系数 阻塞系数在0.8-0.9之间。
比如8核CPU:8/1-0.9=80个线程数

注意:必须熟悉自己的硬件,看服务器是4核还是8核

28. 说说多线程中产生死锁的原因,如何去解决?

死锁就是两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,
若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁。

产生死锁的原因:
(1)竞争系统资源 (2)进程的推进顺序不当 (3)资源分配不当

解决死锁
(1)jps命令定位进程号 (2)jstack找到死锁查看

29. 请说一下对象锁和类锁的区别?

https://zhuanlan.zhihu.com/p/98145713

30. 如何让多个线程顺序执行,什么情况下需要多线程顺序执行?

join可以保证线程的顺序执行。比如:如果线程3依赖于线程1中返回的数据的时候就需要线程顺序执行。

31. 为什么wait,notify和notifyAll要与synchronized一起使用?

wait,notify和notifyAll必须在synchronized块里面,否则会抛出 IllegalMonitorStateException。
为什么这三个方法要与synchronized一起使用呢?解释这个问题之前,我们先要了解几个知识点:
--每一个对象都有一个与之对应的监视器
--每一个监视器里面都有一个该对象的锁和一个等待队列和一个同步队列

wait()方法的语义有两个,一是释放当前对象锁,另一个是进入阻塞队列,可以看到,这些操作都是与监视器相关的,当然要指定一个监视器才能完成这个操作了

notify()方法也是一样的,用来唤醒一个线程,你要去唤醒,首先你得知道他在哪儿,所以必须先找到该对象,也就是获取该对象的锁,当获取到该对象的锁之后,才能去该对象的对应的等待队列去唤醒一个线程。值得注意的是,只有当执行唤醒工作的线程离开同步块,即释放锁之后,被唤醒线程才能去竞争锁。

notifyAll()方法和notify()一样,只不过是唤醒等待队列中的所有线程

因wait()而导致阻塞的线程是放在阻塞队列中的,因竞争失败导致的阻塞是放在同步队列中的,notify()/notifyAll()实质上是把阻塞队列中的线程放到同步队列中去。

参考:https://blog.csdn.net/qq_39907763/article/details/79301813

32. sleep和wait的区别

1.sleep方法是Thread类的静态方法,wait()是Object超类的成员方法

2.sleep()方法导致了程序在指定的时间暂停执行,让出cpu给其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备。

3.sleep方法可以在任何地方使用,wait方法只能在同步方法和同步代码块中使用

 

posted on 2020-01-21 18:37  Eugene_Jin  阅读(252)  评论(0编辑  收藏  举报