当前位置: 首页 > 后端技术 > Java

面试题-多线程精要版

时间:2023-04-01 17:46:06 Java

多线程有什么用?现在我们电脑的CPU都是多核的。如果我们还是用单线程的方式来编程,那么我们电脑上的CPU核心就只有一个被使用了,其他的都是闲置的。为了提高cpu核的利用率,我们可以采用多线程编程。每个内核一次只能运行一个线程。比如你雇了4个工人,如果你只分配一个任务,那么这4个工人中只有一个工人在工作,另外三个人闲着,这是一种资源浪费。如果我们同时分配四个任务,或者更多任务,那么所有四个工人都会被利用。也提高了我们的任务效率。线程和进程有什么区别?一个线程只能属于一个进程;一个进程可以有多个线程(至少只有一个);线程的存在取决于进程。进程是系统资源分配的最小单位,线程是CPU调度的最小单位。进程拥有独立的内存单元,而多个线程共享进程的内存资源。进程编程调试简单可靠,但创建、切换、销毁成本高;线程编程正好相反。进程之间不会相互影响,一个进程宕机不会影响其他进程;但是进程中一个线程的挂起可能会导致其他线程也挂起。Java实现多线程的方式有哪些?继承Thread类实现多线程。实现Runnable接口来实现多线程。使用ExecutorServicde、Callable和Future实现带返回值的多线程。start()和run()启动线程有什么区别?只有调用了start()方法,才真正启用多线程。这时候多个线程的run()体中的代码会同时执行。但是如果你只是执行run(),那么定义的多个run()体中的代码只会在同一个线程中依次执行。线程的生命周期有哪些状态?它们之间如何流动?NEW:就是刚刚创建的线程,还没有被调用。RUNNABLE:即线程已经进入可以运行状态,或者已经在运行状态。BLOCKED:线程没有获取到锁而被阻塞。比如遇到synchronized关键字。WAITING:表示线程处于无限等待状态,调用Object.wait()、Thread.sleep()、Thread.join()方法,等待唤醒TIMED_WAITING:表示线程处于等待状态限定时间内,调用Object.wait(longmillions)、Thread.sleep(longmillions)、Thread.join(longmillions)方法TERMINATED:表示线程已经执行完毕。需要注意的是,一旦线程处于RUNNABLE状态,就不能再回到NEW状态。一旦进入TERMINATED状态,就无法返回到任何其他状态。推荐文章:https://learn.lianglianglee.c...Java多线程sleep和wait有什么区别?使用上:sleep属于Thread类的方法,wait属于Object类的方法;sleep()可以在任何地方使用,而wait只能在同步方法或代码块中使用。CUP和锁资源释放:sleep()和wait()会暂时挂起当前线程,让出CPU的执行时间。但是sleep()不会释放锁,wait()方法会释放锁资源。在异常捕获方面:sleep需要捕获或抛出异常,而wait()方法则不需要。Java中多线程同步的几种方法Synchronization方法:用synchronized关键字修饰的方法。同步代码块:用synchronized关键字修饰的代码块。同步变量:用volatile修饰的变量是可重入锁。什么是线程死锁,如何避免线程死锁?一个线程或多个线程同时被阻塞,其中一个或多个正在等待资源释放。程序无法正常终止,因为线程被无限期阻塞。Java多线程之间如何通信?多线程使用synchronized关键字实现线程间通信,而polling和volatile关键字使用wait/notify机制。线程如何获取返回结果?实现Callable接口结合ExecutorService和future获取返回结果暴力关键字?保证可见性:即一个线程对变量的修改可以立即被其他线程看到。可以禁止指令重新排序以确保原子性。Java如何保证线程的执行顺序?如何使用Thread.join()方法控制同时运行的线程只有3个?信号量(semaphore)用于控制同时访问特定资源的线程数。它协调每个线程以确保公共资源的合理使用。为什么要使用线程池?过多的线程创建和销毁会浪费大量的系统资源。这时候我们就可以使用线程池来复用创建的线程。不是创建线程,而是用完就丢。这样可以大大节省系统资源。Java中常见的线程池有哪些?newCachedThreadPool:一个可缓存的线程池。如果线程池不够用,会自动扩容。如果线程池超过了线程需要的长度,就会自动回收。newFixedThreadPool:定长线程池,指定一个定长线程池。如果执行的任务超过了线程池的长度,则进入等待队列。newScheduledThreadPool:定长线程池,支持周期性执行任务。newSingleThreadExector:单线程线程池。这个线程池只有一个线程,保证了所有的任务都是按照先进先出的顺序执行的。线程池启动线程submit()和execute()方法有什么区别?execute()没有返回值,但是性能会更好。如果需要获取线程的结果,需要调用submit()方法,也可以捕获异常。CyclicBarrier和CountDownLatch有什么区别?CyclicBarrier和CountDownLatch的类都在java.util.concurrent包下,可以表示代码运行到某个点,但是两者还是有区别的:CyclicBarrier的一个线程运行到某个点后,change线程会停止运行,直到所有的线程都运行到这个点,然后所有的线程都会重新启动;CountDownLatch则不然,一个线程运行到某个点后,只是给出某个数-1,线程继续运行。CylicBarrier只能唤起一个任务,而CountDownLatch可以唤起多个任务。CycliBarrier可以复用,CountDownLatch不能复用。如果将计数值更改为0,CountDownLatch将无法重用。什么是活锁、饥饿、无锁、死锁?死锁:多个线程占用了一个资源锁,但是没有人释放,也就没有人能够获得资源。Livelock:多个线程可以获取资源,但是获取到资源后,没有人去执行,而是互相释放。饥饿:线程优先。当高优先级的线程总能拿到资源的时候,低优先级的线程却不能。无锁:即资源上没有锁,即所有线程都可以访问和修改同一个资源,但同时只有一个线程修改成功。什么是原子性、可见性、有序性?原子性:是指一个线程的操作不能被其他线程打断,同时只能有一个线程对一个常量进行操作。可见性:当一个线程修改共享变量的值时,需要保证其他线程能够立即看到共享变量的修改值。有序:为了优化程序,提高CPU的处理能力,JVM和操作系统会对指令进行重新排序。什么是守护进程?有什么用?守护线程不同于用户线程。用户线程是我们手动创建的线程,而守护线程是程序运行时在后台提供一般服务的线程。垃圾收集线程是一个典型的守护线程。当一个用户线程停止时,它对应的守护线程也会停止。线程运行时出现异常怎么办?如果没有捕获到异常,线程将停止执行。Thread.UncaughtExceptionHandler是一个嵌入式接口,用于处理由未捕获的异常引起的线程突然终止。当未捕获的异常会导致线程终止时,JVM会使用Thread.getUncaughtExceptionHandler()查询线程的UncaughtExceptionHandler,并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。线程的yield()方法有什么用?yield()方法可以让当前正在执行的线程进入暂停状态,然后让优先级相同的线程获得执行机会。但是yeild()只能保证当前线程会放弃cpu的使用权,而不能保证其他线程一定可以使用cpu的使用权。有可能执行yeild的线程刚刚停止,下一秒又开始执行。什么是可重入锁?这意味着同一个对象可以重复获取锁。同步的用途是什么?用在类上,用在方法上,用在代码块上。Fork/Join框架有什么用?自动将大任务分成小任务,并发执行这些分布式小任务,最后合并小任务的执行结果。线程过多会导致什么异常?线程的生命周期开销很高消耗太多CPU资源降低稳定性什么是Java线程安全集合和线程不安全集合?线程安全VectorHashTableConcurrentHashMapStackthreadunsafeHashMapArrayListLinkedListHashSetTreeSetTreeMap说说HashMap的线程安全的使用HashMap是用到的。这时候方法中的局部变量就属于当前线程的变量,其他线程无论如何也访问不到,所以不存在线程安全问题。但是如果我们在单例对象中设置一个成员变量HashMap,这时候就会出现线程安全的问题。当多个线程同时访问这个单例对象的成员变量HashMap时,可能会导致HashMap值异常。什么是CAS算法,它有什么问题?CAS算法是:compareandswap(比较和交换)算法。CAS算法有3个重要的概念:内存值V和旧的期望值A会被修改得到一个新的值U。只有当旧的期望值A等于内存值V时,A才会修改为修改后的值值U,否则什么都不做。CAS主要有两个问题:ABA问题:一个线程将内存值V修改为B,然后另一个线程将内存值V修改为A。在给旧期望值A赋值的比较环节中,当将内存值V与旧的期望值A进行比较,发现内存值V没有变化或者等于A。但实际上已经被修改了。这就是ABA问题。解决办法:给变量加上版本号,每修改一次就加+1,然后赋值前进行比较,加上版本的比较。如果发现版本号不一致,则不会给出赋值操作。太长的循环时间会导致过多的开销。CAS算法需要不断地自旋来读取最新的内存值,长时间不能读取会造成不必要的CPU开销。如何检查线程是否拥有锁?使用java.lang.Thread类下的静态holdsLock()方法,如果当前线程持有对象的锁,则返回true。如何解决多线程死锁问题?使用java自带的jstack命令查看Useps-ef|grepjava命令查找我们对应项目的java进程,记录其进程pid号使用top-H-ppidnumber查找我们项目java进程中cpu使用率最高的线程,记录其线程pid号并转换为线程pid刚获取到16个进程号,然后执行jstackpid|grep'threadpidhexadecimalnumber'找到对应的栈错误信息,然后只分析错误信息。线程同步需要注意什么?尽量缩小同步范围,提高系统吞吐量防止死锁,注意加锁的顺序。使用线程wait()方法的前提条件是什么?在同步块中使用。使用Fork/Join框架需要注意什么?如果任务被深度反汇编,系统中线程数据堆积过多,会严重降低系统性能。如果函数调用栈很深,会造成栈内存溢出。保证“可见性”的方法有哪些?viotatile:当一个共享变量被volatile修改时,会保证修改后的值会立即更新到主存中,当其他线程需要读取时,会去内存中读取新的值。synchronized和Lock:另外,通过synchronized和Lock也可以保证可见性。synchronized和Lock可以保证同一时刻只有一个线程获取到锁然后执行同步代码,在释放锁之前将变量的修改刷新到主存中。所以能见度是有保证的。Lock接口与synchronized关键字实现的区别:synchronized关键字是基于JVM层面实现的,JVM控制锁的获取和释放。Lock接口基于JDK级别,手动获取和释放锁;use:synchronized关键字不需要手动释放锁,Lock接口需要手动释放锁,在finally模块中调用unlock方法;锁获取超时机制:不支持synchronized关键字,Lock接口支持;锁获取中断机制:synchronized关键字不支持,Lock接口支持;锁释放条件:synchronized关键字满足持有锁的线程执行完成,或者持有锁的线程异常退出,或者持有锁的线程进入等待状态就会释放锁。Lock接口调用unlock方法释放锁;公平性:synchronized关键字是一种非公平锁。Lock接口可以通过输入参数设置锁的公平性。你能谈谈ThreadLocal吗?ThreadLocal也称为线程局部变量,主要解决多线程并发导致的数据不一致问题。ThreadLocal为每个线程提供了一个变量副本,避免了多个线程同时访问同一个变量的问题。这样做会增加内存占用,但会降低多线程并发控制的难度。谈谈读写锁?ReadWriteLock是读写锁的父接口,ReentrantReadWriteLocak是其接口的具体实现类,实现了读写锁的分离。读锁是共享的,而写锁是独占的。多个线程同时进行读操作不会有任何影响,但是当多个线程同时进行写操作时,就会被锁定,只允许一个线程同时对资源进行操作。什么是FutureTask?FutureTask是一个异步计算任务。可以将Callable的具体实现类传入FutureTask,可以进行等待这个异步计算任务的结果、判断是否完成、取消任务等操作。不可变对象如何帮助多线程?不可变对象保证了对象的内存可见性,读取不可变对象不需要额外的同步手段,提高了代码执行效率。多线程上下文切换是什么意思?多线程上下文切换是指将CPU的控制权从一个已经运行的线程切换到另一个准备好等待获取CPU执行权的线程的过程。Java中使用什么线程调度算法?先发制人。一个线程用完cpu后,操作系统会根据线程优先级、线程饥饿等数据计算出一个总优先级,将下一个时间片分配给某个线程执行。Thread.sleep(0)是做什么的?Thread.sleep(0)可以手动触发分配一个cpu时间片。由于java采用的抢占式线程调度算法,可能会出现这样的情况,一个优先级高的线程往往能拿到cpu的执行权,而某些优先级低的线程可能根本就拿不到cpu的执行权。为了让那些优先级低的线程至少执行一次,我们可以通过调用Thread.sleep(0)来平衡cpu时间片的分配。什么是Java内存模型,哪些区域是线程共享的,哪些不是?线程共享的只有两个:堆和方法区。Stack,程序计数栈Programmer:用来保存当前线程执行的字节码位置Stack:每个方法创建的时候,也会创建一个栈,用来存放方法的一些信息,比如局部变量,方法的入口和出口等。Nativemethodstack:和stack类似,只是存放的是调用nativemethods的信息。什么是乐观锁和悲观锁?乐观锁和悲观锁是解决并发场景下数据竞争问题的两种思路。乐观锁:乐观锁在操作数据时非常乐观,认为其他人不会同时修改数据。因此,乐观锁不会加锁,只是在更新时判断别人是否修改了数据:如果别人修改了数据,则放弃操作,否则执行操作。悲观锁:悲观锁在操作数据时比较悲观,认为其他人会同时修改数据。所以在操作数据的时候直接加锁,直到操作完成才会释放锁;在锁定期间,其他人不能修改数据。除了CAS,版本号机制也可以用来实现乐观锁。版本号机制的基本思想是在数据中加入一个字段version,表示数据的版本号。每当修改数据时,版本号加1。当一个线程查询数据时,一起检查数据的版本号;线程更新数据时,判断当前版本号与之前读取的版本号是否一致,一致才进行操作。常用的方法包括分布式锁和同步关键字。首先,乐观锁和悲观锁只是两种思想,目的是为了解决并发场景下的数据竞争问题。乐观锁乐观锁和悲观锁是两个概念,主要用来解决并发场景下数据不一致的问题。乐观锁:乐观锁在操作数据的时候很乐观,倾向于认为多个线程不会同时修改一条数据。因此,乐观锁不会加锁,只是在更新时判断别人是否修改了数据:如果别人修改了数据,则放弃操作,否则执行操作。悲观锁的实现就是加锁。加锁可以是锁定代码块(如Java中使用synchronized关键字),也可以是锁定数据(如mysql中的排它锁)。悲观锁:悲观锁在操作数据时是悲观的,倾向于认为很可能多个线程同时修改一条数据。所以在操作数据的时候直接加锁,直到操作完成才会释放锁;在锁定期间,其他人不能修改数据。乐观锁的实现方式主要有两种:CAS机制和版本号机制。CAS机制:比较交换,内存值A,期望旧值U,期望修改后的新值B,只有当内存值A和U相同时,才会执行修改到B的操作。但是会出现ABA的问题,就是先把A的值改成B,最后改成A。说明上面的值没变,但是修改了实际的值。版本号机制:增加版本号字段,每次修改+1,只有版本号与修改前相同,才会执行操作。同步方法和同步块哪个更好?synchronizedblock,意思是在synchronized块之外得到的代码是异步执行的,比同步整个方法更能提高代码的效率。同步范围越小越好。什么是自旋锁?如果一个线程获得了锁,发现锁已经被其他线程占用了。那么这个线程就会采用循环等待的方式,等待其他线程释放锁资源。在循环等待的过程中,会不断判断是否已经成功获取到锁,获取到锁后立即退出循环。哪个更好,Runnable还是Thread?实现Runnable接口优于继承Thread类:它可以避免Java单继承带来的局限性。可以实现业务执行逻辑和数据资源的分离。它可以与线程池结合使用来管理线程的生命周期。Java中的notify和notifyAll有什么区别?notify方法是在对象的等待池中随机唤醒一个线程,进入锁池;notifyAll()唤醒对象等待池中的所有线程,进入锁池。为什么wait/notify/notifyAll/这些方法不在线程类中?Java提供的锁是对象级的,不是线程级的。每个对象都有一个锁,由一个线程获取。如果线程需要等待一些锁,调用对象中的wait()方法是有意义的。如果在线程类中定义了wait()方法,线程在等待哪个锁是不明显的。对于简单的锁,由于wait、notify、notifyAll都是锁级别的操作,所以定义在Object类中,因为锁属于对象。为什么在同步块中调用wait和notify方法?主要是因为javaapi要求这样做,如果您不这样做,您的代码将抛出IlegalMonitorStateException。另一个原因是为了避免等待和通知之间的竞争条件。为什么要在循环中检查等待条件?处于等待状态的线程可能会收到错误的警报和虚假的唤醒,如果在循环中不检查等待条件,则程序将在不满足结束条件的情况下退出。因此,当一个等待线程被唤醒时,不能认为其原来的等待状态仍然有效,可能会在调用notify()方法后到等待线程被唤醒前的短时间内发生变化。这就是为什么最好在循环中使用wait()方法。可以在eclipse中创建模板调用wait和notify试试看。Java中的堆和栈有什么区别?作用不同:栈内存用于存放局部变量和方法调用。堆内存在Java中用来存放对象,无论是成员变量、局部变量,还是类变量,它们指向的对象都存放在堆内存中。共享是不同的:堆栈内存是线程私有的。堆内存由所有线程共享。异常错误则不同:栈空间不足时抛出异常:java.lang.StackOverFlowError。堆空间不足时抛出异常:java.lang.OutOfMemoryError。空间大小:栈的空间大小远小于堆。什么是阻塞方法?阻塞方法是指在方法结果返回之前,当前方法的线程会被挂起,等待方法结果的返回,期间不做其他事情。比如ServerSocket类的accept()方法就是一个阻塞方法,会一直等待客户端的连接响应。除了阻塞方法,还有非阻塞方法,即异步方法。