Java并发多线程-----真实大厂面试题汇总(含答案)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_36520235/article/details/86603064

面试题1、说一说自己对于 synchronized 关键字的了解

  1. 首先Synchronized关键字他可以保证他所修饰的方法或者代码块在任何时候都只能有一个线程可以执行。
  2. 他底层的监视器锁(monitor)是依赖操作系统的Mutex Lock来实现的,因为线程的挂起和唤醒都需要操作系统的帮助,而操作系统实现线程的切换是需要从用户状态转换到内核状态,这个时间比较长,时间成本比较高
  3. 在JDK1.6之前Synchronize是一种重量级锁,但是在JDK1.6之后对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

补充:

首先如果有多个线程想要获取锁的话,其实会把每一个等待锁的线程封装成一个ObjectWaiter对象然后先放到等待锁的集合中,然后如果有一个线程获取到了monitor对象的话,会把对象头MarkWord中的锁信息(这个锁信息是一个指针)指向monitor对象的起始地址。
在这里插入图片描述

Entry Set表示当前等待获取锁的集合,The Owner表示当前拥有锁的线程,Wait Set表示拥有锁的线程调用了wait()方法,然后释放了monitor对象,等待被唤醒。
在这里插入图片描述

面试题2、说说自己是怎么使用 synchronized 关键字

可以分为以下几种的情况:
4. 修饰实例方法和实例对象:相当于修饰当前实例的对象,如果进入同步代码块前需要获取当前实例对象的锁。
修饰实例方法:

  public synchronized void testSynchronized(){
        
    }

5. 修饰静态方法:相当于修饰类对象(类对象就相当于是,只要是修饰的静态方法的所在类的所有实例都会进行加锁)

    public static synchronized void testSynchronized(){

    }

6. 修饰代码块:第一种锁的是当前代码块所在的实例对象,第二种锁的是当前代码块所在的类对象(也就是只要是这个类new出来的所有的实例对象都会被锁)
修饰代码块的当前this实例对象:

       synchronized (this) {

        }

修饰代码块的XXX类.class对象:

        synchronized (test.class) {

        }

面试题3、讲一下 synchronized 关键字的底层原理

无论是作用到同步块还是作用到同步方法,其底层的本质都是对一个对象的监视器(monitor)进行获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放,而且这个获取的过程是排他的,也就是同时只能有且只有一个线程进来获取到这个监视器,而其他没有获取到这个监视器(monitor)的线程都只能阻塞在同步块和同步方法的入口处,进入Blocking状态。

这里如果其实作用到代码块和方法上的话,还是会有些不一样的,如果是作用在方法上的话,其实使用并不是monitor对象,而是使用ACC_Synchronized标识符,该标识符表示这是一个同步方法,会来进行执行一个对应的调用

面试题4、谈谈 synchronized和ReenTrantLock 的区别

  • (1):两者都是可重入的锁(什么是可重入锁:简单的来说就是自己可以再次获取自己的锁。比如:如果一个线程已经获取到了某个对象的锁,但是此时这个锁并没有释放,然后我还想继续获取这个对象的锁的时候还是可以获取到的,如果不是可重入锁的话,就会造成死锁,每次加锁,计数器都会加1,释放锁的时候会减1)

  • (2):Synchronize是依赖JVM层面进行加锁的,而ReenTrantLcok主要是依赖API层面来进行加锁的

  • (3):ReenTrantLock 比 synchronized 增加了一些高级功能

  • ReenTrantLock 可以实现一种中断等待锁的线程的方法,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。

  • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁,而且ReenTrantLock默认就是非公平的锁,可以通过构造方法进行指定是公平锁还是非公平锁

  • 可实现选择性通知(锁可以绑定多个条件):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实例中的所有等待线程

面试题5、volitile关键字的作用,原理。

  • 保证数据的可见性,也就是当使用了volitile的修饰变量的时候,只要当这个变量有改变就会从该线程的本地内存中的共享变量刷新到主内存中,保证了主内存中的数据一直都是最新的数据,如果是读一个volitile的数据的时候,JMM会把该线程相对应的本地内存变量置为无效,因为该本地变量无效了,所以就会去主内存中获取最新的数据
  • 防止指令进行重排序,因为JVM在设计的时候为了最大化的利用CUP的效率,他规定了在不影响正常的输出结果的情况下,所有的指令都是乱序的,为了防止普通的读写操作和volitile修饰的变量的读写操作的区别,在有volitile修饰的变量的读写的时候都会加上内存屏障来防止指令重排序

volitile的致命缺点:

  • 不支持原子性,但是synchronized支持原子性,也可以间接保证可见性

面试题6、可重入锁的用处及实现原理

可重入锁可以用于比如一个线程需要重复多次获取锁的场景。
可重入锁的原理:

  • 其实就是基于AQS的原理的

面试题7、讲讲对CAS和AQS的理解

CAS的原理:

  • 首先CAS是一个采用的是乐观锁的思想进行无锁算法实现的,其实就是(compare and swap)比较然后交换,比如CompareAndSwap(V,E,N)V就表示的是当前内存位置的值,E就表示的是预期的要比较的值,N就表示如果跟预期的值是一样的话就把当前内存位置的值要更新的新的值,如果当前内存位置的值和预期的值不一样的话,就不做任何操作,底层的实现是依赖于Unsafe类的方法

AQS的原理:

  • 是什么:首先得先知道AQS是一个同步队列的组件,用来实现各种锁或者其他同步组件的基础框架。
  • 用来干嘛的:使用的方式主要是通过继承,子类通过继承同步器并实现他的抽象方法来进行管理同步状态(它又支持独占式的获取、和共享式的获取同步状态),利用AQS实现的锁有ReentrantLock、ReentrantReadWriteLock、CountDownLatch等。
  • 原理;(待续。。。。)

面试题8、静态变量会有线程安全问题吗?局部变量呢

静态变量:非线程安全的

  • 静态变量其实就是类变量,位于方法区,被所有对象进行共享,共享一份内存,一旦静态变量被改变,其他对象都对修改可见,所以是非线程安全的
    局部变量:线程安全的
  • 因为局部变量都位于每个本地线程的栈贞中的工作内存中,每个线程中的变量都是独立的,互不影响,所以不会出现线程不安全。

面试题9、线程池介绍下

(1)线程池的七个参数的意思:

  1. corePoolSize(核心线程的数量)
  2. maximumPoolSize(线程池的最大数量)
  3. keepAliveTime(线程的存活时间)
  4. timeUnit(线程的存活时间的单位)
  5. BlockingQueue (阻塞队列的类型)
  6. ThreadFactory(生产线程的线程工厂)
  7. RejectedExecutionHandler(如果整个线程池都满的话,需要采用 的拒绝策略)

(2)线程池的工作流程:

  1. 如果有新的任务过来,先进行判断核心线程池的线程是不是都满了,如果没有满的话就直接进行新建一个线程进行执行任务,如果核心线程池满的话,就进入下一步
  2. 此时会先进行判断我的阻塞队列是不是满了(这里选择的阻塞对列十分重要,如果选择的是无界对列的话,就没有最大线程池这一说了,也就是这个参数就没用了),如果阻塞队列没有满的话,就把提交过来的任务包装成一个队列的节点,存储在队列中,如果阻塞队列满的话进入下一步
  3. 到这里就开始判断线程池的最大数量是不是全部都在工作,如果有空闲的话,就直接通过线程工厂去新建一个线程去执行任务,如果所有的线程都在工作状态的话,就去执行下一步
  4. 到了这一步我们定义的拒绝策略就开始起作用了,根据我们定义的拒绝策略去进行执行,如此反复的开始从头开始。

(3)线程池的几个师兄弟(也就是由ThreadPoolExecutor线程池演变的几个兄弟,但是他们是由Executors来进行直接调用的):

  1. newFixedThreadPool()固定线程池的具体多少个的线程池
  2. newSingleThreadExecutor()只有一个线程的线程池
  3. newCachedThreadPool()创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
  4. newScheduledThreadPool()适用于需要多个后台线程执行周期任务,保证顺序的执行各个任务的应用场景的

(4)线程池的几种拒绝策略:

  1. AbortPolicy,这种策略直接抛出异常,丢弃任务。(jdk默认策略,队列满并线程满时直接拒绝添加新任务,并抛出RejectedExecutionException异常
  2. DiscardPolicy,这种策略和AbortPolicy几乎一样,也是丢弃任务,只不过他不抛出异常
  3. DiscardOldestPolicy,这种其实是在当线程池没有关闭的前提下,会先去丢弃掉缓存在队列中的最早的任务
  4. CallerRunsPolicy,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。

10、乐观锁(哪些类实现了)与悲观锁(有哪些具体的实现)的使用场景

使用乐观锁的实现:

  • CAS无锁算法

使用悲观锁实现的:

  • synchronize锁
  • AQS

11、阻塞队列

  1. ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
  2. LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
  3. PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
  4. DelayQueue:一个使用优先级队列实现的无界阻塞队列。
  5. SynchronousQueue:一个不存储元素的阻塞队列。
  6. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
  7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

12、 死锁四个条件,如何避免

四个条件:

  • 互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

  • 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源
    已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

  • 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。

  • 循环等待条件: 若干进程间形成首尾相接循环等待资源的关系

如何进行避免死锁:

  • 破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到
    系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
  • 破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
  • 破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

13、进程通信方式,为什么要有进程?

  1. 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  2. 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
  3. 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

14、java线程变量怎么实现的?内存模型?

15、CountdownLatch和CyclicBarrier的区别和用法

  • CountdownLatch的使用场景是可以用来当测试并行(强调的是多个线程同时开始执行)开始的发令枪
  • CountdownLatch强调的是能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行,而CyclicBarrier强调的是当所有的线程都达到同一个临界屏障的时候(也就相当于说是所有线程都在等待最后一个线程到达),再同时去进行执行任务
  • 对比:CountdownLatch只能使用一次,而CyclicBarrier可以多次利用

16、有什么线程安全的List?(CopyOnWriteArrayList)讲一下怎么实现线程安全的?(写时复制,读时共享,加锁机制)

  • 首先CopyOnWriteArraryList在写的时候会先复制一份集合,然后进行写操作,这样的话就能保证了在写数据的时候,就算有多个线程过来,也能保证线程安全。
    其保证了每次只能有一个线程拿到复制集合的权限,其实也就是通过ReentrantLock 重入锁进行加锁机制。在写完新的集合之后,会把写完之后的集合的引用给原本的集合,此时的原本集合就是写完之后最新的
//源码的添加操作
 public boolean add(E e) {
        final ReentrantLock lock = this.lock;//重入锁
        lock.lock();//加锁啦
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝新数组
            newElements[len] = e;
            setArray(newElements);//将引用指向新数组  1
            return true;
        } finally {
            lock.unlock();//解锁啦
        }
    }

CopyOnWriteArrayList有什么优缺点:

缺点:

  • 会有延迟,当如果当你在put的时候会触发复制数组,如果集合比较大的话,那么就会比较费时间,此时当你还没有复制完成把复制之后的引用更新到原本的数组的时候,这时如果有线程过来读取的话,其实还会读取到原本的修改之前的数据
  • 比较的占用内存,因为如果集合数据比较多的话,底层会有一个Arrays.copyOf()方法的调用

优点:

  • 数据一致性完整,为什么?因为加锁了,并发数据不会乱
  • 解决了像ArrayList、Vector这种集合多线程遍历迭代问题,记住,Vector虽然线程安全,只不过是加了synchronized关键字,迭代问题完全没有解决!

17、atomic底层是如何实现的

在操作系统层面保证原子性有两点:

  • 操作系统通过对处理器使用总线锁来保证原子性,因为如今的电脑都是多个CPU来进行并行操作的,但是会出现一个变量被多个CPU同时缓存到自己的内存中,这样的话就会出现问题,而总线锁其实就是为了解决这一问题,当处理器提供一个LOCK信号的时候,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
  • 使用缓存锁保证原子性,其实就是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效,缓存锁定其实就是如果缓存在处理器缓存行中内存区域在 LOCK 操作期间被锁定
    是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于 Inter486 和奔腾处理器, 就算锁定的内存区域在处理器的缓存行中也会调用总线锁定

在Java中是如何保证原子一致性的:

  • 一般是使用CAS自旋无锁算法来实现的。

底层是使用的CAS无锁无阻塞的算法和自旋实现的

18、每个线程有自己的工作线程,static的变量会被拷贝到工作内存中吗?

  • 不会被拷贝到自己的工作内存中,因为static的变量是存储在JVM运行时数据中的方法区的,也就是相当于是存储在堆中(因为在方法区也在堆中,但是被称为是“非堆”)

猜你喜欢

转载自blog.csdn.net/qq_36520235/article/details/86603064