Java多线程

面试题整理:多线程

1、进程和线程的区别

  • 进程是资源分配的基本单位,线程是CPU调度和执行的基本单位。
  • 线程依赖于进程,一个线程只属于一个进程,一个进程可以有多个线程。
  • 进程分配有独立的内存资源,线程共享进程的内存资源。
  • 进程的创建、切换开小大,线程的创建和切换开销小,因此也称为轻量级进程。

2、说说并发与并行的区别?

  • 并发:同一时间段,多个任务都在执行 (同一时刻只有一个任务执行);
  • 并行:同一时刻,多个任务同时执行。

3、如何避免线程死锁

  1. 破坏互斥条件 : 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  2. 破坏请求与保持条件 : 一次性申请所有的资源。
  3. 破坏不剥夺条件 : 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件 : 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

4、为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run()方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

5、说说自己是怎么使用 synchronized 关键字

1、synchronized 关键字解决的是多个线程之间访问资源的同步性, synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

2、synchronized 关键字最主要的三种使用方式:

  1. 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。
  2. 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得当前 class 的锁。
  3. 修饰代码块: 指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。 synchronized( .class) 表示进入同步代码前要获得当前 class 的锁。

6、构造方法可以使用 synchronized 关键字修饰么?

构造方法不能使用 synchronized 关键字修饰。构造方法本身就属于线程安全的,不存在同步的构造方法一说。

7、讲一下 synchronized 关键字的底层原理

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。
不过两者的本质都是对对象监视器 monitor 的获取。

8、说说 synchronized 关键字和 volatile 关键字的区别

  • volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比 synchronized 关键字要好。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块。
  • volatile 关键字能保证数据的可⻅性,但不能保证数据的原子性。 synchronized 关键字两者都能保证。
  • volatile 关键字主要用于解决变量在多个线程之间的可⻅性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

9、ThreadLocal内存泄漏问题了解吗?

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来, ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现
中已经考虑了这种情况,在调用 set() 、 get() 、 remove() 方法的时候,会清理掉 key 为 null的记录。

10、Java中的四种引用

  1. 强引用:如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。
  2. 软引用:如果一个对象具有软引用,它就不会被垃圾回收器回收。只有在内存空间不足时,软引用才会被垃圾回收器回收。这种引用常常被用来实现缓存技术。因为缓存区里面的东西,之后在内存不足的时候才会被清空。
  3. 弱引用:如果一个对象具有弱引用,在垃圾回收时候,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象。
  4. 虚引用:如果一个对象具有弱引用,就相当于没有引用,在任何时候都有可能被回收。虚引用是使用PhantomReference创建的引用,虚引用也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个。一个对象是否有虚引用的存在,完全不会对其生命周期构成影响,也无法通过虚引用获得一个对象实例。

11、为什么要用线程池?

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

12、Executors四种线程池

  • newCachedThreadPool : 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

    public static ExecutorService newCachedThreadPool() {
       return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
       60L, TimeUnit.SECONDS,
       new SynchronousQueue<Runnable>());
    }
    
  • newFixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

    public static ExecutorService newFixedThreadPool(int nThreads) {
       return new ThreadPoolExecutor(nThreads, nThreads,
                                     0L, TimeUnit.MILLISECONDS,
                                     new LinkedBlockingQueue<Runnable>());
    }
    
  • newSingleThreadExecutor : 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。

    public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
      (new ThreadPoolExecutor(1, 1,
                              0L, TimeUnit.MILLISECONDS,
                              new LinkedBlockingQueue<Runnable>()));
    }
    
  • newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。

    public ScheduledThreadPoolExecutor(int corePoolSize) {
     super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
           new DelayedWorkQueue());
    }
    

13、线程池参数

  1. 核心线程数
    2.总线程数
    3.存活时间
    4.存活时间单位
    5.线程存储队列
    5.1.有限队列-SynchronousQueue(在newCachedThreadPool()方法中使用)
    这是一个内部没有任何容量的阻塞队列,任何一次插入操作的元素都要等待相对的删除/读取操作,否则进行插入操作的线程就要一直等待,反之亦然
    public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
    60L, TimeUnit.SECONDS,
    new SynchronousQueue());
    }
    有限队列-ArrayBlockingQueue
    是一个由数组支持的有界阻塞队列(先进先出),假设初始队列长度为2,当添加第三个元素时线程会进行阻塞。同样的,当元素为空的时候,进行获取也会进行阻塞。
    5.2 无限队列-LinkedBlockingQueue
    创建ThreadPoolExecutor常用的队列。
    当不设置队列初始长度,则为无界队列。
    当设置队列初始长度则类似于ArrayBlockingQueue即插入与读取都会为空进行堵塞。
    6.ThreadFactory给一组线程用来命名,阿里巴巴规范建议使用,方便以后的错误查找。
    7.默认拒绝策略
    7.1 AbortPolicy(抛出一个异常,默认的)
    7.2 CallerRunsPolicy(交给线程池调用所在的线程进行处理)
    7.3 DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
    7.4 DiscardPolicy(直接丢弃任务)
    PS:使用DiscardPolicy或者DiscardOldestPolicy,并且线程池饱和了的时候,我们将会直接丢弃任务,不会抛出任何异常。这个时候再来调用get方法是主线程就会一直等待子线程返回结果,直到超时抛出TimeoutException。

14、AQS原理了解么?

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线
程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞
等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁
的线程加入到队列中。

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

image-20210901104302373

AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队
工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

15、AQS对资源的共享方式

1、Exclusive(独占): 只有一个线程能执行,如 ReentrantLock 。又可分为公平锁和非公平锁:

  • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
  • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

2、Share(共享): 多个线程可同时执行,如CountDownLatch 、 Semaphore 、 CountDownLatch 、 CyclicBarrier 、 ReadWriteLock 我们都会在后面讲到。

ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多
个线程同时对某一资源进行读。

posted @ 2021-09-01 11:07  151it  阅读(67)  评论(0)    收藏  举报