面试复习题-- JUC

36 篇文章 0 订阅
28 篇文章 0 订阅

1、FutureTask阻塞原理

    当调用FutureTask.get()时,如果Future对应的任务已完成(正常执行完成或者抛出异常),执行返回;如果Future对应的任务未执行完成,则会将当前线程封装成一个NodeWait,以CAS方式添加到FutureTask.waiters链表上(单向链表,新节点都会作为head node添加上),然后会阻塞当前线程(包括超时阻塞)。FutureTask中的waiters是一个单向链表,如果多个线程阻塞在该Future上,最新阻塞的线程排列在链表前面,唤醒线程时依次从前到后遍历链表唤醒线程,这样处理貌似对最开始阻塞在Future上的线程不太公平哈,因为最开始阻塞的线程是到最后才被唤醒的。

2、线程池流程

图片

3、线程池execute 和submit的区别:

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。submit()方法用于提交需要返回值的任务。

4、synchronized 原理

对于方法,则是通过ACC_SYNCHRONIZED这个修饰符来完成的,会在代码块的前后分别形成monitorenter和monitorexit这两个字节码指令。monitorenter插入到代码块的开始位置,monitorexit插入到代码块结束处异常处

5、volatile原理

可见性: 

有序性:会允许编译器和处理器对指令序列进行重排序,那如果想阻止重排序要怎么办了? 答案是可以添加内存屏障。

在每个volatile写操作的前面插入一个StoreStore屏障
在每个volatile写操作的后面插入一个StoreLoad屏障

在每个volatile读操作的后面插入一个LoadLoad屏障
在每个volatile读操作的后面插入一个LoadStore屏障

6、Threadlocal

  hash冲突:使用开地址法,线性探测法的地址增量di = 1, 2, … 其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

解决每一个线程只存一个变量,这样的话全部的线程存放到map中的Key都是相同的ThreadLocal,若是一个线程要保存多个变量,就须要建立多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增长Hash冲突的可能。

  导致内存溢出 :map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。但是,我们的value却不能回收,而这块value永远不会被访问到了,所以存在着内存泄露。因为存在一条从current thread连接过来的强引用。只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收。最好的做法是将调用threadlocal的remove方法。

7、CAS原理

CAS 操作包含三个操作数 – 内存地址、预期值和新值。CAS 的实现逻辑是将内存地址的数值与预期数值想比较,若相等,则将内存位置处的值替换为新值。若不相等,则不做任何操作。JAVA中CAS是通过自旋操作完成赋值,若值不相等再更新预期值、重新计算新值,接着进行CAS操作,直到成功为止。底层是JVM调用操作系统原语指令unsafe,并由CPU完成原子操作。

CAS优点:没有引用锁的概念,并发量不高情况下提高效率;减少线程上下文切换

CAS缺点:cpu开销大,在高并发下,许多线程,更新一变量,多次更新不成功,循环反复,给cpu带来大量压力;只是一个变量的原子性操作,不能保证代码块的原子性;ABA问题

ABA问题: 如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的。比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。处理方法:JAVA中提供了AtomicStampedReference/AtomicMarkableReference来处理会发生ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。

8、AQS原理

如果被请求的资源是共享的空闲的,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待 以及 被唤醒时 锁分配的 机制,这个机制 AQS 是用state 和 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

9、ReentrantLock

  • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
  • 非公平锁:当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。

10、ReentrantReadWriteLock

1.公平性选择:支持费公平性(默认)和公平的锁获取方式。
2.重入性:支持重入,读锁获取后可以再次获取读锁,写锁获取之后能够再次获取写锁,同时当前也能获取读锁;
3.锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁。

11、锁升级

锁升级的方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。

1.偏向锁
偏向锁是JDK6中引入的一项锁优化,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。

2.轻量级锁
如果明显存在其它线程申请锁,那么偏向锁将很快升级为轻量级锁。

3.自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

4.重量级锁
指的是原始的Synchronized的实现,重量级锁的特点:其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。

1、当没有被当做锁的时候,这就是个普通对象,锁标志位为01,是否偏向锁为0

2、当对象被当做同步锁时,一个线程A抢到锁时,锁标志位依然是01,是否偏向锁为1,前23位记录A线程的线程ID,此时锁升级为偏向锁

3、当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码,这也是偏向锁的意义

4、当一个线程B尝试获取锁,JVM发现当前的锁处于偏向状态,并且现场ID不是B线程的ID,那么线程B会先用CAS将线程id改为自己的,这里是有可能成功的,因为A线程一般不会释放偏向锁。如果失败,则执行5

5、偏向锁抢锁失败,则说明当前锁存在一定的竞争,偏向锁就升级为轻量级锁。JVM会在当前线程的现场栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁MarkWord中保存指向这片空间的指针。上面的保存都是CAS操作,如果竞争成功,代表线程B抢到了锁,可以执行同步代码。如果抢锁失败,则继续执行6

6、轻量级锁抢锁失败,则JVM会使用自旋锁,自旋锁并非是一个锁,则是一个循环操作,不断的尝试获取锁。从JDK1.7开始,自旋锁默认开启,自旋次数由JVM决定。如果抢锁成功,则执行同步代码;如果抢锁失败,则执行7

7、自旋锁重试之后仍然未抢到锁,同步锁会升级至重量级锁,锁标志位改为10,在这个状态下,未抢到锁的线程都会被阻塞,由Monitor来管理,并会有线程的park与unpark,因为这个存在用户态和内核态的转换,比较消耗资源,故名重量级

12、ThreadPoolExecutor自定义

  1. corePoolSize:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务
  2. maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
  3. keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
  4. unit:keepAliveTime的时间单位
  5. workQueue:用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
  6. threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建
  7. handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy

handler拒绝策略

  • AbortPolicy:中断抛出异常
  • DiscardPolicy:默默丢弃任务,不进行任何通知
  • DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
  • CallerRunsPolicy:让提交任务的线程去执行任务(对比前三种比较友好一丢丢)

workQueue队列----SynchronousQueue(同步移交队列):队列不作为任务的缓冲方式,可以简单理解为队列长度为零 ;LinkedBlockingQueue(无界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致内存占用过多或OOM;ArrayBlockintQueue(有界队列):队列长度受限,当队列满了就需要创建多余的线程来执行任务

线程池最好自定义因为使用

Executors

创建的线程池里的

   public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

      类似此2种方法的最大线程数时 最大整数,或者LinkedBlockingQueue 的大小也是最大指数,导致内存溢出。

13、Exchanger

用于两个线程间的通信,无论哪个线程先调用都会等到另外一个线程调用时进行数据交换,必须成对线程间使用。

14、Flow

自Java 9以来,Flow API是对反应流规范的官方支持。它是Iterator Observer 模式的组合。的流API是一个互操作规范,而不是最终用户API等RxJava

Flow API包含四个基本接口:

  • 订阅服务器订阅服务器订阅发布服务器以进行回调。

  • 发布者发布者将数据项流发布给注册的订阅者。

  • 订阅发布者和订阅者之间的链接。

  • 处理器处理器位于发布者和订阅者之间,并将一个流转换为另一流。

15、LongAdder

  AtomicLong通过循环CAS实现原子操作,缺点是当高并发下竞争比较激烈的时候,会出现大量的CAS失败,导致循环CAS次数大大增加,这种自旋是要消耗时间cpu时间片的,会占用大量cpu的时间,降低效率。那这个问题如何解决呢?JUC给我们提供了一个类,LongAdder, 它的作用和AtomicLong是一样的,都是一个实现了原子操作的累加器,LongAdder通过维护一个基准值base和 Cell 数组,多线程的时候多个线程去竞争Cell数组的不同的元素,进行cas累加操作,并且每个线程竞争的Cell的下标不是固定的,如果CAS失败,会重新获取新的下标去更新,从而极大地减少了CAS失败的概率,最后在将base 和 Cell数组的元素累加,获取到我们需要的结果。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值