JAVA常见面试题总结(三)多线程

1、并发和并行有什么区别?

  • 并行指两个或多个事件在同一时刻发生;而并发指的是两个或多个事件在同一时间间隔发生。
  • 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
  • 并行是在多台处理器上同时处理多个任务,并发是在一台处理器上“同时”处理多个任务。

2、线程和进程的区别?

进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是CPU调度和分派的基本单位,是比程序更小的能独立运行的基本单位,同一进程中的多个线程可以并发执行。

3、守护线程是什么?

守护线程(Daemon Thread),是个服务线程,为服务其它线程而存在的线程,守护线程会自己结束(要服务的线程关闭了),而不需要手动去结束它。例如JVM的垃圾回收线程就是守护线程。

4、创建线程有哪几种方式?

  1. 继承Thread类创建线程类

    • 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程要完成的任务,因此把run()方法称为执行体。

    • 创建Thread子类的实例,即创建了线程对象。

    • 调用线程对象的start()方法来启动该线程。

/**
 * @author ZhengNC
 * @date 2020/5/22 11:21
 */
public class MyThread extends Thread {

    @Override
    public void run(){
        System.out.println("MyThread ......");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
  1. 通过实现Runnable接口创建线程类
    • 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法的方法体同样是该线程的执行体。
    • 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
    • 调用线程对象的start()方法来启动该线程。
/**
 * @author ZhengNC
 * @date 2020/6/2 19:07
 */
public class MyRunnableImpl implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnableImpl......");
    }

    public static void main(String[] args) {
        MyRunnableImpl myRunnable = new MyRunnableImpl();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}
  1. 通过Callable和Future创建线程
    • 定义Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
    • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable实现类对象的call()方法的返回值。
    • 使用FutureTask对象作为Thread的target创建Thread对象,该Thread对象才是真正的线程对象。
    • 调用线程对象的start()方法来启动线程。
    • 调用FutureTask对象的get()方法来获取子线程结束后的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

/**
 * @author ZhengNC
 * @date 2020/6/2 19:10
 */
public class MyCallable<V> implements Callable {
    @Override
    public V call() throws Exception {
        System.out.println("MyCallable.......");
        return (V) "end";
    }

    public static void main(String[] args) throws Exception{
        MyCallable<String> myCallable = new MyCallable<>();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
}

5、Runnable 和 Callable 有什么区别?

  • Runnable 接口中的 run() 方法的返回值是 void ,它做的事情只是纯粹的去执行 run() 方法中的代码。
  • Callable 接口中的 call() 方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

6、线程有哪些状态?

线程有五种状态:创建、就绪、运行、阻塞、死亡。

  • 创建:生成线程对象,还没有调用对象的 start() 方法,此时线程处于创建状态。
  • 就绪:调用了线程的 start() 方法之后,但是此时线程调度程序还没有把该线程设置为当前线程,此时的线程就是就绪状态。线程从等待或睡眠状态回来之后也会处于就绪状态。
  • 运行:线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就处于运行状态,开始执行 run() 方法中的代码。
  • 阻塞:当线程正在运行的时候被暂停就会处于阻塞状态,比如为了等待某个资源就绪,或者是调用了 wait() 、 suspend() 、 sleep() 方法。
  • 死亡:如果一个线程的 run() 方法执行结束或者调用 stop() 方法后,该线程就会处于死亡状态。对于死亡状态的线程,无法使用 start() 方法再使其进入就绪状态。

在这里插入图片描述

7、wait() 、 sleep() 和 suspend() 方法有什么区别?

sleep() 是线程类 Thread 的静态方法,调用之后会令当前线程暂停执行指定的时间,把执行机会让给其它线程,但是监控状态依然保持,到时会自动恢复。调用 sleep() 方法不会释放对象锁。

wait() 方法是 Object 类的方法,对此对象调用 wait() 方法会导致本线程放弃对象锁,进入此对象的等待锁定池,只有针对此对象发出 notify() 、 notifyAll() 方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

suspend() 使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。但由于 suspend() 方法很容易引起死锁问题,已经不推荐使用了。

8、notify() 和 notifyAll() 有什么区别?

  • 如果线程调用了对象的 wait() 方法,线程就会进入此对象的等待池中,等待池中的线程不会去竞争此对象的锁。
  • 对象的 notify() 和 notifyAll() 都是唤醒线程的方法,被唤醒的线程会进入此对象的锁池中,锁池中的线程会竞争此对象的锁。两个方法不同的是 notify() 会随机唤醒一个wait线程,而 notifyAll() 会唤醒所有wait线程。
  • 优先级高的线程更容易获取到对象的锁,获取到锁的线程继续执行,直到运行完成或再次调用 wait() 方法释放锁,锁池中线程会继续竞争该对象的锁。

9、run() 和 start() 方法有什么区别?

  • 线程都是通过Thread对象的 run() 方法来完成操作的,run() 方法是线程的执行体,是一个普通的函数,直接调用 run() 方法只是相当于调用了一个普通函数,要等 run() 方法执行完毕之后才能继续往下执行,并不能实现线程的特性,启动一个线程需要调用线程的 start() 方法。
  • 调用 start() 方法用来启动一个线程,此时线程处于就绪状态,不用等待 run() 方法是否执行完毕就可以继续执行下面的代码,真正实现了多线程运行。当线程处于运行状态时,由Thread对象调用 run() 方法来完成任务,run() 方法被称为线程的执行体,是线程真正要执行的内容,当 run() 方法执行完毕以后线程结束。

10、创建线程池有哪几种方式?

I、使用 Executors 创建线程池

  1. Executors.newFixedThreadPool(int nThreads)

    创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这是线程池的规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。

  2. Executors.newCachedThreadPool()

    创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。

  3. Executors.newSingleThreadExecutor()

    创建一个单线程的线程池,它创建单个线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它,此线程池的特点是能确保依照任务在队列中的顺序来串行执行。

  4. Executors.newScheduledThreadPool(int corePoolSize)

    创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

II、直接调用 ThreadPoolExecutor 的构造方法创建线程池对象

使用 Executors 创建线程池有资源耗尽的风险(OOM:Out Of Memory,内存溢出),因此可以通过 ThreadPoolExecutor 的方式创建线程池对象。

  1. FixedThreadPool 和 SingleThreadPool:

    允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量请求,从而导致OOM。

  2. CachedThreadPool:

    允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM。

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        10, 10, 60L,
        TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(10))

11、线程池都有哪些状态?

线程池的状态有五种:RUNNING(运行状态)、SHUTDOWN(关闭状态)、STOP(停止状态)、TIDYING(整理状态)、TERMINATED(结束状态)。

在这里插入图片描述

  • RUNNING(运行状态):

    线程池一旦被创建就处于RUNNING状态,此状态可以接收新任务,也可以处理已添加的任务。

  • SHUTDOWN(关闭状态):

    线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。

  • STOP(停止状态):

    线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。

  • TIDYING(整理状态):

    当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理,可以通过重写terminated()函数来实现。

  • TERMINATED(结束状态):

    线程池的彻底终止的状态,当terminated()方法执行完毕后线程池就会变为TERMINATED状态。

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

  • 接收的参数不同,submit() 有三个重载方法 submit(Callable task)、submit(Runnable task, T result)、submit(Runnable task)。execute() 只有一个重载方法 execute(Runnable command)。
  • 返回值不同,submit() 有返回值 Future,execute() 没有返回值。
  • submit() 的返回值 Future 在调用 get() 方法时可以捕获处理异常。

13、在 Java 程序中怎么保证多线程的运行安全?

多线程有三大特性:原子性、可见性、有序性

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作。
  • 可见性:一个线程对主内存的修改可以及时的被其它线程看到。
  • 有序性:程序执行的顺序按照代码的先后顺序执行。

在 Java 中使用自动锁 synchtonized 关键字 或 手动锁 Lock 来保证线程运行时的安全。

14、谈一谈Java多线程锁 synchronized 的升级原理。

在 Java 中,锁共有4种状态:无锁、偏向锁、轻量级锁、重量级锁。

  • 偏向锁:经过HotSpot的作者大量研究发现,大多数时候时不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会浪费性能,因此在没有竞争的时候,线程只需把自己的信息存入对象信息里,告诉别的线程这个对象可能是我在用(为什么说是“可能”呢?因为偏向锁不会主动释放,所以线程线程消亡后不会清除自己的信息)。

  • 轻量级锁:轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚阻塞不久这个锁就被释放了,这个切换状态的代价就得不偿失了,因此这个时候就不阻塞这个线程,让它自旋着等待锁的释放。

  • 重量级锁:如果竞争一个锁的线程很多,或者线程执行时间较长,大量的CPU时间就会浪费在线程的自旋操作上。这个时候就需要把锁升级成重量级锁,把除了持有锁的线程都阻塞,防止CPU浪费时间去做自旋操作。

在这里插入图片描述

简单的锁升级示意图
锁状态优点缺点适用场景
偏向锁加锁解锁无需额外的消耗,和非同步方法时间相差纳秒级别如果竞争的多,会带来额外的锁撤销消耗基本没有线程竞争锁的同步场景
轻量级锁竞争的线程不会阻塞,使用自旋,提高程序响应速度如果一直不能获取到锁,长时间的自旋会造成CPU消耗适用于少量线程竞争锁对象,且线程持有锁的时间不长,追求响应速度的场景
重量级锁线程竞争不使用CPU自旋,不会导致CPU空转消耗CPU资源线程阻塞,响应时间长很多线程竞争锁,且锁持有的时间长,追求吞吐量的场景

15、什么是死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

16、怎么防止死锁?

要防止死锁首先要清楚死锁产生的四个必要条件:

  • 互斥条件:进程分配到的资源不允许其它进程访问,若其它进程要访问该资源,只能等待,直至占用该资源的进程使用完毕后释放该资源。
  • 请求保持条件:进程获取到一定资源后,又请求其它资源,但是其它资源可能被其它进程占有,请求被阻塞,但又对自己获得的资源保持不放。
  • 不可剥夺条件:是指进程已获得的资源,在未使用完成之前不可被剥夺,只能在自己使用完成后主动释放。
  • 环路等待条件:是指进程进程等待资源时,若干进程之间形成了一种头尾相接的循环等待资源的关系。

这四个条件是形成死锁的必要条件,只要系统系统发生死锁,这些条件必然成立,而只要上述四个条件有一个不满足就不会发生死锁。

所以防止死锁的方法就是在系统设计、线程调度等方面注意如何不让这四个条件同时成立。

17、ThreadLocal 是什么?有哪些使用场景?

ThreadLocal 是 Java 提供的存储线程局部变量的类,它为每个线程维护了一个数组table,每个线程读取的数据都是相互独立的,线程可以通过get、set方法操作这些局部变量。

由于ThreadLocal存储的变量属于线程私有,不在多个线程间共享,所以它也是一种实现线程安全的方式。

但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

18、说一下 synchronized 底层实现原理?

synchronized 可以保证方法或代码块运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

synchronized 保证内存可见性:1、线程加锁后先清空工作内存中共享变量的值,从而保证在使用共享变量时会重新读取主内存的最新值。2、线程解锁前,必须把共享变量的最新值刷新到主内存中。

Java 中每个对象都可以作为锁,这是 synchronized 实现同步的基础:

  • 普通同步方法,锁的是当前实例对象
  • 静态同步方法,锁的是当前类的class对象
  • 同步方法块,锁的是括号里面的对象

19、synchronized 和 volatile 的区别是什么?

  • volatile 本质是告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized 则是锁定当前对象,只有当前线程可以访问该变量,其它线程被阻塞。
  • volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法和类级别。
  • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
  • volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

20、synchronized 和 Lock 有什么区别?

  • synchronized 是 Java 内置关键字,在 JVM 层面,Lock 是个 Java 的接口,其常见实现有 ReentrantLock 。
  • synchronized 无法判断是否获取到锁的状态,Lock 可以判断是否获取到锁。
  • synchronized 会自动释放锁,Lock需要在finally中手工释放锁,否则容易造成死锁。
  • synchronized 没有 Lock 的实现灵活,Lock 获取不到锁可以选择是否继续等待,而 synchronized 只能一直等待下去。
  • synchronized 的锁可重入、不可中断、非公平,而 Lock 锁可重入、可判断、可公平。
  • Lock 锁适合大量同步的代码的同步问题,synchronized 锁适合代码少量的同步问题。

21、说一下 atomic 的原理。

atomic 包中的类的基本特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型和引用类型)变量进行操作时,具有排他性(原子性),即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未能成功的线程可以自旋尝试,一直等到执行成功。

在这里插入图片描述

atomic 系列的类中的核心方法都会调用 sun.misc.Unsafe 类中的几个本地方法。Unsafe 类包含了大量对C代码的操作,包括直接的内存分配以及原子性操作,所以这个类被标记为非安全的,使用这个类要小心,否则会导致严重后果,例如分配内存可能会出现指针越界等问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值