JAVA并发面试常见问题

1.HashMap

在面试中HashMap是问的很多的,我们来大致看一下HashMap的一些特性和考点

HashMap 的存取是没有顺序的。
KV 均允许为 NULL。
多线程情况下该类安全,可以考虑用 HashTable。
JDk8底层是数组 + 链表 + 红黑树,JDK7底层是数组 + 链表。
初始容量和装载因子是决定整个类性能的关键点,轻易不要动。
HashMap是懒汉式创建的,只有在你put数据时候才会 build。
单向链表转换为红黑树的时候会先变化为双向链表最终转换为红黑树,切记双向链表跟红黑树是共存的。
对于传入的两个key,会强制性的判别出个高低,目的是为了决定向左还是向右放置数据。
链表转红黑树后会努力将红黑树的root节点和链表的头节点 跟table[i]节点融合成一个。
在删除的时候是先判断删除节点红黑树个数是否需要转链表,不转链表就跟RBT类似,找个合适的节点来填充已删除的节点。
红黑树的root节点不一定跟table[i]也就是链表的头节点是同一个,三者同步是靠MoveRootToFront实现的。而HashIterator.remove()会在调用removeNode的时候movable=false。

常见考点

HashMap原理,内部数据结构。
HashMap中的put、get、remove大致过程。
HashMap中 hash函数实现。
HashMap如何扩容。
HashMap几个重要参数为什么这样设定。
HashMap为什么线程不安全,如何替换。
HashMap在JDK7跟JDK8中的区别。
HashMap中链表跟红黑树切换思路。
JDK7中 HashMap环产生原理。

首先看一下HashMap扩容流程图

HashMap的默认大小

默认大小16,负载因子0.75,大小必须为2的整数次方,比如输入7 大小为8,输入10大小其实为16

HashMap的Hash函数怎么设计的

先看源代码

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

就是让hashcode和他的高16位进行异或操作

为什么这么设计呢?

  1. 一定要尽可能降低hash碰撞,越分散越好;
  2. 算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;

hash函数能不能直接用key的hashcode?

因为key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为**-2147483648~2147483647**,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。你想,如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。

JDK1.8的优化有哪些呢?

数组+链表改成了数组+链表或红黑树;

链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;

扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;

在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;

链表转红黑树的长度阈值为多少,为什么是这个数

链表转红黑树阈值为8,红黑树转链表阈值为6,因为经过计算,在hash函数设计合理的情况下,发生hash碰撞8次的几率为百万分之6,概率说话。。因为8够用了,至于为什么转回来是6,因为如果hash碰撞次数在8附近徘徊,会一直发生链表和红黑树的互相转化,为了预防这种情况的发生。

2.ConcurrentHashMap

为了解决hashmap的线程不安全我们一般使用concurrentHashMap

首先来看一下JDK1.7和1.8的区别

2.1 JDK7

JDK7中的 ConcurrentHashMap 使用 Segment + HashEntry 分段锁实现并发,它的缺点是并发程度是由Segment 数组个数来决定的,并发度一旦初始化无法扩容,扩容的话只是HashEntry的扩容。

Segment 继承自 ReentrantLock,在此扮演锁的角色。可以理解为我们的每个Segment都是实现了Lock功能的HashMap。如果我们同时有多个Segment形成了Segment数组那我们就可以实现并发咯。

1.ConcurrentHashMap底层大致实现?

ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,只要多个修改操作发生在不同的段上就可以并发进行。

2.ConcurrentHashMap在并发下的情况下如何保证取得的元素是最新的?

用于存储键值对数据的HashEntry,在设计上它的成员变量valuenext都是volatile类型的,这样就保证别的线程对value值的修改,get方法可以马上看到,并且get的时候是不用加锁的

3.ConcurrentHashMap弱一致性体现在clear和get方法,原因在于没有加锁

比如迭代器在遍历数据的时候是一个Segment一个Segment去遍历的,如果在遍历完一个Segment时正好有一个线程在刚遍历完的Segment上插入数据,就会体现出不一致性。clear也是一样。get方法和containsKey方法都是遍历对应索引位上所有节点,都是不加锁来判断的,如果是修改性质的因为可见性的存在可以直接获得最新值,不过如果是新添加值则无法保持一致性

4.size 统计个数不准确

size方法比较有趣,先无锁的统计所有的数据量看下前后两次是否数据一样,如果一样则返回数据,如果不一样则要把全部的segment进行加锁,统计,解锁。并且size方法只是返回一个统计性的数字

2.2 JDK8

ConcurrentHashMapJDK8中抛弃了分段锁,转为用 CAS + synchronized,同时将HashEntry改为Node,还加入了红黑树的实现

ConcurrentHashMap 是如果来做到高效并发安全?

  1. 读操作

get方法中根本没有使用同步机制,也没有使用unsafe方法,所以读操作是支持并发操作的。

  1. 写操作

基本思路跟HashMap的写操作类似,只不过用到了CAS + syn 实现加锁,同时还涉及到扩容的操作。JDK8中锁已经细化到 table[i] 了,数组位置不同可并发,位置相同则去帮忙扩容。

  1. 同步处理主要是通过synunsafe的硬件级别原子性这两种方式完成
  2. 当我们对某个table[i]操作时候是用syn加锁的。
  1. 取数据的时候用的是unsafe硬件级别指令,直接获取指定内存的最新数据。

3.1 进程跟线程

进程

进程是操作系统调用的最小单位,是系统进行资源分配和调度的独立单位。

线程

  1. 因为进程的创建、销毁、切换产生大量的时间和空间的开销,进程的数量不能太多,而线程是比进程更小的能独立运行的基本单位,他是进程的一个实体,是CPU调度的最小单位。线程可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。
  2. 线程基本不拥有系统资源,只有一些运行时必不可少的资源,比如程序计数器、寄存器和栈,进程则占有堆、栈。线程,Java默认有两个线程 main 跟GC。Java是没有权限开线程的,无法操作硬件,都是调用的 nativestart0 方法 由 C++ 实现

3.2 并行跟并发

并发:

concurrency : 多线程操作同一个资源,单核CPU极速的切换运行多个任务

并行:

parallelism :多个CPU同时使用,CPU多核 真正的同时执行

3.3 线程几个状态

Java中线程的状态分为6种:

  1. 初始(New):

新创建了一个线程对象,但还没有调用start()方法。

  1. 可运行(Runnable):
  2. 调用线程的start()方法,此线程进入就绪状态。就绪状态只是说你资格运行,调度程序没有给你CPU资源,你就永远是就绪状态。
  1. 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
  2. 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
  1. 锁池里的线程拿到对象锁后,进入就绪状态
  2. 运行中(Running)

就绪状态的线程在获得CPU时间片后变为运行中状态(running)。这也是线程进入运行状态的唯一的一种方式。

  1. 阻塞(Blocked):

阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

  1. 等待(Waiting) 跟 超时等待(Timed_Waiting):
  2. 处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒(通知或中断),否则会处于无限期等待的状态。
  1. 处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
  2. 终止(Terminated):

当线程正常运行结束或者被异常中断后就会被终止。线程一旦终止了,就不能复生。

PS:

  1. 调用 obj.wait 的线程需要先获取 objmonitorwait会释放 objmonitor 并进入等待态。所以 wait()/notify() 都要与 synchronized 联用。
  2. 其实线程从阻塞/等待状态 到 可运行状态都涉及到同步队列等待队列的,这点在 AQS 有讲。

3.4. 阻塞与等待的区别

阻塞

当一个线程试图获取对象锁(非JUC库中的锁,即synchronized),而该锁被其他线程持有,则该线程进入阻塞状态。它的特点是使用简单,由JVM调度器来决定唤醒自己,而不需要由另一个线程来显式唤醒自己,不响应中断。

等待

当一个线程等待另一个线程通知调度器一个条件时,该线程进入等待状态。它的特点是需要等待另一个线程显式地唤醒自己,实现灵活,语义更丰富,可响应中断。例如调用:Object.wait()、**Thread.join()**以及等待 LockCondition

虽然 synchronized JUC 里的 Lock 都实现锁的功能,但线程进入的状态是不一样的。synchronized 会让线程进入阻塞态,而 JUC 里的 Lock是用park()/unpark() 来实现阻塞/唤醒 的,会让线程进入等待状态。虽然等锁时进入的状态不一样,但被唤醒后又都进入Runnable状态,从行为效果来看又是一样的。

3.5 yield 跟 sleep 区别

  1. yieldsleep 都能暂停当前线程,都不会释放锁资源sleep 可以指定具体休眠的时间,而 yield 则依赖 CPU 的时间片划分。
  2. sleep方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会。yield方法只会给相同优先级或更高优先级的线程以运行的机会。
  1. 调用 sleep 方法使线程进入等待状态,等待休眠时间达到,而调用我们的 yield方法,线程会进入就绪状态,也就是sleep需要等待设置的时间后才会进行就绪状态,而yield会立即进入就绪状态
  2. sleep方法声明会抛出 InterruptedException,而 yield 方法没有声明任何异常
  1. yield 不能被中断,而 sleep 则可以接受中断。
  2. sleep方法比yield方法具有更好的移植性(跟操作系统CPU调度相关)

3.6 wait 跟 sleep 区别

  1. 来源不同

wait 来自Objectsleep 来自 Thread

  1. 是否释放锁

wait 释放锁,sleep 不释放

  1. 使用范围

wait 必须在同步代码块中,sleep 可以任意使用

  1. 捕捉异常

wait 不需要捕获异常,sleep 需捕获异常

3.7 多线程实现方式

  1. 继承 Thread,实现run方法
  2. 实现 Runnable接口中的run方法,然后用Thread包装下。Thread 是线程对象,Runnable 是任务,线程启动的时候一定是对象。
  1. 实现 Callable接口,FutureTask 包装实现接口,Thread 包装 FutureTaskCallableRunnable 的区别在于Callablecall方法有返回值,可以抛出异常,Callable有缓存。
  2. 通过线程池调用实现。
  1. 通过Spring的注解 @Async 实现。

3.8 死锁

死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于某些锁的特性,比如syn使用下,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其它线程是获取不到这个锁的,而且会一直死等下去,因此这便造成了死锁。

面试官:你给我解释下死锁是什么,解释好了我就录用你。

应聘者:先发Offer,发了Offer我给你解释什么是死锁。

产生条件

  1. 互斥条件:一个资源,或者说一个锁只能被一个线程所占用,当一个线程首先获取到这个锁之后,在该线程释放这个锁之前,其它线程均是无法获取到这个锁的。
  2. 占有且等待:一个线程已经获取到一个锁,再获取另一个锁的过程中,即使获取不到也不会释放已经获得的锁。
  1. 不可剥夺条件:任何一个线程都无法强制获取别的线程已经占有的锁
  2. 循环等待条件:线程A拿着线程B的锁,线程B拿着线程A的锁。。

检查

1、jps -l 定位进程号

2、jstack 进程号找到死锁问题

避免

  1. 加锁顺序:线程按照相同的顺序加锁。
  2. 限时加锁:线程获取锁的过程中限制一定的时间,如果给定时间内获取不到,就算了,这需要用到Lock的一些API。

4、JMM

4.1 JMM由来

随着CPU内存磁盘的高速发展,它们的访问速度差别很大。为了提速就引入了L1、L2、L3三级缓存。以后程序运行获取数据就是如下的步骤了。

这样虽然提速了但是会导致缓存一致性问题跟内存可见性问题。同时编译器跟CPU为了加速也引入了指令重排。指令重排的大致意思就是你写的代码运行运算结果会按照你看到的逻辑思维去运行,但是在JVM内部系统是智能化的会进行加速排序的。

1、编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2、指令级并行的重排序:现代处理器采用了指令级并行技术在不影响数据依赖性前提下重排。

3、内存系统的重排序:处理器使用缓存和读/写缓冲区 进程重排。

指令重排这种机制会导致有序性问题,而在并发编程时经常会涉及到线程之间的通信跟同步问题,一般说是可见性、原子性、有序性。这三个问题对应的底层就是 缓存一致性内存可见性有序性

原子性:原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。比如 a = 1。

可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。Java保证可见性可以认为通过volatilesynchronizedfinal来实现。

有序性:程序执行的顺序按照代码的先后顺序执行,Java通过volatilesynchronized来保证。

为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存模式下多线程程序读写操作行为的规范,既JMM模型,注意JMM只是一个约定概念,是用来保证效果一致的机制规范。它作用于工作内存和主存之间数据同步过程,规定了如何做数据同步以及什么时候做数据同步。

在JMM中,有两条规定:

  1. 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。
  2. 不同线程之间无法访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

共享变量要实现可见性,必须经过如下两个步骤:

  1. 把本地内存1中更新过的共享变量刷新到主内存中。
  2. 把主内存中最新的共享变量的值更新到本地内存2中。

同时人们提出了内存屏障、happen-before、af-if-serial这三种概念来保证系统的可见性、原子性、有序性。

4.2 内存屏障

内存屏障 (Memory Barrier) 是一种CPU指令,用于控制特定条件下的重排序内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,从而让程序按我们预想的流程去执行。具有如下功能:

  1. 保证特定操作的执行顺序。
  2. 影响某些数据(或则是某条指令的执行结果)的内存可见性。

volatile 中就用到了内存屏障,volatile部分已详细讲述。

4.3 happen-before

因为有指令重排的存在会导致难以理解CPU内部运行规则,JDK用 happens-before 的概念来阐述操作之间的内存可见性。在JMM 中如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系 。其中CPU的happens-before无需任何同步手段就可以保证的。

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  • 线程中断规则:对线程interrupt方法的调用happens-before于被中断线程的代码检测到中断事件的发生。

4.4 af-if-serial

af-if-serial 的含义是不管怎么重排序(编译器和处理器为了提高并行度),单线程环境下程序的执行结果不能被改变且必须正确。该语义使单线程环境下程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。

5、volatile

volatile 关键字的引入可以保证变量的可见性,但是无法保证变量的原子性,比如 a++ 这样的是无法保证的。这里其实涉及到JMM 的知识点,Java多线程交互是通过共享内存的方式实现的。当我们读写volatile变量时具有如下规则:

  1. 当写一个volatile变量时,JMM会把该线程对应的本地中的共享变量值刷新到主内存
  2. 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量

volatile就会用到上面说到的内存屏障,目前有四种内存屏障:

  1. StoreStore屏障,保证普通写不和volatile写发生重排序
  2. StoreLoad屏障,保证volatile写与后面可能的volatile读写不发生重排序
  1. LoadLoad屏障,禁止volatile读与后面的普通读重排序
  2. LoadStore屏障,禁止volatile读和后面的普通写重排序

volatile原理:用volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令,在CPU级别的功能如下:

  1. 将当前处理器缓存行的数据写回到 系统内存。
  2. 这个写回内存的操作会告知在其他CPU你们拿到的变量是无效的下一次使用时候要重新共享内存拿。

6、单例模式 DCL + volatile

6.1 标准单例模式

高频考点单例模式:就是将类的构造函数进行private化,然后只留出一个静态的 Instance 函数供外部调用者调用。单例模式一般标准写法是 DCL + volatile

public class SingleDcl {
    private volatile static SingleDcl singleDcl; //保证可见性
    private SingleDcl(){
    }
    public static SingleDcl getInstance(){
        // 放置进入加锁代码,先判断下是否已经初始化好了
     if(singleDcl == null) { 
     // 类锁 可能会出现 AB线程都在这卡着,A获得锁,B等待获得锁。
      synchronized (SingleDcl.class) { 
    if(singleDcl == null) {
     // 如果A线程初始化好了,然后通过vloatile 将变量复杂给住线程。
     // 如果此时没有singleDel === null,判断 B进程 进来后还会再次执行 new 语句
     singleDcl = new SingleDcl();
    }
   }
     }
        return singleDcl;
    }
}

6.2 为什么用Volatile修饰

不用Volatile则代码运行时可能存在指令重排,会导致线程一在运行时执行顺序是 1-->2--> 4 就赋值给instance变量了,然后接下来再执行构造方法初始化。问题是如果构造方法初始化执行没完成前 线程二进入发现instance != null,直接给线程二个半成品,加入volatile后底层会使用内存屏障强制按照你以为的执行。

单例模式几乎是面试必考点,,一般有如下特性:

懒汉式:在需要用到对象时才实例化对象,正确的实现方式是 Double Check + Lock + volatile,解决了并发安全和性能低下问题,对内存要求非常高,那么使用懒汉式写法。

饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题。

枚举式:Effective Java 这本书也列举了使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。

7、线程池

7.1 五分钟了解线程池

老王是个深耕在帝都的一线码农,辛苦一年挣了点钱,想把钱存储到银行卡里,拿钱去银行办理遇到了如下的遭遇

  1. 老王银行门口取号后发现有柜台营业ing 但是没人办理业务就直接办理了。
  2. 老王取号后发现柜台上都有人在办理,等待席有空地,去坐着等办理去了。
  1. 老王取号后发现柜台都有人办理,等待席也人坐满了,这个时候银行经理看到老王是老实人本着关爱老实人的态度,新开一个临时窗口给他办理了。
  2. 老王取号后发现柜台都满了,等待座位席也满了,临时窗口也人满了。这个时候银行经理给出了若干解决策略。
  1. 直接告知人太多不给你办理了。
  2. 采用冷暴力模式,也不给不办理也不让他走。
  1. 经理让老王取尝试跟座位席中最前面的人聊一聊看是否可以加塞,可以就办理,不可以还是被踢走。
  2. 经理直接跟老王说谁让你来的你找谁去我这办理不了。

上面的这个流程几乎就跟JDK线程池的大致流程类似,其中7大参数:

  1. 营业中的3个窗口对应核心线程池数:corePoolSize
  2. 银行总的营业窗口数对应:maximumPoolSize
  1. 打开的临时窗口在多少时间内无人办理则关闭对应:keepAliveTime
  2. 临时窗口存货时间单位:TimeUnit
  1. 银行里的等待座椅就是等待队列:BlockingQueue
  2. threadFactory 该参数在JDK中是 线程工厂,用来创建线程对象,一般不会动。
  1. 无法办理的时候银行给出的解决方法对应:RejectedExecutionHandler

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,一般有四大拒绝策略

  1. ThreadPoolExecutor.AbortPolicy :丢弃任务,并抛出 RejectedExecutionException 异常。
  2. ThreadPoolExecutor.CallerRunsPolicy:该任务被线程池拒绝,由调用 execute方法的线程执行该任务。
  1. ThreadPoolExecutor.DiscardOldestPolicy :抛弃队列最前面的任务,然后重新尝试执行任务。
  2. ThreadPoolExecutor.DiscardPolicy:丢弃任务,也不抛出异常。

7.2 正确创建方式

使用Executors创建线程池可能会导致OOM。原因在于线程池中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueueLinkedBlockingQueue

  1. ArrayBlockingQueue 是一个用数组实现的有界阻塞队列,必须设置容量。
  2. LinkedBlockingQueue 是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,极易容易导致线程池OOM。

正确创建线程池的方式就是自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(10));

7.3 常见线程池

罗列几种常见的线程池创建方式。

  1. Executors.newFixedThreadPool

定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程。 使用的无界的等待队列是LinkedBlockingQueue。使用时候小心堵满等待队列。

  1. Executors.newSingleThreadExecutor

创建单个线程数的线程池,它可以保证先进先出的执行顺序

  1. Executors.newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  1. Executors.newScheduledThreadPool

创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行

  1. ThreadPoolExecutor

最原始跟常见的创建线程池的方式,它包含了 7 个参数、4种拒绝策略 可用。

7.4 线程池核心点

线程池 在工作中常用,面试也是必考点。关于线程池的细节跟使用在以前举例过一个 银行排队 办业务的例子了。线程池一般主要也无非就是下面几个考点了:

  1. 为什么用线程池。
  2. 线程池的作用。
  1. 7大重要参数
  2. 4大拒绝策略
  1. 常见线程池任务队列,如何理解有界跟无界。
  2. 常用的线程池模版。
  1. 如何分配线程池个数,IO密集型 还是 CPU密集型
  2. 设定一个线程池优先级队列,Runable 类要实现可对比功能,任务队列使用优先级队列。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值