常见面试题
多线程常见的面试题_上山打卤面的博客-CSDN博客_多线程面试题
什么是线程和进程? 线程与进程的关系,区别及优缺点?
总结:进程是程序运行的基本单位,线程是资源分配的最小单位,但是上面的还是比较的抽象,使用图片进行解释:下面是一个Java进程 一个进程中有两个线程。多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。**线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反
说说并发与并行的区别?
并发与并行的区别_詹詹自喜KING的博客-CSDN博客_并发和并行的区别
并发是指一个处理器同时处理多个任务。 并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。 并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。 来个比喻:并发是一个人同时吃三个馒头,而并行是三个人同时吃三个馒头。
线程的生命周期?线程有几种状态?
线程一共有六种状态。就是六种, 1.NEW 2.RUNNABLE 3.BLOCKED 4.WAITING 5.TIMED_WAITING 6.TERMINATED 下面分别说明下各种状态情况 1.NEW 线程创建完但未调用 start方法 2.RUNNABLE 可细分两种情况 1. 线程正在Java虚拟机中执行 2. 等待操作系统分配资源(例如CPU时间片) 3.BLOCKED 可细分两种情况: 1. 准备进入synchronized修饰的代码块或方法,等待对象监视器锁 2. 已进入synchronized修饰的代码块或方法中并调用了Object.wait()方法 4.WAITING 有由一下三种情况触发 1. Object.wait() 没有设置timeout(区别于 Object.wait(long timeout)) 2. thread.join() 没有设置timeout (区别于 thread.join(long timeout)) 3. LockSupport.park 5.TIMED_WAITING 线程指定了定时等待状态。有以下五种情况: 1. 调用Thread.sleep 2. Object.wait(long timeout) (这里区别于Object.wait(),有超时时间) 3. thread.join(long) (这里区别于Thread.join(),有超时时间) 4. LockSupport.parkNanos 5. LockSupport.parkUntil 6.TERMINATED 线程执行完成。
线程停止的常规方法
-
使用退出标志,使线程正常退出
public class FlagStop implements Runnable { private int ticket = 20; private volatile boolean flag = true; @Override public void run() { while (flag) { ticket--; System.out.println(Thread.currentThread().getName() + ": flage is " + flag + ", 卖了一张票,剩余票:" + ticket); if (ticket == 0) { flag = false; System.out.println(Thread.currentThread().getName() + ": flage is " + flag + ", 卖了一张票,剩余票:" + ticket); } } } public static void main(String[] args) { FlagStop flagStop = new FlagStop(); Thread thread = new Thread(flagStop); thread.setName("flag-stop"); thread.start(); } } 运行结果: flag-stop: flage is true, 卖了一张票,剩余票:19 flag-stop: flage is true, 卖了一张票,剩余票:18 flag-stop: flage is true, 卖了一张票,剩余票:17 flag-stop: flage is true, 卖了一张票,剩余票:16 flag-stop: flage is true, 卖了一张票,剩余票:15 flag-stop: flage is true, 卖了一张票,剩余票:14 flag-stop: flage is true, 卖了一张票,剩余票:13 flag-stop: flage is true, 卖了一张票,剩余票:12 flag-stop: flage is true, 卖了一张票,剩余票:11 flag-stop: flage is true, 卖了一张票,剩余票:10 flag-stop: flage is true, 卖了一张票,剩余票:9 flag-stop: flage is true, 卖了一张票,剩余票:8 flag-stop: flage is true, 卖了一张票,剩余票:7 flag-stop: flage is true, 卖了一张票,剩余票:6 flag-stop: flage is true, 卖了一张票,剩余票:5 flag-stop: flage is true, 卖了一张票,剩余票:4 flag-stop: flage is true, 卖了一张票,剩余票:3 flag-stop: flage is true, 卖了一张票,剩余票:2 flag-stop: flage is true, 卖了一张票,剩余票:1 flag-stop: flage is true, 卖了一张票,剩余票:0 flag-stop: flag is false, 票卖完了,剩余票0张
-
使用interrupt方法终止线程
public class InterruptStop implements Runnable { @Override public void run() { for (int i = 0; i < 50000; i++) { System.out.println(i); if (Thread.currentThread().isInterrupted()) { System.out.println("I will quit~"); break; } } } public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new InterruptStop()); thread.start(); thread.sleep(1000); thread.interrupt(); } } 运行结果: 0 ...... 40562 I will quit~
-
使用stop方法强行终止线程,是过期作废的方法,但不推荐(加分项)
问:为何废弃java线程的stop()方法?stop方法有何隐患?
不推荐使用,暴力终止,可能使一些清理性的工作得不到完成。还可能对锁定的内容进行解锁,容易造成数据不同步的问题。
sleep()、wait()、join()、yield()的区别
在java中,每个对象都有两个池,锁(monitor)池和等待池
锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中
等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池
sleep和wait的区别:
-
sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
-
sleep不会释放锁,而wait会释放锁,而且会加入到等待队列中
-
sleep方法不需要依赖于同步器synchronized,但是wait需要依赖synchronized关键字
-
sleep不需要被唤醒,休眠之后自动退出阻塞,而wait不指定时间需要被别人中断,notify或者notifyAll唤醒
-
sleep一般用于当前线程休眠,而wait多用于多线程之间的通信
yield:让正在执行的线程进入就绪状态,马上释放了CPU的执行权,但是依然保留了CPU的执行资格,所以有可能CPU下次进行线程调度还是会让这个线程获取到执行权
join:执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程
package threadTest.joinTest; /** * @ClassName JoinTest * @Description TODO * @Author QiuYiping * @Date 2022/4/11 16:56 */ public class JoinTest { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("休眠后run方法执行内容"); }); t1.start(); t1.join(); System.out.println("等join()内容结束后最后执行main方法内容"); } } 运行结果: //休眠后run方法执行内容 //等join()内容结束后最后执行main方法内容
说说你对线程安全的理解
线程安全指的是内存安全,堆是共享内存,可以被所有线程访问。
当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的
堆:是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏
堆是java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所存在的区域唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
栈:是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里显式的分配和释放
目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是有操作系统保障的
在每个进程的内存空间中都会有一个特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因
说说你对守护线程的理解
守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆
守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;只要其他线程结束,没有执行了,程序就结束了,守护线程也就中断了
注意:守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;
守护线程的作用?
举例,GC垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,也就不需要垃圾回收器,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源
应用场景:
为其它线程提供服务支持 在任何情况下,程序结束时,这个线程必须正常且立刻关闭;反之如果一个正在执行某个操作的线程必须要正确的关闭否则就会出现异常的情况,就不能使用守护线程,而是使用用户线程(如数据库录入或者更新,这些操作都是不能中断的)。 设置守护线程:thread.setDaemon(true)必须在thread.start()之前设置,否者会报异常(IllegalThreadStateException),不能把正在运行的常规线程设置为守护线程
在守护线程中产生的新线程也是守护线程。守护线程不能用于访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间出现中断
java中自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线程就不能用java的线程池
ThreadLocal的原理和使用场景
ThreadLocal原理及使用场景_小猫的秋刀鱼的博客-CSDN博客_threadlocal使用场景和原理
一句话说就是 ThreadLocal
适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用(相同线程数据共享),也就是变量在线程间隔离(不同的线程数据隔离)而在方法或类间共享的场景。
ThreadLocal为什么可能产生内存泄漏,如何避免?
通过之前的分析已经知道,当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。
由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生 命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引 用链的关系一直存在:Thread --> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收, Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。
既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法
子线程如何共享主线程的ThreadLocal变量?
ThreadLocal在做线程级的数据隔离时非常好用,但是有时候我们会想如何让子线程获取到父线程的ThreadLocal,其实在线程中除了ThreadLocal外还有InheritableThreadLocal,顾名思义,可继承的线程变量表,可以让子线程获取到父线程中ThreadLocal的值
并发的三大特性
-
原子性:一个或者多个操作,要么全部执行(执行的过程是不会被打断的)、要么全部不执行
从i++谈起
这三步骤需要合在一起要保证原子性 1.将i从主存读到工作内存的副本中 2.+1的运算 3.将结果写入工作内存中 这一步要保证可见性 4.将工作内存中的值刷回主存(什么时候刷入由操作系统决定,不确定的)
-
有序性:即程序的执行顺序按照代码的先后顺序执行。(在java内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响单线程程序的执行,但是会影响多线程并发执行的正确性)
int a = 0 boolean flag = false; public void write(){ a = 2; //1 flag = true; //2 步骤1和2可能会交换顺序 } public void multiply(){ if(flag){ int ret = a * a; } }
由于write方法里的1和2做了重排序,导致最后的ret可能为4或者0
-
可见性:可见性是指多个线程访问同一个变量的时候,一个线程修改了这个变量的值,其他线程也可以立刻看到这个修改后的值。Java提供关键字volatile关键字来保证可见性(当一个共享变量被volatile修饰后,它会保证修改的值立即更新到主存中,其他线程如果需要用到这个变量,会到主存中去取,这样保证每个线程读取到的数据都是最新的)。Synchronized和Lock也能保证可见性,
关键字:volatile(可见性和有序性)、synchronized(三大特性都可以)
为什么用线程池?解释下线程池的参数
它的主要特点为:线程复用;控制最大并发数;管理线程。
1.降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 2.提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行。 3.提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分 配,调优和监控。
线程池常见参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize:线程池中常驻核心线程数
maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于1
keepAliveTime:多余空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余空闲线程会被销毁直到剩下corePoolSize为止。
unit:keepAliveTime的单位
workQueue:里面放了被提交但是尚未执行的任务
threadFactory:表示线程池中工作线程的线程工厂,用于创建线程
handler:拒绝策略,当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时,对任务的拒绝方式
当有任务过来时,采用线程工厂创建核心线程数,然后有任务过来就加入等待队列,然后等待的任务太多了,就扩展到最大线程数,然后最大线程数目满了,还处理不了数据,就有一个拒绝策略。最后如果请求数量降下来了,存活时间超过那个规定的时间单位,就销毁这些空闲线程
线程池中阻塞队列的作用?为什么要先添加队列而不是先创建最大线程?
1)一般的对列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前当前的任务了,阻塞队列可以通过阻塞保留当前想要继续入队的任务
2)阻塞队列中可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程获取wait状态,释放CPU资源。
3)阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列take方法挂起,从而保持核心线程的存活,不至于一直占用CPU资源
为什么是添加队列而不是先创建最大线程
在创建新线程的时候,是要获取全局锁的,这个时候其他的就得阻塞,影响了整体效率。
线程池中线程复用的原理
线程池可以把线程和任务进行解耦,线程归线程,任务归任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。在线程池中,同一个线程可以从 BlockingQueue 中不断提取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中,不停地检查是否还有任务等待被执行,如果有则直接去执行这个任务,也就是调用任务的 run 方法,把 run 方法当作和普通方法一样的地位去调用,相当于把每个任务的 run() 方法串联了起来,所以线程数量并不增加