这9道面试题,给你答案都不必定能看明白,但面试必问,建议看完

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

synchronized的实现原理git

  • 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上下文切换。在轻量级锁升级为重量级锁时,就使用了自旋加锁的方式
  • 锁粗化:将多个连续的加锁、解锁操做链接在一块儿,扩展成一个范围更大的锁。

框架

举个例子,买门票进动物园。老师带一群小朋友去参观,验票员若是知道他们是个集体,就能够把他们当作一个总体(锁租化),一次性验票过,而不须要一个个找他们验票。jvm

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

有兴趣的朋友们能够看看我这篇文章: Synchronized解析——若是你愿意一层一层剥开个人心[1]

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乐观锁解决并发问题的一次实践[2]

8. 如何保证多线程下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
复制代码

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

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

 

「死锁的四个必要条件:」

  • 互斥:一次只有一个进程可使用一个资源。其余进程不能访问已分配给其余进程的资源。
  • 占有且等待:当一个进程在等待分配获得其余资源时,其继续占有已分配获得的资源。
  • 非抢占:不能强行抢占进程中已占有的资源。
  • 循环等待:存在一个封闭的进程链,使得每一个资源至少占有此链中下一个进程所须要的一个资源。

「如何预防死锁?」

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

怎么样,这几道题你遇到了能回答多少?

而这,只是常见的多线程,高并发以及jvm调优的源码问题中最多见的几道面试题,其余的面试题我已经整理到个人git仓库中,并在不断地更新上传中

 

相应的文章已经整理造成文档,git扫码获取资料看这里

https://gitee.com/biwangsheng/personal.git