一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是面试题,也是你 Java 知识点的扫盲贴。
回答在Java中我们可以通过如下几种方式来判断线程池中的任务是否已全部执行完成。使用isTerminated()方法使用ThreadPoolExecutor的getCompletedTaskCount()使用CountDownLatch扩展使用isTerminated()方法isTerminated()用来判断线程池是否结束,如果结束返回true。使用它时我们必须要在shutdown()方法关闭线程池之后才能使用,否则isTerminated()永不为true。shutdown():拒绝接受新任务,但会继续处理已提交的任务。shutdownNow():尝试停止所有正在执行的任务并返回未执行的任务列表。如下:publicclassThreadPoolExample{publicstaticvoidmain(String[]args)throwsInterruptedException{Thr
回答因为在try-catch中是无法捕获到子线程抛出的异常。主要原因是因为,每个线程都有自己独立的堆栈空间和执行上下文。由于它是独立的,所以当一个线程抛出异常后,这个异常只会沿着该线程的调用栈向上传播,它不会自动传播到启动它的主线程上去,所以也就不会被主线程的try-catch捕获。那子线程抛了异常要怎么处理呢?目前主流的处理方法有四个:子线程自己捕获异常,自己处理。这是一种最直接的方式。使用Thread.UncaughtExceptionHandler:UncaughtExceptionHandler是Thread的一个内部函数式接口,它用于处理由于线程抛出的未捕获异常而导致线程突然终止的情况。当线程中的方法由于未捕获的异常而终止时,JVM会检查该线程是否有其自己的UncaughtExceptionHandler,如果有,则执行。使用Future和ExecutorService。使用af
回答run()run()是Thread的一个实例方法,它实际上是线程执行的入口,但是它本身不会启动一个新的线程,它仅仅只是一个普通的方法调用。publicclassThreadTest{publicstaticvoidmain(String[]args){MyRunnablerunnable=newMyRunnable();runnable.run();}staticclassMyRunnableimplementsRunnable{@Overridepublicvoidrun(){System.out.println("Runningin"+Thread.currentThread().getName());}}}执行结果:Runninginmain所以,在这里run()仅仅只是一个普通方法的调用,并不会创建新的线程,方法在main里面执行。start()与run()
回答yield()是Thread中的一个静态方法,它的作用是让当前正在执行的线程主动让出CPU使用权,但并不将线程从可运行状态(Runnable)变为阻塞状态(Blocked),也不放弃锁。换句话说,调用yield()的线程仍然是可运行的(Runnable),它只是告诉线程调度器,当前线程愿意让出CPU时间片,供其他同样处于可运行状态的线程使用。扩展通常情况下,yield()的使用场景比较少,使用它时我们需要注意两个问题:不可预测性:yield()仅仅只是让出CPU时间片,使用它理论上可以增加线程切换的机会,但是也只是理论上的可能,因为线程调度器并不会立刻进行切换,而且它可能会忽略这个提示,继续执行当前线程。优先级的影响:高优先级的线程调用yield()后,大概率仍会被线程调度器再次执行,而低优先级的线程调用yield()后,则更有可能让出CPU给其他同优先级或更高优先级的线程。当一个线程
回答死锁死锁是指两个或两个以上的线程相互等待对方释放已经持有的锁,从而导致所有涉及的线程都无法继续执行的情况。死锁的发生需要满足四个条件:互斥:线程对资源的访问是互斥的,即一次只能一个线程访问资源。占有并等待:线程已经占有至少一个资源,并且在等待其他线程占有的资源。不可剥夺:资源不能被强制性地从一个线程中剥夺,只能由占有它的线程释放。循环等待:存在一个线程集{T1,T2,...,Tn},其中T1等待T2占有的资源,T2等待T3占有的资源,直到Tn等待T1占有的资源,形成一个循环等待链。活锁活锁是指两个或两个以上的线程不断地相互让出资源,但是都无法继续执行具体的工作。活锁与死锁不同的地方就在于:线程没有被阻塞,但因为每次尝试都被其他线程影响而无法向前推进。锁饥饿锁饥饿是指线程长期无法获得所需的资源,导致它无法正常执行业务。由于其他线程不断地获得资源,导致该线程无法继续执行。锁饥饿通常是发生在
回答notify()用于唤醒在该对象的监视器上等待的单个线程。如果有多个线程在该对象上等待,notify()会随机选择其中一个线程进行唤醒。notifyAll()用于唤醒在该对象的监视器上等待的所有线程。所有被唤醒的线程将重新获取对象的锁,但是只有其中一个线程能获取成功,其他线程则会继续等待直到再次获取锁。扩展锁池和等待池锁池(EntrySet):指那些试图获取某个对象的锁,当又没有获取到锁的线程的集合。当一个线程尝试获取对象锁时,如果该对象锁已经被其他线程持有,那么该线程就会进入锁池,等待获取锁的机会。等待池(WaitSet):指那些调用了wait()方法,并且当前正在等待被唤醒的线程的集合。当一个线程调用某个对象的wait()方法,它会释放该对象的锁,并进入这个对象的等待池,直到以下三种情况之一发生:该线程被唤醒(通过调用notify()或notifyAll())。该线程被中断。指定的
回答在Java中我们可以通过Thread类的isAlive()来判断线程是否存活。扩展isAlive()返回一个Boolean,true表示线程还在执行,false表示线程已经终止。@TestpublicvoidisAliveTest(){Threadthread=newThread(()->{for(inti=0;i<5;i++){log.warn("{}-正在执行-[{}]...",Thread.currentThread().getName(),i);try{Thread.sleep(1000L);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}});thread.start();while(thread.isAlive()){log.info("{}-线程是存活的,is
回答LongAdder和AtomicLong都是用于确保在多线程环境下对Long类型变量进行原子性操作,而不会出现线程安全问题。虽然两者功能相似,但是还是存在一些差异。一、实现方式AtomicLong是基于CAS方式自旋更新,使用单个变量来存储计数值,每次更新操作都是对这个单一变量进行CAS操作。LongAdder是基于分段计数,将值分散到多个变量(称之为Cell),每个线程会选择不同的Cell来更新,减少了对单个变量的竞争。在最终需要获取总计数值时,将所有Cell的值累加起来返回。二、性能AtomicLong由于使用的CAS自旋方式,如果在高并发环境下,则容易导致CAS操作失败,需要不断自旋重试,从而导致性能下降。所以,它适用于并发度较低的场景。LongAdder由于分散了竞争,通过多个Cell进行更新操作,减少了CAS冲突,性能更优。所以,它比较适用于并发度较高的场景,但是,由于Lon
回答Java的原子操作类是位于java.util.concurrent.atomic包中的类,他们主要用于确保在多线程环境下对某些变量的操作是原子操作。在多线程环境下,对单个变量进行操作,会出现多线程安全问题,导致数据不一致,为了避免这种情况发生,我们可以使用同步机制,比如synchronized或者Lock,但是他们成本太高了,而原子操作类则提供了一种高效的方法来实现线程安全操作。Java提供了12个原子操作类,共分为四大类。一、原子更新基本类AtomicBoolean:原子更新布尔值。AtomicInteger:原子更新整数值。AtomicLong:原子更新长整数值。二、原子更新数组AtomicIntegerArray:原子更新整数数组中的元素。AtomicLongArray:原子更新长整数数组中的元素。AtomicReferenceArray:原子更新引用类型数组中的元素。三、原子更
回答Unsafe是Java中一个非常底层的类,它位于sun.misc,它主要用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等。Unsafe中的方法大多数都是native方法,这些native方法都是操作系统级别的方法:执行内存操作:我们可以利用Unsafe直接操作内存,比如分配内存(allocateMemory)、释放内存(freeMemory)、设置内存(setMemory)、获取内存值(getInt、getLong等)。CAS操作:调用操作系统的CAS指令,实现CAS的功能。对象操作:利用Unsafe,我们可以突破Java语法本身的限制,直接从内存级别去操作Java堆中的对象。包括不通过构造函数可以直接创建对象,可以直接修改对象的字段值。线程控制:通过Unsafe,可以在操作系统层次将线程挂起和恢复。Unsafe虽然功能强大,但是它非常危险,因为它绕过了Ja
回答默认情况下,线程池的核心线程是不会被回收的,即使它们处于空闲状态。但是ThreadPoolExecutor还是提供了一个参数来控制这个行为,通过allowCoreThreadTimeOut(true)设置后,核心线程在空闲超过keepAliveTime时就会被回收。我们直接看源码,直接看ThreadPoolExecutor中runWorker():finalvoidrunWorker(Workerw){Threadwt=Thread.currentThread();Runnabletask=w.firstTask;w.firstTask=null;w.unlock();//allowinterruptsbooleancompletedAbruptly=true;try{while(task!=null||(task=getTask())!=null){//不断从任务队列中拿任务执行,直
回答ThreadPoolExecutor对核心参数提供了一些setter方法,根据这些setter方法我们可以在程序运行时动态调整线程池的核心参数:publicvoidsetCorePoolSize(intcorePoolSize);publicvoidsetMaximumPoolSize(intmaximumPoolSize);publicvoidsetKeepAliveTime(longtime,TimeUnitunit);publicvoidsetThreadFactory(ThreadFactorythreadFactory);publicvoidsetRejectedExecutionHandler(RejectedExecutionHandlerhandler);在实际生产场景下,我们需要定期检查线程池中的任务队列长度、活动线程数和核心线程数等指标。然后根据监控结果,调用对应的
回答线程池的拒绝策略是任务在提交给线程池时,线程池无法接受新任务的情况下执行的处理策略。目前线程池内置了四种拒绝策略。一、AbortPolicy默认的拒绝策略。抛出异常,中止任务。如果任务无法提交到线程池时会抛出RejectedExecutionException,我们需要处理好该异常,否则会影响后续任务的执行。二、CallerRunsPolicy该策略不会丢弃任务,只要线程池没有关闭的话,则使用提交任务的线程来执行这个任务。它一般适用于并发量比较小,对任务执行时间要求不严格的场景。但是,由于是调用者自身执行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大。三、DiscardPolicy直接丢弃,不做任何处理,但是不会抛出异常。该策略适用于可以容忍任务丢失的场景。不会对提交任务的线程产生任何影响。四、DiscardOldestPolicy丢弃任务队列中最旧的未处理任务
回答Executors是Java并发包里面提供的一个用于创建Java线程池的工具类,它提供了一个些工厂方法用于创建常见的线程池,给我们带来了一定程度上的遍历,但是为什么不推荐时间呢?主要有两个原因:一、任务队列没有设置固定容量大小newFixedThreadPool()和newSingleThreadExecutor(),他们在创建线程池时使用的是无界队列LinkedBlockingQueue。这就意味着当所有线程都在处理任务时,新来的任务会不断地加入到队列中,由于是无界的,所以可以无限制的添加直到系统内存耗尽,从而OOM。二、最大线程数量是Integer.MAX_VALUEnewCachedThreadPool()使用的一个没有容量的任务队列,提交的任务全部直接交给线程执行,如果没有空闲的线程,则会创建新的线程。所以,如果系统的任务数量非常多,线程池就会创建大量的线程池,再加上,它的最大
回答从公平的角度来看,Java的锁分为两类:公平锁和非公平锁。公平锁公平锁是指多个线程在尝试获取同一个锁时,锁会按照线程请求的顺序来分配。也就是说,谁先请求锁,谁就先获取锁。公平锁的底层实现依赖于一个FIFO队列,当一个线程请求获取锁时,如果锁被其他线程占用,则该线程会被加入到等待队列的末尾。当锁被释放时,锁会从队列取出第一个等待的线程并将锁分配给它。公平锁的优点在于它避免了线程饥饿问题,因为所有线程都有同等的获取锁的权利。但是,它性能相比于非公平锁是低的,因为它每次获取锁都需要维护一个队列,增加了额外的开销。非公平锁非公平锁是指多个线程在尝试获取同一个锁时,锁的分配不考虑请求顺序,谁抢到就是谁的。也就是说,任何一个请求锁的线程都有机会获取锁,哪怕它刚刚发起获取锁请求。飞公平锁在尝试获取锁的时候,它是不需要考虑排队情况的,而是采取直接获取锁的方式,内部没有维护一个FIFO队列,减少了锁的开
回答Java提供了多种解决线程安全问题的方法,主要分为几类:同步机制无锁机制线程局部变量同步方式一、synchronizedsynchronized是一个用于实现线程同步,确保多线程环境下对共享资源安全访问的关键字。它可以修饰方法或代码块,保证同一时刻只有一个线程可以执行同步方法或代码块内的代码。publicvoidincrement(){synchronized(this){count++;}}关于synchronized的核心原理请阅读这几篇文章:synchronized的实现原理是怎样的?synchronized锁的是什么?synchronized的锁升级过程是怎样的?synchronized的锁优化是怎样的?二、ReentrantLock相比于synchronized,ReentrantLock提供了更加轻量级和灵活的同步机制。privatefinalReentrantLocklo
回答要想保证T1、T2、T3顺序执行,有多种方法。我们先新建一个Tread:classMyThreadextendsThread{privateStringthreadName;publicMyThread(Stringname){this.threadName=name;}@Overridepublicvoidrun(){System.out.println(threadName+"isrunning");}}一、使用join()join()是Java提供的一种用于线程同步的机制,它能够让一个线程等待另一个线程完成后再继续执行。它有三个重载方法:join():无限等待,直到目标线程执行完毕。join(longmillis):等待目标线程执行完毕,或者达到指定的毫秒时间。join(longmillis,intnanos):等待目标线程执行完毕,或者达到指定的时间(以毫秒
回答ThreadPoolExecutor使用corePoolSize和maximumPoolSize控制核心线程数和最大线程数。初始时,线程池会创建核心线程数(corePoolSize)的线程来处理任务。当任务被提交到线程池后,线程池会首先尝试使用空闲的核心线程执行任务,如果没有空闲的核心线程,则任务会被放入任务队列(workQueue)。工作线程会从任务队列中获取任务执行,从而避免频繁创建和销毁线程。那线程是如何实现复用的呢?在ThreadPoolExecutor#runWorker()方法中,工作线程会不断地从任务队列(workQueue)中获取任务,当一个线程执行完任务后,线程并不会直接退出,而是采用死循环的方式不断从任务队列中获取新的任务执行,只要任务队列中有任务,那么该线程就会一直执行下去。同时,调用它的方式并不是通过Thread#start()的方式,而是直接调用它的run()
回答死锁指的是两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果没有外力的帮助下,它们都将无法推进下去。简单的说就是两个或以上的进程都在互相等待对方释放资源,但是它们谁都不释放进程,系统就卡住了。解决死锁的方法主要有以下几种:以确定的顺序获得锁:在获取锁的顺序上进行控制,确保所有线程获取锁的顺序是一致的,这样就可以预防死锁的产生。超时放弃:在获取锁的时候设置超时时间,如果在指定时间内未获取锁,我们可以做其他操作,避免死锁。比如使用ReentrantLock的tryLock()。监控和检测:使用一些死锁检测工具来检测线程是否发生了死锁,如果发现死锁,我们可以采取一些必要的措施,比如中断其中一个线程。避免嵌套锁:尽量避免一个线程同时持有多把锁扩展死锁演示产生死锁有四个必要条件:互斥条件:一个资源每次只能被一个进程使用。占有并等待条件:一个进程因请求资源而阻塞时,对已获
回答在我们调用ConcurrentHashMap的put()时,ConcurrentHashMap会校验key和value是否为null,如果为null就抛出NullPointerException,如下:为什么要这么设计呢?大明哥认为主要有如何几个原因。一、避免多线程下的二义性null是一个特殊的值,表示没有对象或者引用。在ConcurrentHashMap中如果key或者value为null,当我们通过get(key)获取对应的value的时候,如果返回的结果是null,我们是没有办法判断的,它是put(k,v)的时候,value本身为null值,还是这个key本身就不存在呢?在并发编程中,一致性和可预测性非常重要。如果一个线程试图通过get(key)获取key的值时,而这个key尚未被任何线程写入,那它应该返回什么呢?如果返回null,那么这个返回值是表示key不存在,还是key的值
回答线程安全的本质是内存安全,在Java中我们绝大多数的数据都是存储在Java堆中的,而Java堆是所有线程共享的。当多个线程访问同一个对象时,如果能够获得预期的结果,那么我们就会说这个对象线程安全,反之线程不安全。一个对象是否线程安全需要考虑两个问题:共享资源首先我们的资源需要是共享的,例如线程独享的局部变量我们是无需考虑其线程安全问题的。多个线程访问和修改同一块资源,可能会导致资源状态的不一致。竞争条件如果多个线程同时访问共享资源,并试图同时修改它,就会产生竞争条件。同时,线程安全还问题还涉及三个核心问题:原子性原子性指的是一个或多个操作在CPU执行的过程中是不可分割的、执行过程中不可被中断的一整套操作。如果一个操作是原子的,那么这些操作要么全部成功执行并完成,要么全部不执行,不会出现中间状态。比如下面这个操作就不是原子性的:i++因为它包含了三个步骤:读取i的值。增加i的值。将新值写
CAS,CompareAndSwap,即比较并交换,它是一种无锁编程技术的核心机制。但是你确定CAS是不加锁的吗?我们先看AtomicInteger的getAndIncrement()源码:publicfinalintgetAndIncrement(){returnunsafe.getAndAddInt(this,valueOffset,1);}publicfinalintgetAndAddInt(Objectvar1,longvar2,intvar4){intvar5;do{var5=this.getIntVolatile(var1,var2);}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));returnvar5;}publicfinalnativebooleancompareAndSwapInt(Objectvar1,
回答CAS,CompareAndSwap,即比较并交换,它一种无锁编程技术的核心机制。其工作方式分为两步:比较:它首先会比较内存中的某个值(V)与预期的值(A)是否相等。**交换:**如果相等,那么会自动将该值(V)更新为新值(B)。如果不相等,不做任何操作。这个过程是原子操作,保证在并发环境中的数据一致性和线程安全。CAS主要存在如下三个问题:ABA问题:如果变量V的值原先是A,然后被其他线程改为B,然后又改回A,这时CAS操作会误认为自从上次读取以来V没有被修改过,从而可能产生错误的操作结果。循环时间过长问题:CAS操作如果长时间不成功,会不断进行重试,这可能会导致线程长时间处于忙等(Busy-Wait)状态,从而导致CPU长时间做无效操作。多变量原子问题:CAS只能保证一个变量的原子操作。详解CAS详解CAS,CompareAndSwap,即比较并交换。Douglea大神在同步组件中
synchronized和ReentrantLock两者的功能是一致的,都是Java中用于管理并发和同步机制的,但它们两者之间还是存在一些差异的。用法不同synchronized可用来修饰普通方法、静态方法和代码块publicsynchronizedvoidtest(){//...}publicvoidtest(){synchronized(this){//...}}ReentrantLock只能用在代码块上。privatefinalReentrantLocklock=newReentrantLock();publicvoidtest(){//加锁lock.lock();try{//...}finally{//释放锁lock.unlock();}}获取锁和释放锁机制不同synchronized的获取锁和释放锁是自动的,当进入synchronized修饰的方法或者方法体内,会自动获取锁,当执
回答ReentrantLock是一种可重入的排它锁,主要用来解决多线程对共享资源竞争的问题。ReentrantLock提供了比synchronized更加灵活、更强大的锁机制。它的核心特性有如下几个:可重入性:ReentrantLock是一个可重入锁,也就是说同一个线程可以多次获取同一个锁。公平锁和非公平锁:ReentrantLock支持公平和非公平两种方式。公平锁意味着等待时间最长的线程将会首先获得锁非公平锁可能会让新请求的线程优先于正在等待的线程获得锁。ReentrantLock的底层实现是基于AbstractQueuedSynchronizer框架,即AQS。AQS使用一个volatile整数(state)来表示同步状态,并维护了一个等待线程的队列。在ReentrantLock中,当线程尝试获取锁时,如果锁未被占用(即state为0),该线程将成功获取锁并将state值+1。如果锁已
当线程调用release()释放锁时,完成锁释放后调用unparkSuccessor()唤醒后继节点的线程,这个方法里面有一个这样的逻辑判断,当该节点的next为空,或者状态为已取消,则AQS从tail节点开始往前搜索节点:这里为什么要从tail节点开始呢?我们知道一个线程获取锁失败后,会被包装成一个Node节点加入到CLH同步队列中:如果tail没有被初始化或者设置tail节点失败都会走enq(),在这里我们考虑设置tail节点失败的情况,tail节点失败说明有多个线程在设置tail节点,走enq():privateNodeenq(finalNodenode){for(;;){Nodet=tail;if(t==null){//Mustinitializeif(compareAndSetHead(newNode()))tail=head;}else{node.prev=t;if(compa
回答当线程获取锁失败后,会加入到CLH同步队列中,同时调用LockSupport.park()阻塞。当线程将锁释放完毕后,需要唤醒后继节点,调用LockSupport.unpark()唤醒线程。LockSupport用来创建锁和其他同步类的基本线程阻塞原语,相比wait()和notify()提供了一种非常灵活的线程阻塞和唤醒机制。它提供提供了两个方法:park():用于阻塞当前线程。unpark():用于唤醒一个被park()阻塞的线程。LockSupport使用一种名为“许可证”的概念。每个线程都有一个与之关联的许可证(最多一个)。park()会消耗掉这个许可证(如果存在的话),并在没有许可证的情况下阻塞线程。而unpark()则提供一个许可证。详细AQS实现线程阻塞在线程获取锁失败后,会被包装为一个Node节点,添加到CLH同步队列中,并且该节点会自旋式地不断检测是否可以获取锁,在获取
回答一个线程获取锁失败后,会被包装为Node节点接入到CLH同步队列中,CLH同步队列是一个FIFO的队列,理论上来说实现一个单向的就可以了,为什么要设计为双向的呢?其实JDK注释就已经说明了答案:prev用于处理中断next用于唤醒后续阻塞线程详解CLH单向链表结构是这样的:+------+prev+-----++-----+head||<----||<----||tail+------++-----++-----+只有prev没有next。更好处理中断操作AQS提供了一个acquireInterruptibly(intarg),该方法用于获取锁过程中处理中断信号。publicfinalvoidacquireInterruptibly(intarg)throwsInterruptedException{if(Thread.interrupted())thrownewInter
回答AQS是AbstractQueuedSynchronizer简称,它是J.U.C包中多个组件的底座,如ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier都是基于AQS来实现的。AQS底层利用volatile类型的intstate来表示同步状态,该字段的含义取决于实现的同步器,如在ReentrantLock中,state表示持有锁的数量,在Semaphore中,state表示可用的许可证数量。当线程来获取同步锁时,如果status=0,说明目前没有任何线程占有锁,该线程可以获得锁并设置state=1。如果state>0,则说明有线程正在持有锁,则线程必须加入同步队列进行等待(不考虑重入)。AQS内部还维护着一个FIFO的双向同步队列,该队列通过Node的实例来构建的,每个Node代表一个等待获取资源的线程。获取锁失败的线程都会被
回答SimpleDateFormat不是线程安全,当多个线程使用一个SimpleDateFormat实例(如static修饰),调用format()格式化时,会出现线程不安全的情况。其原因是SimpleDateFormat内部使用一个Calendar实例来存储解析和格式化的中间结果,但这个Calendar实例是一个成员变量且可变,当多个线程共享SimpleDateFormat时,也会共享Calendar,然而SimpleDateFormat内部没有进行任何形式的锁或者同步机制来保护这个Calendar,所以,在多线程环境下,他们之间就会互相干扰,导致日期解析或格式化错误。详细分析SimpleDateFormat线程不安全演示示例一@Testpublicvoidtest01(){SimpleDateFormatsimpleDateFormat=newSimpleDateFormat(&quo