JAVA后端面试《多线程》

多线程

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

  • 并行是指两个或多个事件在同一时刻发生在不同实体上。
  • 并发是指两个或多个事件在同一时间间隔发生在同一实体上。
  • 串行:单条线程按照先后顺序执行多个任务
  • 并发:在一台处理器上"同时"处理多个任务
  • 并行:在多台处理器上同时处理多个任务
  • 并发编程的目的:充分利用处理器的每一个核,以达到最高的处理性能。

2.线程和进程的区别?

  • 进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。
  • 线程是进程的一个实体,是cpu调度和分派的基本单位,是一个能独立运行的基本单位,同一进程中多个线程之间可以并发执行,以提高效率

3.Java中线程有哪几类?

  • 用户线程 User Thread
  • 守护线程 Daemon Thread:作用就是为其他线程的运行提供便利服务,比如垃圾回收线程就是一个守护线程
  • 两者区别:虚拟机的离开。如果User Thread已经全部运行完毕了,只剩Daemon Thread了,这时候虚拟机会直接离开,因为Daemon Thread没有可守护的线程了,所以程序没有继续运行必要了。

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

①继承Thread类,重写run方法,run方法体就是线程执行体。
②实现Runnable接口,重写run方法,run方法体就是线程执行体。
③实现Callable接口,重写call方法,通过FutureTask类包装Callable实现类再创建线程,FutureTask类已经实现了Runnable接口,call方法体就是线程执行体。

class H implements Callable {


    @Override
    public Object call() throws Exception {
        return null;
    }


    public static void main(String[] args) {
        H h =new H();
        FutureTask task=new FutureTask(h);
        Thread thread=new Thread(task);
        thread.start();
    }
}

5.说以下Runnable和Callable的区别?

  • Runnable接口中的run方法没有返回值
  • Callable接口中call方法有返回值,是1个泛型,和FutureTask配合可以获取异步执行的结果

6.线程都有哪些状态?

  • ①新建状态(new): 新创建了一个线程对象
  • ②可运行状态(runnable):线程对象创建后,其他线程调用了该对象的start方法,该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu的使用权
  • ③运行状态(running):可运行的线程获取到了cpu的使用权,执行程序代码
  • ④阻塞状态(block):是指线程因为某种原因放弃了cpu的使用权被暂停,sleep和wait方法都可以导致线程阻塞。
  • ⑤死亡状态(dead):线程run方法、main方法执行结束,或因异常退出run方法,则该线程的生命周期结束,死亡线程不可再次复生。
    在这里插入图片描述

7.sleep()和wait()方法的区别?

  • ① 方法来源:sleep是Thread的方法,wait是Object的方法。
  • ② 释放锁:sleep方法没有释放锁。wait方法释放了锁,使得其他线程可以抢到锁访问同步代码块或同步方法。
  • ③ 使用范围:wait,notify和notifyAll只能在同步方法或块中使用。sleep方法可以在任何地方使用。
  • ④ 捕获异常:sleep必须捕获异常。wait方法不需要捕获异常。

8.notify和notifyAll方法有什么区别?

  • 如果线程调用了对象的wait的方法,那么线程便会进入该对象的等待池中(等待池中线程不会去竞争该对象的锁)。
  • 当有线程调用了notify方法(随机唤醒一个wait状态线程)后,只有一个随机线程会从等待池中进入锁池中。如果调用的是notifyAll方法(唤醒所有wait状态线程),则会将等待池中所有线程移动到锁池中,等待锁竞争。

9.run方法和start方法的区别?

  • 一个线程对象对应一个run方法,run方法是线程执行体,负责完成线程具体内容,run方法运行结束,此线程就终止。
  • start方法用来启动一个线程,真正实现了多线程的运行。多个线程对象先后调用start方法,此时无需等待run方法体代码执行完毕,也可以直接运行下面的代码。

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

  • ①newFixedThreadPool(int nThreads)
    创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
  • ②newCachedThreadPool()
    创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
  • ③newSingleThreadExecutor()
    这是一个单线程的Executor,它创建单个
  • ④newScheduledThreadPool(int corePoolSize)
    创建一个固定长度的线程池,而且以延迟或定时的方式执行任务。

11.线程池有哪几种状态?

  • 线程池有五种状态:Running,ShutDown,Stop,Tidying,Terminated。
    在这里插入图片描述

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

  • ①接收参数: 接收的参数不一样
  • ②返回值:submit返回future,而execute无返回值
  • ③异常处理:submit方法方便异常处理
    submit方法执行线程得到一个future,如果调用这个future的API去获取结果,例如future.get(),如果线程中有异常产生,就可以通过在submit方法周围环绕try…catch来捕获这个异常。

13.JAVA程序中怎么保证多线程的安全运行?

多线程的安全性体现在以下3个方面:

  • ①原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作(Atomic开头的原子类,synchronized,Lock可以解决原子性问题)
  • ②可见性:一个线程对共享变量修改可以及时地被其他线程看到(synchronized,volatile,Lock可以解决可见性问题)
  • ③ 有序性:程序执行的顺序按照代码的先后顺序执行(happens-before原则可以实现有序性)

14.多线程锁的升级原理是什么?

java中,锁共有四种状态,级别从高到底依次为:无状态锁,偏向锁,轻量级锁和重量级锁,这几个状态会随着竞争情况而逐渐升级。锁可以升级但不能降级。
锁升级的示意图:
在这里插入图片描述

15.什么是死锁?

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

16.怎么防止死锁?

死锁的四个必要的条件:

  • ①互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占用该资源的进程使用完成后释放该资源。
  • ②请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其进程占用,此时请求阻塞,但又对自己获得的资源保持不妨。
  • ③不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
  • ④环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系。
    所以在系统设计,进程调度等方面注意如何不让这四个条件成立,对资源的分配要给予合理的规划。

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

  • 线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。
  • Java提供ThreadLocal类来支持线程局部变量(其实就是一个Map),ThreadLocal会为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,将对象的可见范围限制在同一个线程内,而不会影响其他线程所对应的副本。(这样做其实就是以空间交换时间的方式,以耗费内存为代价,但大大减少了线程同步所带来的性能消耗以及减少线程并发控制的复杂度,和synchronized相反)
//ThreadLocal类中提供的几个常用方法
 public T get() { }---获取ThreadLocal在当前线程中保存的变量副本
    public void set(T value) { }---设置当前线程中变量的副本
    public void remove() { }---移除当前线程中变量的副本
    protected T initialValue() { }---protected修饰的方法。
    ThreadLocal提供的只是一个浅拷贝,如果变量是一个引用类型,那么就要重写该函数来实现深拷贝。建议在使用ThreadLocal一开始时就重写该函数

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

18.synchronized和volatile的区别是什么?

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

19.synchronized和Lock的区别?

  • ①内置关键字:synchronized是Java内置关键字,在jvm层面,Lock是个Java类。
  • ②判断获取锁:synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  • ③释放锁方式:synchronized会自动释放锁(a线程执行完同步代码会释放锁;b线程执行过程中发生异常会释放锁),Lock需要在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  • ④线程等待:使用synchronized关键字,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一致等待下去;而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  • ⑤synchronized的锁可重入,不可中断,非公平,而Lock锁可重入,可判断,可公平(两者皆可)。
  • ⑥适用同步代码量:Lock锁适合大量代码的同步问题,synchronized适合少量代码的同步问题。

20.synchronized和ReentrantLock的区别是什么?

  • 本质区别:synchronized是java内置关键字(if,else,for,while),ReentrantLock是类(类就有各种方法,会更加灵活)。
  • ReentrantLock比synchronized的扩展性体现在以下几点上:
    • ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁。
    • ReentrantLock可以获取各种锁的信息。
    • ReentrantLock可以灵活地实现多路通知。

21.说一下atomic的原理?

Atomic包中的类基本的特性就是在多线程环境下,当多个线程同时对单个变量进行操作时,具有排他性。即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一致等待到执行成功。

  • Atomic系列的类中都会调用unsafe类中几个本地方法。unsafe类中包含了大量对C代码的操作,包括很多直接内存分配以及原子操作的调用,这里的大量方法调用都会存在安全隐患,需小心使用。
  • 如何保证原子性:自旋+CAS(乐观锁)
  • 适用场景:低并发有同步数据的情况下

22.什么是线程安全?

  • 在多线程环境下共享同一数据,线程安全的代码会通过同步机制(锁/同步代码块/同步方法)来保证每个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

23.线程安全的类有哪些?

  • StringBuffer
  • Vector(比arrayList多了线程安全)
  • statck(堆栈类,先进后出)
  • hashtable(比hashmap多了线程安全)
  • enumeration(枚举,相当于迭代器)

24.什么是原子操作?有哪些原子操作类?

  • 概念:所谓原子操作就是指不会被线程调度机制打断的一个操作或一系列的操作。
  • java原子操作类:
    • 原子更新基本类型:
      • ①AtomicBoolean:原子更新布尔类型。
      • ②AtomicInteger:原子更新整型。
      • ③AtomicLong:原子更新长整型。
    • 原子更新数组:
      • ①AtomicIntegerArray: 原子更新整型数组里的元素。
      • ②AtomicLongArray: 原子更新长整型数组里的元素。
      • ③AtomicReferenceArray: 原子更新引用类型数组里的元素。

25.volatile关键字的作用?

  • 保证了变量的可见性。被volatile关键字修饰的变量,如果值发生了改变,其他线程能够立马看见,避免了脏读的现象。

26.什么是乐观锁?什么是悲观锁?

  • 乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
  • 悲观锁: 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

27.什么是CAS?

  • CAS: 全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。

28.什么是AQS?

  • AQS: 全称为AbstractQueuedSychronizer:抽象队列同步器。

  • 如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。

  • AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。

29.怎么检测一个线程是否拥有锁?

  • java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。

30.生产者消费者模型的作用是什么?

  • ①通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率。
  • ②解耦:意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不受到对方的约束。

31.Thread类中的yieId方法有什么作用?

  • yieId方法:可以理解为"谦让"。它会使当前线程让掉CPU的执行权,使处于运行状态的线程变为就绪状态,并重新和其它同级线程竞争CPU调度权。

32.有三个线程T1,T2,T3,怎么保证它们按顺序执行?

  • 使用Thread类的join方法保证。
  • join方法的作用: 实现同步,它可以使得线程之间的并发执行变为串行执行。当调用某个线程A的join方法时,主线程会等待线程A死亡才继续执行。
  • join方法的实现原理: join方法是通过调用线程的wait方法来达到同步的目的的。例如A线程中调用了B线程的join方法,则相当于在A线程中调用了B线程的wait方法,当B线程执行完(或者到达等待时间),B线程会自动调用自身的notifyAll方法唤醒A线程,从而达到同步的目的。
class ThreadTest{
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread);
        Thread t2 = new Thread(myThread);
        Thread t3 = new Thread(myThread);
        t1.start();
        t1.join();
        t2.start();
        t2.join();
        t3.start();
            /* 输出结果  Thread-1:0
						Thread-1:1
						Thread-1:2
						Thread-1:3
						Thread-1:4
						Thread-2:0
						Thread-2:1
						Thread-2:2
						Thread-2:3
						Thread-2:4
						Thread-3:0
						Thread-3:1
						Thread-3:2
						Thread-3:3
						Thread-3:4  */
    }
}
class MyThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i <5 ; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
发布了18 篇原创文章 · 获赞 6 · 访问量 1861

猜你喜欢

转载自blog.csdn.net/weixin_43766298/article/details/104080790