java面试--多线程

391 阅读36分钟

1. 创建线程有几种方式?

image.png Java中创建线程主要有以下这几种方式:

  • 定义Thread类的子类,并重写该类的run方法
  • 定义Runnable接口的实现类,并重写该接口的run()方法
  • 定义Callable接口的实现类,并重写该接口的call()方法,一般配合Future使用
  • 线程池的方式ThreadPoolExecutor

2. 为什么要使用多线程

我们使用多线程就是因为: 在正确的场景下,设置恰当数目的线程,可以用来程提高序的运行速率。更专业点讲,就是充分地利用CPU和I/O的利用率,提升程序运行速率。

3. start()方法和run()方法的区别

其实startrun的主要区别如下:

  • start方法可以启动一个新线程,run方法只是类的一个普通方法而已,如果直接调用run方法,程序中依然只有主线程这一个线程。
  • start方法实现了多线程,而run方法没有实现多线程。
  • start不能被重复调用,而run方法可以。
  • start方法中的run代码可以不执行完,就继续执行下面的代码,也就是说进行了线程切换。然而,如果直接调用run方法,就必须等待其代码全部执行完才能继续执行下面的代码。

4. 线程和进程的区别

  • 进程是运行中的应用程序,线程是进程的内部的一个执行序列
  • 进程是资源分配的最小单位,线程是CPU调度的最小单位。
  • 一个进程可以有多个线程。线程又叫做轻量级进程,多个线程共享进程的资源
  • 进程间切换代价大,线程间切换代价小
  • 进程拥有资源多,线程拥有资源少
  • 进程是存在地址空间的,而线程本身无地址空间,线程的地址空间是包含在进程中的

5. 说一下 Runnable和 Callable有什么区别?

  • Runnable接口中的run()方法没有返回值,是void类型,它做的事情只是纯粹地去执行run()方法中的代码而已;
  • Callable接口中的call()方法是有返回值的,是一个泛型。它一般配合Future、FutureTask一起使用,用来获取异步执行的结果。
  • Callable接口call()方法允许抛出异常;而Runnable接口run()方法不能继续上抛异常;

6. 聊聊volatile作用,原理

volatile关键字是Java虚拟机提供的的最轻量级的同步机制。它作为一个修饰符,用来修饰变量。它保证变量对所有线程可见性,禁止指令重排,但是不保证原子性。 volatile变量,保证新值能立即同步回主内存,以及每次使用前立即从主内存刷新,所以我们说volatile保证了多线程操作变量的可见性。

7. 说说并发与并行的区别?

并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是同时

9. 线程有哪些状态?

线程有6个状态,分别是:New, Runnable, Blocked, Waiting, Timed_Waiting, Terminated

image.png

10. synchronized和ReentrantLock的区别?

  • Synchronized是依赖于JVM实现的,而ReenTrantLockAPI实现的。
  • Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者性能就差不多了。
  • Synchronized的使用比较方便简洁,它由编译器去保证锁的加锁和释放。而ReenTrantLock需要手工声明来加锁和释放锁,最好在finally中声明释放锁。
  • ReentrantLock可以指定是公平锁还是⾮公平锁。⽽synchronized只能是⾮公平锁。
  • ReentrantLock可响应中断、可轮回,而Synchronized是不可以响应中断的

11. wait(),notify()和suspend(),resume()之间的区别

  • wait()方法使得线程进入阻塞等待状态,并且释放锁
  • notify()唤醒一个处于等待状态的线程,它一般跟wait()方法配套使用。
  • suspend()使得线程进入阻塞状态,并且不会自动恢复,必须对应的resume()被调用,才能使得线程重新进入可执行状态。suspend()方法很容易引起死锁问题。
  • resume()方法跟suspend()方法配套使用。 suspend()不建议使用,因为suspend()方法在调用后,线程不会释放已经占有的资 源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。

12. CAS?CAS 有什么缺陷,如何解决?

CAS,全称是Compare and Swap,翻译过来就是比较并交换;

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

CAS有什么缺陷?

image.png

  • ABA 问题

并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。可以通过AtomicStampedReference 解决ABA问题,它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。

  • 循环时间长开销

自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~

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

CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。可以通过这两个方式解决这个问题:1. 使用互斥锁来保证原子性; 2.将多个变量封装成对象,通过AtomicReference来保证原子性。

13. 说说CountDownLatch与CyclicBarrier 区别

CountDownLatch和CyclicBarrier都用于让线程等待,达到一定条件时再运行。主要区别是:

  • CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;
  • CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。

举个例子吧:

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

14. 什么是多线程环境下的伪共享

14.1 什么是伪共享?

CPU的缓存是以缓存行(cache line)为单位进行缓存的,当多个线程修改相互独立的变量,而这些变量又处于同一个缓存行时就会影响彼此的性能。这就是伪共享

现代计算机计算模型:

  • CPU执行速度比内存速度快好几个数量级,为了提高执行效率,现代计算机模型演变出CPU、缓存(L1,L2,L3),内存的模型。
  • CPU执行运算时,如先从L1缓存查询数据,找不到再去L2缓存找,依次类推,直到在内存获取到数据。
  • 为了避免频繁从内存获取数据,聪明的科学家设计出缓存行,缓存行大小为64字节。

也正是因为缓存行的存在,就导致了伪共享问题,如图所示:

假设数据a、b被加载到同一个缓存行。

  • 当线程1修改了a的值,这时候CPU1就会通知其他CPU核,当前缓存行(Cache line)已经失效。
  • 这时候,如果线程2发起修改b,因为缓存行已经失效了,所以「core2 这时会重新从主内存中读取该 Cache line 数据」。读完后,因为它要修改b的值,那么CPU2就通知其他CPU核,当前缓存行(Cache line)又已经失效。
  • 酱紫,如果同一个Cache line的内容被多个线程读写,就很容易产生相互竞争,频繁回写主内存,会大大降低性能。

14.2 如何解决伪共享问题

既然伪共享是因为相互独立的变量存储到相同的Cache line导致的,一个缓存行大小是64字节。那么,我们就可以使用空间换时间的方法,即数据填充的方式,把独立的变量分散到不同的Cache line~

来看个例子:

ini
复制代码
/**
 * 更多干货内容,关注公众号:捡田螺的小男孩
 */
public class FalseShareTest  {

    public static void main(String[] args) throws InterruptedException {
        Rectangle rectangle = new Rectangle();
        long beginTime = System.currentTimeMillis();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                rectangle.a = rectangle.a + 1;
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                rectangle.b = rectangle.b + 1;
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println("执行时间" + (System.currentTimeMillis() - beginTime));
    }

}

class Rectangle {
    volatile long a;
    volatile long b;
}

//运行结果:
执行时间2815

一个long类型是8字节,我们在变量a和b之间不上7个long类型变量呢,输出结果是啥呢?如下:

arduino
复制代码
class Rectangle {
    volatile long a;
    long a1,a2,a3,a4,a5,a6,a7;
    volatile long b;
}
//运行结果
执行时间1113

可以发现利用填充数据的方式,让读写的变量分割到不同缓存行,可以很好挺高性能~

15. Fork/Join框架的理解

Fork/Join框架是Java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

Fork/Join框架需要理解两个点,「分而治之」和「工作窃取算法」。

分而治之

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

工作窃取算法

把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~

工作盗窃算法就是,「某个线程从其他队列中窃取任务进行执行的过程」。一般就是指做得快的线程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程各在一端。

16. 聊聊ThreadLocal原理?

ThreadLocal的内存结构图

为了对ThreadLocal有个宏观的认识,我们先来看下ThreadLocal的内存结构图

从内存结构图,我们可以看到:

  • Thread类中,有个ThreadLocal.ThreadLocalMap 的成员变量。
  • ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,keyThreadLocal本身,valueThreadLocal的泛型对象值。

关键源码分析

对照着关键源码来看,更容易理解一点哈~

首先看下Thread类的源码,可以看到成员变量ThreadLocalMap的初始值是为null

java
复制代码
public class Thread implements Runnable {
   //ThreadLocal.ThreadLocalMap是Thread的属性
   ThreadLocal.ThreadLocalMap threadLocals = null;
}

成员变量ThreadLocalMap的关键源码如下:

scala
复制代码
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;
        }
    }
    //Entry数组
    private Entry[] table;
    
    // ThreadLocalMap的构造器,ThreadLocal作为key
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
}

ThreadLocal类中的关键set()方法:

typescript
复制代码
 public void set(T value) {
        Thread t = Thread.currentThread(); //获取当前线程t
        ThreadLocalMap map = getMap(t);  //根据当前线程获取到ThreadLocalMap
        if (map != null)  //如果获取的ThreadLocalMap对象不为空
            map.set(this, value); //K,V设置到ThreadLocalMap中
        else
            createMap(t, value); //创建一个新的ThreadLocalMap
    }
    
     ThreadLocalMap getMap(Thread t) {
       return t.threadLocals; //返回Thread对象的ThreadLocalMap属性
    }

    void createMap(Thread t, T firstValue) { //调用ThreadLocalMap的构造函数
        t.threadLocals = new ThreadLocalMap(this, firstValue); this表示当前类ThreadLocal
    }
    

ThreadLocal类中的关键get()方法

scss
复制代码
    public T get() {
        Thread t = Thread.currentThread();//获取当前线程t
        ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
        if (map != null) { //如果获取的ThreadLocalMap对象不为空
            //由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value; 
                return result;
            }
        }
        return setInitialValue(); //初始化threadLocals成员变量的值
    }
    
     private T setInitialValue() {
        T value = initialValue(); //初始化value的值
        Thread t = Thread.currentThread(); 
        ThreadLocalMap map = getMap(t); //以当前线程为key,获取threadLocals成员变量,它是一个ThreadLocalMap
        if (map != null)
            map.set(this, value);  //K,V设置到ThreadLocalMap中
        else
            createMap(t, value); //实例化threadLocals成员变量
        return value;
    }

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

  • Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap
  • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,keyThreadLocal本身,valueThreadLocal的泛型值。
  • 并发多线程场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离

20. 如何保证多线程下 i++ 结果正确?

  • 使用循环CAS,实现i++原子操作
  • 使用锁机制,实现i++原子操作
  • 使用synchronized,实现i++原子操作
  • 21. 如何检测死锁?怎么预防死锁?死锁四个必要条件

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

死锁的四个必要条件:

1.互斥条件,进程要求对所分配的资源进行排它性控制,即该资源一次仅能为一个进程所享用。

2.请求并保持,即该进程对已获得的资源保持不放,又对新的资源保持请求的状态,就像个贪心的人,吃着碗里,看着锅里。

3.不可剥夺,该资源仅能当前进行释放,偏偏这个贪心的人房产证名字写的是他。

4.环路等待条件,闭环了,兄弟们,他想要的东西是他老婆的,他老婆又想要他手里的房产证。 如何预防死锁?

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

22. 如果线程过多,会怎样?

使用多线程可以提升程序性能。但是如果使用过多的线程,则适得其反。

过多的线程会影响程序的系统。

  • 一方面,线程的启动和销毁,都是需要开销的。
  • 其次,过多的并发线程也会导致共享有限资源的开销增大。过多的线程,还会导致内存泄漏,笔者在以前公司,看到一个生产问题:一个第三方的包是使用new Thread来实现的,使用完没有恰当回收销毁,最后引发内存泄漏问题。

因此,我们平时尽量使用线程池来管理线程。同时还需要设置恰当的线程数

23. 聊聊happens-before原则

在Java语言中,有一个先行发生原则(happens-before)。它包括八大规则,如下:

  • 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 管程锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程终止规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
  • 传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则

24. 如何实现两个线程间共享数据

  1. 可以通过类变量直接将数据放到主存中
  2. 通过并发的数据结构来存储数据
  3. 使用volatile变量或者锁
  4. 调用atomic类(如AtomicInteger)

25. LockSupport作用是?

LockSupport是一个工具类。它的主要作用是挂起和唤醒线程。该工具类是创建锁和其他同步类的基础。它的主要方法是

arduino
复制代码
public static void park(Object blocker); // 暂停指定线程
public static void unpark(Thread thread); // 恢复指定的线程
public static void park(); // 无期限暂停当前线程

看个代码的例子:

typescript
复制代码
public class LockSupportTest {

    private static Object object = new Object();
    static MyThread thread = new MyThread("线程田螺");

    public static class MyThread extends Thread {

        public MyThread(String name) {
            super(name);
        }

        @Override public void run() {
            synchronized (object) {
                System.out.println("线程名字: " + Thread.currentThread());
                try {
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LockSupport.park();
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("线程被中断了");
                }
                System.out.println("继续执行");
            }
        }
    }

    public static void main(String[] args) {
        thread.start();
        LockSupport.unpark(thread);
        System.out.println("恢复线程调用");
    }
}
//output
恢复线程调用
线程名字: Thread[线程田螺,5,main]
继续执行

因为thread线程内部有休眠2秒的操作,所以unpark方法的操作肯定先于park方法的调用。为什么thread线程最终仍然可以结束,是因为parkunpark会对每个线程维持一个许可证(布尔值)

6 线程池如何调优,如何确认最佳线程数?

复制代码
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

我们的服务器CPU核数为8核,一个任务线程cpu耗时为20ms,线程等待(网络IO、磁盘IO)耗时80ms,那最佳线程数目:( 80 + 20 )/20 * 8 = 40。也就是设置 40个线程数最佳。

27. 为什么要用线程池?

线程池:一个管理线程的池子。线程池可以:

  • 管理线程,避免增加创建线程和销毁线程的资源损耗。
  • 提高响应速度。
  • 重复利用线程。

28. Java的线程池执行原理

线程池的执行原理如下:

为了形象描述线程池执行,打个比喻:

  • 核心线程比作公司正式员工
  • 非核心线程比作外包员工
  • 阻塞队列比作需求池
  • 提交任务比作提需求

29. 聊聊线程池的核心参数

我们先来看看ThreadPoolExecutor的构造函数

java
复制代码
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
   long keepAliveTime,
   TimeUnit unit,
   BlockingQueue<Runnable> workQueue,
   ThreadFactory threadFactory,
   RejectedExecutionHandler handler) 
  • corePoolSize:线程池核心线程数最大值
  • maximumPoolSize:线程池最大线程数大小
  • keepAliveTime:线程池中非核心线程空闲的存活时间大小
  • unit:线程空闲存活时间单位
  • workQueue:存放任务的阻塞队列
  • threadFactory:用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
  • handler:线城池的饱和策略事件,主要有四种类型拒绝策略。

四种拒绝策略

  • AbortPolicy(抛出一个异常,默认的)
  • DiscardPolicy(直接丢弃任务)
  • DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
  • CallerRunsPolicy(交给线程池调用所在的线程进行处理)

几种工作阻塞队列

  • ArrayBlockingQueue(用数组实现的有界阻塞队列,按FIFO排序量)
  • LinkedBlockingQueue(基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列)
  • DelayQueue(一个任务定时周期的延迟执行的队列)
  • PriorityBlockingQueue(具有优先级的无界阻塞队列)
  • SynchronousQueue(一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态)

30.当提交新任务时,异常如何处理?

OK,线程的异常处理,我们可以直接try...catch捕获。

31. AQS组件,实现原理

AQS,即AbstractQueuedSynchronizer,是构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。可以回答以下这几个关键点哈:

  • state 状态的维护。
  • CLH队列
  • ConditionObject通知
  • 模板方法设计模式
  • 独占与共享模式。
  • 自定义同步器。
  • AQS全家桶的一些延伸,如:ReentrantLock等。

31.1 state 状态的维护

  • state,int变量,锁的状态,用volatile修饰,保证多线程中的可见性。
  • getState()和setState()方法采用final修饰,限制AQS的子类重写它们两。
  • compareAndSetState()方法采用乐观锁思想的CAS算法操作确保线程安全,保证状态 设置的原子性。

31.2 CLH队列

CLH 同步队列,全英文Craig, Landin, and Hagersten locks。是一个FIFO双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。AQS依赖它来完成同步状态state的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。

31.3 ConditionObject通知

我们都知道,synchronized控制同步的时候,可以配合Object的wait()、notify(),notifyAll() 系列方法可以实现等待/通知模式。而Lock呢?它提供了条件Condition接口,配合await(),signal(),signalAll() 等方法也可以实现等待/通知机制。ConditionObject实现了Condition接口,给AQS提供条件变量的支持

ConditionObject队列与CLH队列的爱恨情仇:

  • 调用了await()方法的线程,会被加入到conditionObject等待队列中,并且唤醒CLH队列中head节点的下一个节点。
  • 线程在某个ConditionObject对象上调用了singnal()方法后,等待队列中的firstWaiter会被加入到AQS的CLH队列中,等待被唤醒。
  • 当线程调用unLock()方法释放锁时,CLH队列中的head节点的下一个节点(在本例中是firtWaiter),会被唤醒。

31.4 模板方法设计模式

模板方法模式:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

AQS的典型设计模式就是模板方法设计模式啦。AQS全家桶(ReentrantLock,Semaphore)的衍生实现,就体现出这个设计模式。如AQS提供tryAcquire,tryAcquireShared等模板方法,给子类实现自定义的同步器。

31.5 独占与共享模式。

  • 独占式: 同一时刻仅有一个线程持有同步状态,如ReentrantLock。又可分为公平锁和非公平锁。
  • 共享模式:多个线程可同时执行,如Semaphore/CountDownLatch等都是共享式的产物。

31.6 自定义同步器

你要实现自定义锁的话,首先需要确定你要实现的是独占锁还是

32 Semaphore原理

Semaphore,我们也把它叫做信号量。可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

我们可以把它简单的理解成我们停车场入口立着的那个显示屏,每当有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,显示屏上显示的剩余车辆就会加1,当显示屏上的剩余车位为0时,停车场入口的栏杆就不会再打开,车辆就无法进入停车场了,直到有一辆车从停车场出去为止。

32.1 Semaphore使用demo

我们就以停车场的例子,来实现demo。

假设停车场最多可以停20辆车,现在有100辆要进入停车场。

我们很容易写出以下代码;

csharp
复制代码
public class SemaphoreTest {

    private  static Semaphore semaphore=new Semaphore(20);

    public static void main(String[] args) {
         
        ExecutorService executorService= Executors.newFixedThreadPool(200);

        //模拟100辆车要来
        for (int i = 0; i < 100; i++) {
            executorService.execute(()->{
                System.out.println("===="+Thread.currentThread().getName()+"准备进入停车场==");
                //车位判断
                if (semaphore.availablePermits() == 0) {
                    System.out.println("车辆不足,请耐心等待");
                }

                try {
                    //获取令牌尝试进入停车场
                    semaphore.acquire();
                    System.out.println("====" + Thread.currentThread().getName() + "成功进入停车场");
                    //模拟车辆在停车场停留的时间
                    Thread.sleep(new Random().nextInt(20000));
                    System.out.println("====" + Thread.currentThread().getName() + "驶出停车场");
                    //释放令牌,腾出停车场车位
                    semaphore.release();
                 } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            });
            //线程池关闭          
            executorService.shutdown();
        }
    }
}

32.2 Semaphore原理

我们来看下实现的原理是怎样的。

  • Semaphore构造函数
  • 可用令牌数
  • 获取令牌
  • 释放令牌

33 synchronized做了哪些优化?什么是偏向锁?什么是自旋锁?锁租化?

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

  • 偏向锁:在无竞争的情况下,只是在Mark Word里存储当前线程指针,CAS操作都不做。
  • 轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量带来的性能消耗。但是,如果存在锁竞争,除了互斥量本身开销,还额外有CAS操作的开销。
  • 自旋锁:减少不必要的CPU上下文切换。在轻量级锁升级为重量级锁时,就使用了自旋加锁的方式
  • 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
  • 锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

34 什么是上下文切换?

什么是CPU上下文?

CPU 寄存器,是CPU内置的容量小、但速度极快的内存。而程序计数器,则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。它们都是 CPU 在运行任何任务前,必须的依赖环境,因此叫做CPU上下文。

什么是CPU上下文切换?

它是指,先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

一般我们说的上下文切换,就是指内核(操作系统的核心)在CPU上对进程或者线程进行切换。进程从用户态到内核态的转变,需要通过系统调用来完成。系统调用的过程,会发生CPU上下文的切换。

所以大家有时候会听到这种说法,线程的上下文切换。 它指,CPU资源的分配采用了时间片轮转,即给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是线程的上下文切换。看个图,可能会更容易理解一点

35.为什么wait(),notify(),notifyAll()在对象中,而不在Thread类中

锁只是个一个标记,存在对象头里面。

下面从面向对象和观察者模式角度来分析。

  • 面向对象的角度:我们可以把wait和notify直接理解为get和set方法。wait和notify方法都是对对象的锁进行操作,那么自然这些方法应该属于对象。举例来说,门对象上有锁属性,开锁和关锁的方法应该属于门对象,而不应该属于人对象。
  • 从观察者模式的角度:对象是被观察者,线程是观察者。被观察者的状态如果发生变化,理应有被观察者去轮询通知观察者,否则的话,观察者怎么知道notify方法应该在哪个时刻调用?n个观察者的notify又如何做到同时调用?
  • 来源:知乎 www.zhihu.com/question/32…

36. 线程池中 submit()和 execute()方法有什么区别?

  • execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。
  • execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
  • execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。

37 AtomicInteger 的原理?

AtomicInteger的底层,是基于CAS实现的。我们可以看下AtomicInteger的添加方法。如下

arduino
复制代码
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    通过Unsafe类的实例来进行添加操作
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//使用了CAS算法实现

        return var5;
    }

注意:compareAndSwapInt是一个native方法哈,它是基于CAS来操作int类型的变量。并且,其它的原子操作类基本也大同小异。

38 Java中用到的线程调度算法是什么?

我们知道有两种调度模型:分时调度和抢占式调度

  • 分时调度模型:让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的 CPU 的时间片。
  • 抢占式调度:优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。

Java默认的线程调度算法是抢占式。即线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

39. shutdown() 和 shutdownNow()的区别

  • shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大。
  • shutdown()只是关闭了提交通道,用submit()是无效的;而内部的任务该怎么跑还是怎么跑,跑完再彻底停止线程池。

40 说说几种常见的线程池及使用场景?

几种常用线程池:

  • newFixedThreadPool (固定数目线程的线程池)
  • newCachedThreadPool(可缓存线程的线程池)
  • newSingleThreadExecutor(单线程的线程池)
  • newScheduledThreadPool(定时及周期执行的线程池)

40.1 newFixedThreadPool

arduino
复制代码
  public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }
  • 核心线程数和最大线程数大小一样
  • 没有所谓的非空闲时间,即keepAliveTime为0
  • 阻塞队列为无界队列LinkedBlockingQueue

使用场景

FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。

40.2 newCachedThreadPool

typescript
复制代码
 public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }
  • 核心线程数为0
  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是SynchronousQueue
  • 非核心线程空闲存活时间为60秒

使用场景

当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

40.3 newSingleThreadExecutor 单线程的线程池

typescript
复制代码
  public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }
  • 核心线程数为1
  • 最大线程数也为1
  • 阻塞队列是LinkedBlockingQueue
  • keepAliveTime为0

使用场景

适用于串行执行任务的场景,一个任务一个任务地执行。

40.4 newScheduledThreadPool

java
复制代码
  public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是DelayedWorkQueue
  • keepAliveTime为0
  • scheduleAtFixedRate() :按某种速率周期执行
  • scheduleWithFixedDelay():在某个延迟后执行

使用场景

周期性执行任务的场景,需要限制线程数量的场景

41 什么是FutureTask

FutureTask是一种可以取消的异步的计算任务。它的计算是通过Callable实现的,可以把它理解为是可以返回结果的Runnable

使用FutureTask的优点:

  • 可以获取线程执行后的返回结果;
  • 提供了超时控制功能。

它实现了Runnable接口和Future接口,底层基于生产者消费者模式实现。

FutureTask用于在异步操作场景中,FutureTask作为生产者(执行FutureTask的线程)和消费者(获取FutureTask结果的线程)的桥梁,如果生产者先生产出了数据,那么消费者get时能会直接拿到结果;如果生产者还未产生数据,那么get时会一直阻塞或者超时阻塞,一直到生产者产生数据唤醒阻塞的消费者为止。

42 java中interrupt(),interrupted()和isInterrupted()的区别

  • interrupt 它是真正触发中断的方法。
  • interrupted是Thread中的一个类方法,它也调用了isInterrupted(true)方法,不过它传递的参数是true,表示将会清除中断标志位。
  • isInterruptedThread类中的一个实例方法,可以判断实例线程是否被中断。。
scss
复制代码
    
    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }
    
   public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

    public boolean isInterrupted() {
        return isInterrupted(false);
    }

43 有三个线程T1,T2,T3,怎么确保它们按顺序执行

可以使用join方法解决这个问题。比如在线程A中,调用线程B的join方法表示的意思就是:A等待B线程执行完毕后(释放CPU执行权),在继续执行。

代码如下:

csharp
复制代码
public class ThreadTest {

    public static void main(String[] args) {

        Thread spring = new Thread(new SeasonThreadTask("春天"));
        Thread summer = new Thread(new SeasonThreadTask("夏天"));
        Thread autumn = new Thread(new SeasonThreadTask("秋天"));

        try
        {
            //春天线程先启动
            spring.start();
            //主线程等待线程spring执行完,再往下执行
            spring.join();
            //夏天线程再启动
            summer.start();
            //主线程等待线程summer执行完,再往下执行
            summer.join();
            //秋天线程最后启动
            autumn.start();
            //主线程等待线程autumn执行完,再往下执行
            autumn.join();
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

class SeasonThreadTask implements Runnable{

    private String name;

    public SeasonThreadTask(String name){
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 1; i <4; i++) {
            System.out.println(this.name + "来了: " + i + "次");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

春天来了: 1次
春天来了: 2次
春天来了: 3次
夏天来了: 1次
夏天来了: 2次
夏天来了: 3次
秋天来了: 1次
秋天来了: 2次
秋天来了: 3

44 有哪些阻塞队列

  • ArrayBlockingQueue 一个由数组构成的有界阻塞队列
  • LinkedBlockingQueue 一个由链表构成的有界阻塞队列
  • PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列
  • DelayQueue 一个使用优先队列实现的无界阻塞队列。
  • SynchroniouQueue 一个不储存元素的阻塞队列
  • LinkedTransferQueue 一个由链表结构组成的无界阻塞队列
  • LinkedBlockingDeque 一个由链表结构组成的双向阻塞队列

45 Java中ConcurrentHashMap的并发度是什么?

并发度就是segment的个数,通常是2的N次方。默认是16

46 Java线程有哪些常用的调度方法?

46.1 线程休眠

Thread.sleep(long)方法,使线程转到超时等待阻塞(TIMED_WAITING) 状态。long参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,线程自动转为就绪(Runnable)状态。

46.2 线程中断

interrupt()表示中断线程。需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到wait()/sleep()/join()后,就会立刻抛出InterruptedException。可以用isInterrupted()来获取状态。

46.3 线程等待

Object类中的wait()方法,会导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()唤醒方法。

46.4 线程让步

Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

46.5 线程通知

Object的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。

notifyAll(),则是唤醒在此对象监视器上等待的所有线程。

47. ReentrantLock的加锁原理

ReentrantLock,是可重入锁,是JDK5中添加在并发包下的一个高性能的工具。它支持同一个线程在未释放锁的情况下重复获取锁。

47.1 ReentrantLock使用的模板

我们先来看下是ReentrantLock使用的模板:

csharp
复制代码
   //实例化对象
    ReentrantLock lock = new ReentrantLock();
    //获取锁操作
    lock.lock();
    try {
        // 执行业务代码逻辑
    } catch (Exception ex) {
        //异常处理
    } finally {
        // 解锁操作
        lock.unlock();
    }

47.2 什么是非公平锁,什么是公平锁?

ReentrantLock无参构造函数,默认创建的是非公平锁,如下:

csharp
复制代码
public ReentrantLock() {
    sync = new NonfairSync();
}

而通过fair参数指定使用公平锁(FairSync)还是非公平锁(NonfairSync)

arduino
复制代码
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

什么是公平锁?

  • 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

什么是非公平锁?

  • 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

47.3 lock()加锁流程

大家可以结合AQS + 公平锁/非公平锁 + CAS去讲ReentrantLock的原理哈。

48. 线程间的通讯方式

48.1 volatile和synchronized关键字

  • volatile关键字用来修饰共享变量,保证了共享变量的可见性,任何线程需要读取时都要到内存中读取(确保获得最新值)。
  • synchronized关键字确保只能同时有一个线程访问方法或者变量,保证了线程访问的可见性和排他性。

48.2 等待/通知机制

等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B 调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而 执行后续操作。

48.3 管道输入/输出流

  • 管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要 用于线程之间的数据传输,而传输的媒介为内存。
  • 管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。

48.4 join()方法

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才 从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时 时间里没有终止,那么将会从该超时方法中返回。

48.5 ThreadLocal

ThreadLocal,即线程本地变量(每个线程都有自己唯一的一个哦),是一个以ThreadLocal对象为键、任意对象为值的存储结构。底层是一个ThreadLocalMap来存储信息,key是弱引用,value是强引用,所以使用完毕后要及时清理(尤其使用线程池时)。

大家可以看下我之前这篇文章,ThreadLocal的八个关键知识点

49 写出3条你遵循的多线程最佳实践

  • 多用同步类,少用wait,notify
  • 少用锁,应当缩小同步范围
  • 给线程一个自己的名字
  • 多用并发集合少用同步集合

50. 为什么阿里发布的 Java开发手册中强制线程池不允许使用 Executors 去创建?

这是因为,JDK开发者提供了线程池的实现类都是有坑的,如newFixedThreadPoolnewCachedThreadPool都有内存泄漏的坑