您的位置 首页 java

java后端必会「基础知识点」

(一) java 集合类(done)

在java集合类中最常用的是 Collection 和Map的接口实现类。Collection又分为List和Set两类接口,List的实现类有ArrayList、LinkedList、 Vector 、Stack,Set接口的实现类有 hash Set、TreeSet,而Map的实现类主要有 hashMap ConcurrentHashMap 、TreeMap。

ArrayList是容量可以改变的非线程安全集合(可以使用Collections. synchronized List方法实现线程安全),内部使用数组进行存储。ArrayList支持对元素的快速随机访问,但是插入和删除时速度通常会很慢,因为这个过程需要移动其他元素。ArrayList的默认大小为10,在不传入指定的列表大小时,默认使用空列表。ArrayList在每次添加数据时都会检查空间是否足够,若不足就会按原数组空间的1.5倍扩容(向下取整),最后会将新的数组空间大小和插入数据后需要的空间大小作比较,取两者之前的更大者。之后就会将旧数组整体拷贝进新的数组空间。

HashMap的默认容量值为16,默认负载因子为0.75,默认 阈值 为12。HashMap的容量不会在new的时候就分配,而是在put的时候才进行分配。HashMap的实际容量为比传入容量参数大的2的幂,而ConcurrentHashMap实际容量为比(传入容量参数除4/3后)大的2的幂,此后每次扩容都是增加2倍。HashMap的容量为2的幂的主要是因为HashMap在计算槽位的时候使用的算法是(n-1)& hash,当n为2的幂时元素可以更加均匀的散列。 HashTable 是HashMap的线程安全版本,两者主要区别在于HashTable在一些关键的函数中增加了synchronized关键字,由于性能不佳目前已经被ConcurrentHashMap淘汰了。HashMap是线程不安全的,在java8以前甚至可能出现由于 多线程 resize导致的循环链表, 线程 不安全的两个小例子:两个线程同时对同一桶位插入数据可能导致某个线程的数据被覆盖、某个线程在执行resize时另一线程可能读不到数据。

HashMap的key和value都可以为null,key为null的元素会被散列到数组的第一个元素,但在使用stream的Collectors.toMap方法时HashMap的value不可以为null,且key不可以重复,否则会抛出异常。另外不同于HashMap,ConcurrentHashMap的key和value都不能为null。

java8对HashMap的改进?

主要有两大改动

(1)在数组桶的 链表 长度超过8时,HashMap通过 红黑树 替换链表。

(2)在HashMap扩容时不再重新对每个节点计算桶位,而是通过(节点的hash值&旧数组容量 == 0)来形成两条新的链表,分别插在原桶位和旧数组容量+原桶位。

树的基础知识:

高度:从某节点出发到叶子节点的简单路径上边的数量被称为该节点的“高度”

重要的 二叉树 平衡二叉树 、二叉查找树、红黑树

平衡二叉树:树及所有子树的左右高度差不超过1

二叉查找树:对于树的任意节点而言它的左子树的所有节点值都小于它,而他的右子树的所有节点值都大于它。它主要有前序遍历、 中序遍历 、后序遍历三种遍历方式。随着数据不停的增加或删除,二叉查找树容易失衡

红黑树:红黑树是一种可以自平衡的二叉查找树。红黑树在每个节点上增加了颜色属性,可以为红色或黑色,红黑树通过按规则着色和特定的旋转来保持自身的平衡,它新增、删除、查找的最坏时间复杂度均为O(logN)。相对于其他的自平衡树例如AVL树,红黑树并不严格保证时左右子树的高度差超过1,这使得红黑树在删除时能够更快的恢复平衡,成本比较低,所以面对频繁的插入删除时,红黑树更适合,而面对低频修改,大量查询时AVL树更合适

Comparable接口和Comparator接口的区别?

如果要使用Comparable接口,就必须实现该接口并重写 compareTo 方法。而Comparator接口可以在类外实现,并可以将其实现对象传入到Collections.sort或Arrays.sort方法中以实现排序。Comparator接口的使用体现了基于开闭原则的设计。

集合中的 hashCode 和equals?

hashCode和equals用来标识对象,两者协同工作来判断对象是否相等,当hashCode的值相同时,还需要调用equals进行一次值比较。任何时候在覆写equals时一定要同时覆写hashCode,因为Map、Set等集合都是同时使用两者来判断对象是否相同的。hashCode是根据对象地址进行相关计算得到的int类型数值。在做对象间的比较时,尽量使用Objects.equals方法来避免空指针。

集合的 fail-fast 机制?

fail-fast是一种在集合遍历时的错误检测机制,如果在遍历的过程中出现了意料之外的修改就会抛出ConcurrentModification Exception 异常,这种机制经常出现在多线程环境下。线程会维护一一个modCount用来记录集合已经修改的次数,在遍历集合时会时时检查modCount的数值是否发生变化,若发生变化则抛出异常。

ConcurrentHashMap知识:

在java8以前ConcurrentHashMap通过锁分段的思想将整个hashMap分为16个 segment ,每个segment负责一部分数组元素,并通过reentrant lock 锁来保证每个segment的数据安全。而在java8后ConcurrentHashMap取消了segment,大量的使用了 volatile 、cas等技术进一步减少了锁竞争造成的性能影响。java8后ConcurrentHashMap有三点主要的改动:(1)取消分段锁机制,进一步降低冲突概率、(2)引入红黑树、(3)使用了更加优秀的方式统计元素的数量。

get操作逻辑:计算出key的hash值并计算出槽位,然后通过getObjectVolatile获取数该槽位的元素,并比较hash值和key值,若相同则返回,如果槽位内节点的hash值小于0则说明正在进行扩容,则通过ForwardingNode的find函数去新的数组nextTable中进行查找。否则就遍历单链表查找相应节点。

put操作逻辑:首先检查核心的Node<K,V>[] table是否已经初始化,如果没有初始化,则利用CAS将sizeCtl的值置为-1进行初始化。查询key相应的槽位是否为 null,若为null直接通过CAS将键值对放入槽位。如果相应的槽位已经有节点,并且其hash值为-1,则表示正在进行扩容,则当前线程帮忙进行扩容。否则通过synchronized锁住槽位内的节点即链表的头结点,然后遍历链表,寻找是否有hash值及key值相同的节点,若有则将value设置进去,否者创建新的节点加入链表。通过addCount函数更新ConcurrentHashMap键值对的数量,并检查是否需要进行扩容。

扩容操作逻辑:首先新建一个两倍长度的数组nextTable。初始化ForwardingNode节点,其中保存了新数组nextTable的引用,在处理完每个槽位节点后当做占位节点,表示该槽位已经处理过了。通过倒序的方式为线程分配需要处理数组元素个数(默认每个线程16个元素)。每个线程处理自己负责的数组元素,具体逻辑和HashMap基本相应,处理完的元素的位置上会放入ForwardingNode节点。

计数方法:代码里的变量baseCount用于在无竞争环境下记录元素的个数,每当插入元素或删除元素时都会利用CAS更新键值对个数。当有线程竞争时,会使用CounterCell数组来计数,每个ConuterCell都是一个独立的计数单元。线程可以通过 ThreadLocal Random.getProbe() & m找到属于它的CounterCell进行计数。这种方法能够降低线程的竞争,相比所有线程对一个共享变量不停进行CAS操作性能上要好很多。这里的CounterCell数组初始容量为2,最大容量是机器的 CPU 数。

(二)java并发包(done)

同步框架AQS介绍?

AQS是构建锁和其他同步组件基础框架,内部维护一个整形字段 state 来表示同步状态,同时维护了一个双向链表(FIFO的CLH同步队列),链表的每个节点都表示一个线程,包含了线程引用、节点状态、前驱节点引用、后驱节点引用。整数state在不同的同步组件中可以表示不同的状态,例如: reentrant Lock可以用它来表示锁的重入次数、 semaphore 用它来表示剩余的许可数量,futureTask用来表示任务的状态(尚未开始、正在运行、已完成、已取消)。AQS是一个抽象模板类,内部实现了大量的构建同步器通用方法,通过继承实现AQS的相应接口就可以构建出很多同步组件,例如:reentrantLock、semaphore、futureTask等组件都通过了继承AQS的内部类来实现。除此之外AQS还实现了Condition接口,通过ConditionObject实现了条件队列(单向链表)。Condition条件队列需要配合ReentrantLock一起使用,当线程获取到ReentrantLock锁后,可以通过await进入条件队列并释放锁,或者通过singinal方法唤醒条件队列等待时间最久的线程并加入到等待队列中,条件队列使用和同步队列相同的节点类型。

reetrantLock的实现方式?

ReetrantLock提供了nonfairSync和fairSyn两个实例,分别用于实现非公平锁和公平锁,两者都继承于AQS。ReetrantLock主要提供了lock和unlock两个方法。

(1)非公平锁的lock方式实现:(1)首先直接通过CAS尝试将state从0设置为1,若成功则说明获取到了锁,保存当前线程引用以实现可重入。(2)否则获取state若值为0,则再次尝试将state设置为1,成功则同样保存当前线程。(3)否则检查获取锁的是否就是当前线程,如果是就增加state值,记录重入次数。(4)如果都不满足,则将当前线程封装成Node放入同步队列中,并将线程挂起。公平锁的不同之处在于:没有(1)这一步,另外在执行(2)时需检测同步队列是否有等待中的线程,若没有才能执行。

(2)非公平锁和公平锁的unlock方法完全相同: 获取state值,通过cas减去释放锁定的个数,如果值为0,则释放锁,并将同步队列中的第一个节点内的线程唤醒(LockSupport的unpark方法)。

reetrantLock和synchronize的差别?

reetrantLock和synchronize都是可重入的独占锁,且在加锁和内存上两者语义完全相同。reetrantLock还提供了一些高级的功能:可定时/可轮询/可中断的锁获取操作、公平队列、非块状结构的锁等,同时reetrantLock更加灵活且性能稍稍优于synchronize。但是reetrantLock的操作更加复杂,加锁和释放都需要手动操作,存在可能忘记释放锁的风险。reetrantLock的死锁检测更加复杂,在调试reetrantLock导致的 死锁 问题时会更加复杂。最后由于synchronize是 jvm 的内置属性字段,未来会随着版本持续优化,而reetrantLock的优化的可能性不大。所以在synchronize不能满足我们的高级需求时可以使用reetrantLock,否则最好还是使用synchronize。

countDownLatch的实现方式?

CountDownLatch主要用来保障某批线程的执行需要等待另一批线程执行完成后方可执行,例如老板巡视工人完成工作情况,需要等待三个工人全部完成好工作以后才能巡视。首先会获取要求同时开始某个动作的线程数量,将数量值设置给state。当state的值大于0时,调用CountDownLatch的await方法,相应线程会被挂起并放入AQS的同步队列中,直到调用countdown()函数将相应的state的值减为0,被挂起的线程才会被重新唤醒。和它比较相似的是CyclicBarrier,CyclicBarrier会确保所有线程都执行到某个点后,才可以继续往下执行。

futureTask的实现方式?

FutureTask利用了AQS的state来表示不同状态:主要有未开始、进行中、已完成。FutureTask继承了Runnable接口,对线程实际需要执行的逻辑进行了一层包装,当执行了线程实际逻辑获取到结果后,会更新state的值来标识任务已完成并将结果保存起来,同时会唤醒等待结果的线程。其他需要结果的线程通过get接口获取结果,如果任务已经执行完就直接获取相应结果,如果任务尚未执行完就挂起等待,知道执行任务的线程将其唤醒。

volatile的实现方式?

volatile具备两个关键的特性:一个是保证变量对所有线程的可见性,另一个是禁止指令重排序(包括cpu层面的指令乱序)。volatile在抽象逻辑层面上通过“ 内存栅栏 ”实现,而实际 字节码 层面通过lock指令实现:这个命令会将变量数据立即刷到主内存中,并利用cpu总线嗅探机制使其他线程高速缓存内的cacheline失效(cacheline是cpu高速缓存cache的基本读写单位),使用时必须重新到主内存Memory读取。同时因为需要立刻刷数据到内存中,那么 volatile变量 操作前的所有操作都需要完全执行完成,这样进而也保证了volatile变量写操作前后不会出现重排序。通常volatile变量的读写效率和普通变量没有多大差别,但在volatile变量并发访问冲突非常频繁的情况下可能造成性能的下降,具体的例子及解决方案可以百度“伪共享”问题。

synchronize的实现方式?

synchronized底层实现依赖于jvm用C++实现的管程(ObjectMonitor), 管程 是一种类似于信号量的程序结构,它封装了同步操作并对进程隐蔽了同步细节,其整体实现逻辑和ReentrantLock很相似。我们在使用synchronized时通常有两种方式:1.修饰方法、2.修饰代码块,其实两者差别不大,本质上都是同步代码块。在虚拟机层面上,当用synchronized修饰方法时, class文件 中会在方法表中为相应方法增加ACC_SYNCHRONIZED访问标志,用以标识该方法为同步方法。而当用synchronized修饰代码块时,会在相应代码段字节码的前后分别插入monitorenter和monitorexit字节码指令,用以表示该段代码需要同步。

在代码即将进入同步代码块的时候,如果此时同步对象还没有被锁定,虚拟机先会在栈帧中建立一个名为锁记录的空间(Lock Record)用于存储对象目前的mark word拷贝数据。然后虚拟机会把对象的mark word数据拷贝到锁记录空间中,并尝试通过CAS操作将对象的mark word数据更新为指向锁记录空间的引用,如果成功了就说明获取到了锁。如果出现两个以上的线程争用同一个锁时,那么轻量级锁就会膨胀为重量级锁,后面等待锁的线程都会进入阻塞状态。

在使用synchronized时,我们可以把synchronized修饰的方法或代码段想象成一段不可以并发访问的临界区资源,这种资源必须独占使用。而如何实现独占访问呢?我们可以想象每个对象都有把独占锁,我们需要借助某个对象的独占锁来访问这种 临界区 资源,而同一个锁某个时刻只能被一个线程所获取,其他线程都得等待锁的释放。用synchronized修饰的实例方法(public synchronized void method())默认使用当前对象(this)的锁,而用synchronized修饰的 静态方法 (public synchronized static void method())默认使用当前对象对应的Class对象锁,他们分别对应于synchronized修饰代码块中的synchronized(object)和synchronized(Object.class)。Class对象存在于方法区中,具有全局唯一性,在一个jvm实例中一个Class对象只有一把锁,所有使用该Class对象作为锁的静态方法或代码块,执行前都必须先获得该Class对象锁。而同一个Class可以有很多实例对象,每个实例对象都有一个自己的锁,使用实例对象A锁的线程和使用实例对象B锁的线程间不存在竞争关系。

线程池 的实现原理?

ThreadPoolExecutor 是线程池的主要实现类,它有以下几个构造参数:

(1)corePoolSize:常驻核心线程数,如果其值为0,则任务执行完成后,没有任何请求时会销毁线程池的线程。如果值大于0,没有任务时核心线程也不会被销毁。

(2)maximumPoolSize:线程池能够容纳的最大并行线程数,如果与corePoolSize相等,即为固定大小线程池。

(3)keepAliveTime:线程池中线程的空闲时间,当线程数量大于corePoolSize时才会起作用,当线程的空闲时间到达keepAliveTime时,线程会被销毁直到只剩下corePoolSize个线程为止。

(4)timeUnit:表示空闲时间单位。

(5)workQueue:需要使用的 缓存 队列,当核心线程无法处理新来的任务时,就会将任务放入到缓存队列workQueue中,当缓存队列存满后,如果还有需要处理,那么线程池就会继续创建新的线程,直到线程数量达到maximumPoolSize。

(6) thread Factory:用来生产线程的工厂。

(7)rejected Execution Handler:用来执行拒绝策略的对象。当缓存队列存满后,且线程数量已达到了maximumPoolSize时,就会执行拒绝策略。

线程池在创建后,当线程数量小于corePoolSize时,每来一个任务后就会创建一个线程来执行该任务,直到线程数达到corePoolSize。之后若新来的任务没有核心线程能够处理,就会将任务存入阻塞队列workQueue中,直到阻塞队列workQueue存满。若阻塞队列workQueue存满后,仍有大量的任务无法处理,就会继续增加线程处理任务,直到线程数量达到maximumPoolSize。若此时仍有无法处理的任务,就会执行任务的拒绝策略。ThreadPoolExecutor提供了4个公开的内部静态类:

(1)AbortPolicy(默认策略):丢弃任务并抛出RejectedExecutionException异常。

(2)DiscardPolicy:丢弃任务且不抛出异常。

(3) Discard OldestPolicy:抛弃等待最久的任务,然后把当前任务加入队列中。

(4)CallerRunsPolicy:主线程直接执行任务。

在spring中对ThreadPoolExecutor进一步进行了封装,提供了ThreadPoolTaskExecutor,帮忙我们提供了默认的timeUnit、workQueue、threadFactory、rejectedExecutionHandler实现对象。同时也可以让我们自主设置:阻塞队列大小、线程名称前缀、线程池关闭等待任务执行完成时间等等。除了ThreadPoolExecutor外,java还提供了线程池静态工厂类Executors,该类提供了几种默认的工厂实现,但实际开发中不建议使用:

(1)Executors.newCachedThreadPool:使用的是无界线程池,线程最大数量可达Integer.MAX_VALUE(2的31次幂减1),具有高度的伸缩性,keepAliveTime的默认值为60s,一旦线程空闲时间超过keepAliveTime,线程就会被回收,当长时间没有任何任务时,线程池的线程数为0,新的任务来时会建立新的线程。该线程使用的阻塞队列是 Synchronous Queue,不会缓存任何任务,每次任务过来后,都需要线程池内的线程连接处理,

(2)Executors.newSingleThreadExecutor:使用的是无界队列,线程池内只有一个线程,相当于单线程串行执行所有任务,保证任务能够按照提交顺序依次执行。

(3)Executors.newFixedThreadPool:同样使用的是无界队列,输入的参数即为固定线程数,不存在空闲线程。

线程池不应该通过Executors创建,而应该通过ThreadPoolExecutor创建,这样能够更加明确线程池的运行规则,规避资源耗尽的风险。以上线程池都不推荐使用,因为这些队列要么使用的是无界队列,要么使用的就是无界线程池,都可能会将服务器资源耗尽导致OOM。

阻塞队列LinkedBlockingQueue的实现原理?

LinkedBlockingQueue主要使用单向链表实现,并且使用了两个reetrantLock,用来分别生成notEmpty条件队列和notFull条件队列,这两个条件队列分别用来控制存操作和取操作。之所以使用两个reetrantLock同步队列,主要是因为使用单个reetrantLock则同一时刻只能进行读或者进行写。

ThreadLocal的实现原理及可能存在的问题?

ThreadLocal的实现并不算复杂,首先每个线程Thread对象都维护了一个ThreadLocalMap,这个Map是由ThreadLocal类实现的一个使用线性探测的自定义Map,Map的key是ThreadLocal对象的引用,而value就是我们需要存储的本地线程变量。值得注意的是ThreadLocalMap并没有使用拉链法,而是使用了线性探测法,这可能主要是因为ThreadLocalMap存储的数据量一般不会很大。另外还有一点非常重要:ThreadLocalMap的key被封装成了弱引用。当ThreadLocal对象threadLocalA没有其他强引用时,在下次GC来临时threadLocalA就会被回收,同时ThreadLocalMap相应槽位的key值会变为null,ThreadLocalMap在每次进行get/set操作时都会主动的去清空key为null的键值对。ThreadLocal的这种设计主要是为了防止出现内存泄露。假如key为强引用,那么当threadLocalA使用完后,ThreadLocalMap仍持有threadLocalA的强引用,将会导致threadLocalA无法回收。所以当我们使用ThreadLocal时一般都会定义一个static的threadLocal,并且在使用完相应的数据后,需要手动的执行ThreadLocal的remove移除相应的数据,防止出现内存泄露。

ThreadLocal主要存在两点可能的问题:(1)脏数据问题:因为线程池内的线程采取复用策略,如果线程执行上个任务时,没能显示的通过remove清理线程相关的ThreadLocal数据,那么在下个任务中便可能读到上个任务设置的ThreadLocal数据。(2)内存泄露:一般使用ThreadLocal时都会使用static,此时在使用完ThreadLocal后,若忘记通过remove清理数据,就可能导致内存泄露。

线程安全 的理解,什么样的类是线程安全的?

我们通常会将能够安全的被多个线程使用的对象称为线程安全对象,这个对象就是线程安全的。能够保证线程安全有以下几种情况:

(1)仅在单线程内可见的数据是线程安全的,比如使用ThreadLocal存储的数据。

(2)无状态对象总是线程安全的,即对象不包含任何属性以及对其他对象属性的引用,有的仅仅是纯代码方法。

(3)不可变的只读对象是线程安全的,这种对象只在构建时会被初始化,之后不允许进行任何修改和变更,通常会通过final修饰类或属性,例如String、Integer等等。

(4)java提供的线程安全类,比如concurrentHashMap等。

除了上面的这几种情况外,其他的并发情况就需要我们自己通过锁或者合适的同步工具保证数据安全。

线程的同步方式?

计算机的线程同步主要指的是:线程之间按照某种机制协调先后执行顺序,当有某个线程对内存进行操作时,其他线程都不可以对这块内存地址进行操作,直到该线程完成操作。实现线程同步的方式比较多,包括volatile、synchronized锁、reetrantLock锁、阻塞队列、及AQS实现的各种线程同步类等等。

Thread的join()函数实现原理是怎样的?

Thread threadObject = new Thread(new Runnable() {});threadObject.join();join方法会阻塞当前执行的主线程,而不是threadObject线程,直到threadObject线程执行完毕后才会唤醒当前执行的主线程。实现原理也比较简单,主要就是调用join()时将当前线程阻塞,当threadObject执行结束后,将所有因为自己阻塞的线程唤醒。

Thread.sleep、Object.wait、LockSupport.park区别?

三者都可以使线程阻塞挂起,Thread.sleep需要指定挂起时长,且不会释放持有的锁,相应的线程状态为TIMED_WAITING。Object.wait会让线程挂起但会释放掉持有的锁,且需要通过notify或notifyAll方法唤醒后才能继续运行,另外必须保证wait和notify的执行顺序,否则会出现问题,其线程状态为WAITING。LockSupport.park不会释放持有的锁,可以通过LockSupport.unpark唤醒,由于采用的是类似二元信号量的实现方式,所以unpark方法可以比park方法先执行,不会丢失唤醒信号,其线程状态同样为WAITING。

同步/异步&阻塞/非阻塞?

阻塞还是非阻塞,取决于线程所做的操作是否需要将线程挂起等待。同步还是非同步,取决于是否是当前线程亲自执行操作,若当前线程亲自执行操作则为同步,当前线程通过创建或利用其他线程执行操作则为异步。同步/异步&阻塞/非阻塞

高并发情况下pv、uv的统计?

pv:使用redis的incr原子命令进行统计,然后为key设置相应的过期时间即可。

uv:在uv量不大的情况下,可以使用redis的set类型数据,设置相应的过期时间,将用户id存入set集合中,通过scard就可获取某个时间点的uv数据。在uv量很大且数据并不要求十分精准时,可以使用hyperLogLog。

高并发情况下的限流?

在高并发下保护系统的三大利器:缓存、降级、限流。缓存可以增加系统访问速度和增大系统处理容量,降级是当服务器压力剧增时,根据业务情况和流量对一些服务和页面降级,以保证核心业务功能的正常运行。限流的目的主要是通过对并发请求进行限速和限量来保护系统,请求速率或请求量一旦达到阈值就可以排队、等待或者拒绝服务。

常用的限流方法:有计数器方式、令牌桶和和漏桶。计数器法比较简单粗暴,主要用来限制并发请求数量,一旦并发请求数量达到阈值,就可以直接拒绝请求,可以通过semaphore来实现。令牌桶除了能够限制请求的平均速率,还能够允许一定程度的突发流量。漏桶算法能够使请求速率均匀,不会接受突发流量。

死锁的检查与排查?

死锁主要分为两种情况:(1)锁顺序死锁、(2)资源死锁。锁顺序死锁:主要是因为线程之间分别持有对方需要的锁,但互相不释放自己持有的锁资源,最终就导致了死锁,谁也无法继续往下推进。在mysql数据库中存在同样的情况,但是mysql会通过在表示等待关系的有向图中搜索是否有环,若存在环则说明发生了死锁,mysql会选择代价比较低的事务进行回滚,这样就能保证另一个事务可以正常的执行。而在java中一旦发生死锁,这些线程就永远不能在使用了,可能造成系统性能降低,甚至造成应用程序完全停止。当线程需要获取多个锁时,如果所有线程都以固定的顺序获取锁,那么程序中就不会出现锁顺序死锁问题。资源死锁:当我们在使用线程池或信号量来限制对资源的使用时,这些限制也可能会导致资源死锁。比如线程的“饥饿死锁”,如果线程池中只有有限个线程,并且线程执行的任务的完成依赖于其生成的子任务,子任务同样在该线程池中运行,那么当子任务无法获取线程执行,一直存储在阻塞队列中,就造成了线程的“饥饿死锁”。

java死锁的排查比较简单:首先通过jps或ps -ef | grep java命令获取java进程的pid,然后通过jstack命令查看当前虚拟机内所有线程的快照,jstack会展示所有线程的状态信息,同时也会帮我们统计展示存在死锁的线程信息,包括死锁线程持有的锁及需要的锁,及相关锁的地址

文章来源:智云一二三科技

文章标题:java后端必会「基础知识点」

文章地址:https://www.zhihuclub.com/174705.shtml

关于作者: 智云科技

热门文章

网站地图