关于java:个人珍藏的80道多线程并发面试题110答案解析

22次阅读

共计 7449 个字符,预计需要花费 19 分钟才能阅读完成。

前言

集体收藏的 80 道 Java 多线程 / 并发经典面试题,因为篇幅太长,当初先给出 1 -10 的答案解析哈,前面一起欠缺,并且上传 github 哈~

https://github.com/whx123/Jav…

公众号:捡田螺的小男孩

1. synchronized 的实现原理以及锁优化?

synchronized 的实现原理

  • synchronized 作用于 办法 或者 代码块,保障被润饰的代码在同一时间只能被一个线程拜访。
  • synchronized 润饰代码块时,JVM 采纳 monitorenter、monitorexit 两个指令来实现同步
  • synchronized 润饰同步办法时,JVM 采纳 ACC_SYNCHRONIZED 标记符来实现同步
  • monitorenter、monitorexit 或者 ACC_SYNCHRONIZED 都是 基于 Monitor 实现
  • 实例对象里有对象头,对象头外面有 Mark Word,Mark Word 指针指向了monitor
  • Monitor 其实是一种 同步工具 ,也能够说是一种 同步机制
  • 在 Java 虚拟机(HotSpot)中,Monitor 是由 ObjectMonitor 实现 的。ObjectMonitor 体现出 Monitor 的工作原理~
ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录线程获取锁的次数
    _waiters      = 0,
    _recursions   = 0;  // 锁的重入次数
    _object       = NULL;
    _owner        = NULL;  // 指向持有 ObjectMonitor 对象的线程
    _WaitSet      = NULL;  // 处于 wait 状态的线程,会被退出到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 处于期待锁 block 状态的线程,会被退出到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor 的几个要害属性 _count、_recursions、_owner、_WaitSet、_EntryList 体现了 monitor 的工作原理

锁优化

在探讨锁优化前,先看看 JAVA 对象头 (32 位 JVM) 中 Mark Word 的结构图吧~

Mark Word 存储对象本身的运行数据,如 哈希码、GC 分代年龄、锁状态标记、偏差工夫戳(Epoch) 等,为什么辨别 偏差锁、轻量级锁、重量级锁 等几种锁状态呢?

在 JDK1.6 之前,synchronized 的实现间接调用 ObjectMonitor 的 enter 和 exit,这种锁被称之为 重量级锁。从 JDK6 开始,HotSpot 虚拟机开发团队对 Java 中的锁进行优化,如减少了适应性自旋、锁打消、锁粗化、轻量级锁和偏差锁等优化策略。

  • 偏差锁:在无竞争的状况下,把整个同步都打消掉,CAS 操作都不做。
  • 轻量级锁:在没有多线程竞争时,绝对重量级锁,缩小操作系统互斥量带来的性能耗费。然而,如果存在锁竞争,除了互斥量自身开销,还额定有 CAS 操作的开销。
  • 自旋锁:缩小不必要的 CPU 上下文切换。在轻量级锁降级为重量级锁时,就应用了自旋加锁的形式
  • 锁粗化:将多个间断的加锁、解锁操作连贯在一起,扩大成一个范畴更大的锁。

举个例子,买门票进动物园。老师带一群小朋友去参观,验票员如果晓得他们是个个体,就能够把他们看成一个整体(锁租化),一次性验票过,而不须要一个个找他们验票。

  • 锁打消: 虚拟机即时编译器在运行时,对一些代码上要求同步,然而被检测到不可能存在共享数据竞争的锁进行削除。

有趣味的敌人们能够看看我这篇文章:
Synchronized 解析——如果你违心一层一层剥开我的心

2. ThreadLocal 原理,应用留神点,利用场景有哪些?

答复四个次要点:

  • ThreadLocal 是什么?
  • ThreadLocal 原理
  • ThreadLocal 应用留神点
  • ThreadLocal 的利用场景

ThreadLocal 是什么?

ThreadLocal,即线程本地变量。如果你创立了一个 ThreadLocal 变量,那么拜访这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,理论是操作本人本地内存外面的变量,从而起到线程隔离的作用,防止了线程平安问题。

// 创立一个 ThreadLocal 变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();

ThreadLocal 原理

ThreadLocal 内存结构图:


由结构图是能够看出:

  • Thread 对象中持有一个 ThreadLocal.ThreadLocalMap 的成员变量。
  • ThreadLocalMap 外部保护了 Entry 数组,每个 Entry 代表一个残缺的对象,key 是 ThreadLocal 自身,value 是 ThreadLocal 的泛型值。

对照着几段要害源码来看,更容易了解一点哈~

public class Thread implements Runnable {
   //ThreadLocal.ThreadLocalMap 是 Thread 的属性
   ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal 中的要害办法 set()和 get()

    public void set(T value) {Thread t = Thread.currentThread(); // 获取以后线程 t
        ThreadLocalMap map = getMap(t);  // 依据以后线程获取到 ThreadLocalMap
        if (map != null)
            map.set(this, value); //K,V 设置到 ThreadLocalMap 中
        else
            createMap(t, value); // 创立一个新的 ThreadLocalMap
    }

    public T get() {Thread t = Thread.currentThread();// 获取以后线程 t
        ThreadLocalMap map = getMap(t);// 依据以后线程获取到 ThreadLocalMap
        if (map != null) {
            // 由 this(即 ThreadLoca 对象)失去对应的 Value,即 ThreadLocal 的泛型值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {@SuppressWarnings("unchecked")
                T result = (T)e.value; 
                return result;
            }
        }
        return setInitialValue();}

ThreadLocalMap 的 Entry 数组

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {super(k);
            value = v;
        }
    }
}

所以怎么答复ThreadLocal 的实现原理?如下,最好是能联合以上结构图一起阐明哈~

  • Thread 类有一个类型为 ThreadLocal.ThreadLocalMap 的实例变量 threadLocals,即每个线程都有一个属于本人的 ThreadLocalMap。
  • ThreadLocalMap 外部保护着 Entry 数组,每个 Entry 代表一个残缺的对象,key 是 ThreadLocal 自身,value 是 ThreadLocal 的泛型值。
  • 每个线程在往 ThreadLocal 里设置值的时候,都是往本人的 ThreadLocalMap 里存,读也是以某个 ThreadLocal 作为援用,在本人的 map 里找对应的 key,从而实现了线程隔离。

ThreadLocal 内存泄露问题

先看看一下的 TreadLocal 的援用示意图哈,

ThreadLocalMap 中应用的 key 为 ThreadLocal 的弱援用,如下

弱援用:只有垃圾回收机制一运行,不论 JVM 的内存空间是否短缺,都会回收该对象占用的内存。

弱援用比拟容易被回收。因而,如果 ThreadLocal(ThreadLocalMap 的 Key)被垃圾回收器回收了,然而因为 ThreadLocalMap 生命周期和 Thread 是一样的,它这时候如果不被回收,就会呈现这种状况:ThreadLocalMap 的 key 没了,value 还在,这就会 造成了内存透露问题

如何 解决内存透露问题 ?应用完 ThreadLocal 后,及时调用 remove() 办法开释内存空间。

ThreadLocal 的利用场景

  • 数据库连接池
  • 会话治理中应用

3. synchronized 和 ReentrantLock 的区别?

我记得校招的时候,这道面试题呈现的频率还是挺高的~ 能够从锁的实现、性能特点、性能等几个维度去答复这个问题,

  • 锁的实现: synchronized 是 Java 语言的关键字,基于 JVM 实现。而 ReentrantLock 是基于 JDK 的 API 层面实现的(个别是 lock()和 unlock()办法配合 try/finally 语句块来实现。)
  • 性能: 在 JDK1.6 锁优化以前,synchronized 的性能比 ReenTrantLock 差很多。然而 JDK6 开始,减少了适应性自旋、锁打消等,两者性能就差不多了。
  • 性能特点: ReentrantLock 比 synchronized 减少了一些高级性能,如期待可中断、可实现偏心锁、可实现选择性告诉。
  • ReentrantLock 提供了一种可能中断期待锁的线程的机制,通过 lock.lockInterruptibly()来实现这个机制。
  • ReentrantLock 能够指定是偏心锁还是非偏心锁。而 synchronized 只能是非偏心锁。所谓的偏心锁就是先期待的线程先取得锁。
  • synchronized 与 wait()和 notify()/notifyAll()办法联合实现期待 / 告诉机制,ReentrantLock 类借助 Condition 接口与 newCondition()办法实现。
  • ReentrantLock 须要手工申明来加锁和开释锁,个别跟 finally 配合开释锁。而 synchronized 不必手动开释锁。

4. 说说 CountDownLatch 与 CyclicBarrier 区别

  • CountDownLatch:一个或者多个线程,期待其余多个线程实现某件事情之后能力执行;
  • CyclicBarrier:多个线程相互期待,直到达到同一个同步点,再持续一起执行。

举个例子吧:

  • CountDownLatch:假如老师跟同学约定周末在公园门口汇合,等人齐了再发门票。那么,发门票(这个主线程),须要等各位同学都到齐(多个其余线程都实现),能力执行。
  • CyclicBarrier: 多名长跑运动员要开始田径比赛,只有等所有运动员筹备好,裁判才会鸣枪开始,这时候所有的运动员才会疾步如飞。

5. Fork/Join 框架的了解

Fork/Join 框架是 Java7 提供的一个用于并行执行工作的框架,是一个把大工作宰割成若干个小工作,最终汇总每个小工作后果后失去大工作后果的框架。

Fork/Join 框架须要了解两个点,分而治之 工作窃取算法

分而治之

以上 Fork/Join 框架的定义,就是分而治之思维的体现啦

工作窃取算法

把大工作拆分成小工作,放到不同队列执行,交由不同的线程别离执行时。有的线程优先把本人负责的工作执行完了,其余线程还在慢慢悠悠解决本人的工作,这时候为了充沛提高效率,就须要工作偷盗算法啦~

工作偷盗算法就是,某个线程从其余队列中窃取工作进行执行的过程。个别就是指做得快的线程(偷盗线程)抢慢的线程的工作来做,同时为了缩小锁竞争,通常应用双端队列,即快线程和慢线程各在一端。

6. 为什么咱们调用 start()办法时会执行 run()办法,为什么咱们不能间接调用 run()办法?

看看 Thread 的 start 办法阐明哈~

    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
    public synchronized void start() {......}

JVM 执行 start 办法,会另起一条线程执行 thread 的 run 办法,这才起到多线程的成果~ 为什么咱们不能间接调用 run()办法?
如果间接调用 Thread 的 run()办法,其办法还是运行在主线程中,没有起到多线程成果。

7. CAS?CAS 有什么缺点,如何解决?

CAS,Compare and Swap,比拟并替换;

CAS 波及 3 个操作数,内存地址值 V,预期原值 A,新值 B;
如果内存地位的值 V 与预期原 A 值相匹配,就更新为新值 B,否则不更新

CAS 有什么缺点?

ABA 问题

并发环境下,假如初始条件是 A,去批改数据时,发现是 A 就会执行批改。然而看到的尽管是 A,两头可能产生了 A 变 B,B 又变回 A 的状况。此时 A 曾经非彼 A,数据即便胜利批改,也可能有问题。

能够通过 AtomicStampedReference解决 ABA 问题,它,一个带有标记的原子援用类,通过管制变量值的版本来保障 CAS 的正确性。

循环工夫长开销

自旋 CAS,如果始终循环执行,始终不胜利,会给 CPU 带来十分大的执行开销。

很多时候,CAS 思维体现,是有个自旋次数的,就是为了避开这个耗时问题~

只能保障一个变量的原子操作。

CAS 保障的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无奈间接保障操作的原子性的。

能够通过这两个形式解决这个问题:

  • 应用互斥锁来保障原子性;
  • 将多个变量封装成对象,通过 AtomicReference 来保障原子性。

有趣味的敌人能够看看我之前的这篇实战文章哈~
CAS 乐观锁解决并发问题的一次实际

9. 如何保障多线程下 i ++ 后果正确?

  • 应用循环 CAS,实现 i ++ 原子操作
  • 应用锁机制,实现 i ++ 原子操作
  • 应用 synchronized,实现 i ++ 原子操作

没有代码 demo,感觉是没有灵魂的~ 如下:

/**
 *  @Author 捡田螺的小男孩
 */
public class AtomicIntegerTest {private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {testIAdd();
    }

    private static void testIAdd() throws InterruptedException {
        // 创立线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 1000; i++) {executorService.execute(() -> {for (int j = 0; j < 2; j++) {
                    // 自增并返回以后值
                    int andIncrement = atomicInteger.incrementAndGet();
                    System.out.println("线程:" + Thread.currentThread().getName() + "count=" + andIncrement);
                }
            });
        }
        executorService.shutdown();
        Thread.sleep(100);
        System.out.println("最终后果是:" + atomicInteger.get());
    }
    
}

运行后果:

...
线程:pool-1-thread-1 count=1997
线程:pool-1-thread-1 count=1998
线程:pool-1-thread-1 count=1999
线程:pool-1-thread-2 count=315
线程:pool-1-thread-2 count=2000
最终后果是:2000

10. 如何检测死锁?怎么预防死锁?死锁四个必要条件

死锁是指多个线程因竞争资源而造成的一种相互期待的僵局。如图感受一下:

死锁的四个必要条件:

  • 互斥:一次只有一个过程能够应用一个资源。其余过程不能拜访已调配给其余过程的资源。
  • 占有且期待:当一个过程在期待调配失去其余资源时,其持续占有已调配失去的资源。
  • 非抢占:不能强行抢占过程中已占有的资源。
  • 循环期待:存在一个关闭的过程链,使得每个资源至多占有此链中下一个过程所须要的一个资源。

如何预防死锁?

  • 加锁程序(线程按程序办事)
  • 加锁时限(线程申请所加上权限,超时就放弃,同时开释本人占有的锁)
  • 死锁检测

参考与感激

牛顿说,我之所以看得远,是因为我站在伟人的肩膀上~ 谢谢以下各位前辈哈~

  • 面试必问的 CAS,你懂了吗?
  • Java 多线程:死锁
  • ReenTrantLock 可重入锁(和 synchronized 的区别)总结
  • 聊聊并发(八)——Fork/Join 框架介绍

集体公众号

  • 感觉写得好的小伙伴给个点赞 + 关注啦,谢谢~
  • 如果有写得不正确的中央,麻烦指出,感激不尽。
  • 同时十分期待小伙伴们可能关注我公众号,前面缓缓推出更好的干货~ 嘻嘻
  • github 地址:https://github.com/whx123/Jav…

正文完
 0