常见的集合类
Java集合类主要是由两个接口派生而来:Collection和Map。
其中Collection有三个子接口:List,Set和Queue。
Map接口没有继承Collection接口,但是Map接口和Collection接口一样,也是一个顶层接口。有三个子类:HashMap,TreeMap和HashTable。
list中有Vector,ArrayList,LinkedList,其中Vector是线程安全的,由Stack实现,ArrayList是非线程安全的,LinkedList是双向链表。
set中有hashset和sortedset,其中sortedset有treeset实现,hashset是基于linkedhashSet实现的。
queue中有priorityqueue和deque,其中deque有ArrayDeque和LinkedList实现,priorityqueue是基于heap实现的。
List,Set和Map的区别
List以索引来存取数据,可以有重复的值,可以有null值,有序。
Set不可以有重复的值,可以有null值,无序。
Map以键值对的形式存储数据,键不可以重复,值可以重复,可以有null值,无序。
List底层实现有数组和链表,Set底层实现有哈希表和红黑树,Map底层实现有哈希表和红黑树。
ArrayList
底层是动态数组,实现了List接口,实现了RandomAccess接口,支持随机访问,非线程安全,允许null值,有序。
扩容机制:当数组容量不够时,会扩容为原来的1.5倍。本质就是计算出新扩容数组的size后实例化,并将原有数组的值复制到新数组中。
HashMap扩容机制
当元素个数超过数组大小*负载因子时,就会发生扩容,扩容为原来的2倍。1.7使用头插法,1.8使用尾插法。但是在多线程情况下会出现环形链表或者数据覆盖的情况,所以是hashmap是线程不安全的。
HashMap和HashTable的区别
- HashMap是非线程安全的,HashTable是线程安全的。
- HashMap允许null键和null值,HashTable不允许null键和null值。
- hashtable很多方法都是同步的,效率低,HashMap不是同步的,效率高。
- 哈希值的使用不同,HashTable直接使用对象的hashCode,HashMap重新计算hash值。
linkedHashMap的实现原理
linkedHashMap继承自HashMap,底层是哈希表和双向链表,双向链表用来维护插入顺序或者访问顺序。linkedHashMap有两种顺序:插入顺序和访问顺序。默认是插入顺序,可以通过构造函数来指定访问顺序。
hashSet,linkedHashSet和treeSet的区别
hashSet底层是哈希表,linkedHashSet底层是哈希表和双向链表,treeSet底层是红黑树。hashSet不保证顺序,linkedHashSet保证插入顺序,treeSet保证排序顺序且排序顺序可以自定义。
手动创建线程的缺点
- 每次创建线程都需要消耗系统的资源,频繁创建线程会降低系统的性能。
- 不受控制,线程的生命周期由程序员控制,容易造成线程的泄漏。
创建线程的过程
- JVM会为每个线程分配一个独立的程序计数器,用来记录线程执行的位置。
- 每一栈帧由一个局部变量数组,返回值,操作数栈和常量池引用组成。
- 系统创建一个与Java线程对应的操作系统线程。
- 将与线程相关的描述符添加到JVM内部数据结构中
- 线程共享堆和方法区资源。
线程池的好处
- 降低资源消耗,重复利用已创建的线程,减少线程创建和销毁造成的消耗。
- 提高响应速度,任务到达时,无需等待线程创建就能立即执行。
- 提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。
如何判断线程池的任务是否执行完毕
- 调用线程池的isTerminated()方法,判断线程池中的任务是否执行完毕。
- 使用CountDownLatch,创建一个计数器,每次执行完一个任务,计数器减1,当计数器为0时,说明所有任务执行完毕。
- 使用重入锁,每次执行完一个任务,锁计数器减1,当计数器为0时,说明所有任务执行完毕。
- submit向线程池提交一个任务,返回一个future对象,调用future对象的get方法,如果任务执行完毕,返回null,否则阻塞。
为啥要使用Executor线程池框架
- 每次执行任务都需要创建线程,销毁线程,频繁创建和销毁线程会消耗系统资源,降低系统性能。
- 调用new Thread()创建线程,可以无限制的创建线程,如果创建过多的线程,会导致系统内存溢出。
- 直接使用new Thread()启动的线程不利于扩展,比如定时执行,定期执行,线程中断等操作,无法做到统一管理。
execute和submit的区别
- execute只能接受Runnable类型的任务,submit可以接受Runnable和Callable类型的任务。
- execute方法没有返回值,submit方法有返回值。
- execute在执行任务时,如果遇到异常,会直接抛出,submit在执行任务时,如果遇到异常,会将异常包装成ExecutionException,然后返回。
- execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。
线程的生命周期
- 初始状态:线程被创建,但是还没有调用start方法。
- 运行状态:包括就绪和运行两种状态,就绪状态表示线程已经调用start方法,但是还没有被分配到CPU执行,运行状态表示线程已经被分配到CPU执行。
- 阻塞状态:一般是被动的,在抢占资源中得不到资源,被动的挂起在内存,等待资源释放将其唤醒,线程被阻塞会释放CPU,不释放内存。
- 等待状态:一般是主动的,主动调用wait方法,释放CPU,释放内存,等待其他线程唤醒。
- 超时等待状态:一般是主动的,主动调用sleep方法,释放CPU,不释放内存,等待超时后自动唤醒。
- 终止状态:线程执行完毕或者异常终止。
创建线程的方式
- 通过扩展Thread类来创建线程类,重写run方法。
- 通过实现Runnable接口来创建线程类,实现run方法。
- 实现Callable接口来创建线程类,通过FutureTask包装器来创建Thread线程类。
- 使用Exector框架来创建线程池。
什么是线程死锁
线程死锁是指两个或者两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,这些线程都将无法推进下去。
线程中断
- 调用interrupt方法,将线程的中断标志位置为true。
- isInterrupted方法,判断线程的中断标志位是否为true。
- interrupted方法,判断线程的中断标志位是否为true,如果为true,将中断标志位重新置为false。
线程中断是指线程运行过程中被其他线程打断。与 stop 最大的区别是:stop 是由系统强制终止线程,而线程中断则是给目标线程发送一个中断信号,如果目标线程没有接收线程中断的信号并结束线程,线程则不会终止,具体是否退出或者执行其他逻辑取决于目标线程。
线程死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
避免死锁的方法
- 互斥条件不能破坏,因为加锁就是为了保证互斥
- 一次性申请所有的资源,避免线程占有资源而且在等待其他资源
- 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源
- 按序申请资源
volatile底层原理
volatile是轻量级的同步机制,保证可见性和有序性,不保证原子性。
- 当对volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。
- 由于缓存一致性协议,当处理器发现自己缓存行对应的内存地址被修改,会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作时,会重新从系统内存中把数据读到处理器缓存中。
synchronized
- 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁。
- 修饰静态方法:作用于当前类的Class对象,进入同步代码前要获得当前类的Class对象的锁。
- 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized的作用
- 原子性:确保线程互斥的访问同步代码
- 可见性:保证共享变量的修改能够及时可见
- 有序性:禁止指令重排序优化
synchronized底层原理
通过moniterenter和moniterexit指令实现,moniterenter指令插入到同步代码块的开始位置,moniterexit指令插入到同步代码块的结束位置,任何一个对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态,线程执行moniterenter指令时尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0 ,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
synchronized和volatile的区别
- volatile是轻量级的同步机制,只能修饰变量,而synchronized可以修饰方法,代码块,类。
- volatile保证可见性和有序性,不保证原子性,synchronized保证原子性,可见性和有序性。
- volatile不会造成线程阻塞,synchronized可能会造成线程阻塞。
- volatile禁止指令重排序优化,synchronized不会
synchronized和ReentrantLock的区别
- 使用sychronized不需要用户去手动释放锁,而ReentrantLock需要用户去手动释放锁,如果不释放锁,就有可能导致死锁。
- synchronized是非公平锁,ReentrantLock可以是公平锁也可以是非公平锁。
- ReetrantLock上等待获取锁的线程可以被中断,而synchronized不行。
- ReetranLock可以设置超时时间,超过时间后放弃获取锁,synchronized不行。
- ReentrantLock的tryLock方法可以尝试获取锁,如果获取不到,可以不用等待直接返回,synchronized不行。
ThreadLocal内存泄漏的原因
ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。ThreadLocalMap的key是弱引用,而value是强引用,如果弱引用的key在GC的时候发现没有其他强引用,就会被回收,但是value却不会,这样就会导致内存泄漏。
解决方法:每次使用完ThreadLocal,都调用它的remove方法,清除数据。
ThreadLocal的使用场景
- 用作保存每个线程独享的对象,为每个线程创建一个对象副本,各个线程之间互不干扰。这种情况下,ThreadLocal可以作为线程的一个工具类,用来保存线程的上下文信息,比如用户id,用户名称等。
- 每个线程内需要保存独立的信息,以便供其他方法更方便第获取该信息时,可以考虑将这些信息保存在ThreadLocal中。例如,事务管理器中的Connection,Session等。
AQS
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如常用的ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,SynchronousQueue等等。
AQS作为一个抽象类,通常通过继承来使用,本身是没有同步接口的,只是定义了若干同步状态获取和释放的方法来供自定义同步组件使用。AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
接口和抽象类的区别
- 概念上的区别:接口是对动作的抽象,它定义了对象应该具备的行为和功能,但并不关心这些功能的具体实现。抽象类则是对根源的抽象,它不仅定义了对象应该具备的行为和功能,还提供了这些功能的一些实现。
- 语法上的区别:在语法上,接口中只能包含抽象方法,而抽象类可以包含普通方法。此外,接口中不能定义静态方法,而抽象类可以。接口中只能定义静态常量属性,不能定义普通属性,而抽象类既可以定义普通属性,也可以定义静态常量属性。
- 继承和实现上的区别:一个类可以实现多个接口,但只能继承一个抽象类。这是因为接口中所有的方法都是抽象的,一个类可以实现多个接口来提供这些方法的具体实现。而抽象类是声明方法的存在而不去实现它的类,一个类继承一个抽象类后,需要继续实现抽象类中的抽象方法。
- 功能上的区别:接口中不包含构造器,抽象类有。抽象类中的构造器不是用于创建对象,而是让子类调用这些构造器来完成属于抽象类的初始化工作。此外,接口中不包含初始化块,而抽象类可以包含初始化块。
什么是CAS
CAS全称Compare and Swap,比较和交换,是乐观锁的主要实现方式,CAS在不使用锁的情况下实现多线程之间的变量同步,CAS操作包含三个操作数:内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B并返回true,否则什么都不做并返回false。
CAS存在的问题
- ABA问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个漏洞称为CAS操作的“ABA”问题。可以通过添加版本号的方式来解决ABA问题。
- 循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
- 只能保证一个共享变量的原子操作:对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
SyncronizedMap和ConcurrentHashMap的区别
SyncronizedMap一次锁住整张表来保证线程安全,所以每次只能有一个线程访问,性能低下。ConcurrentHashMap使用分段锁技术,每次只锁住一部分数据,多个线程可以同时访问,性能高。
在JDK1.8中,ConcurrentHashMap使用了CAS算法和Syncronized,不再使用Segment分段锁,而是使用Node+CAS+Synchronized来保证线程安全。数据结构使用数组+链表+红黑树,数组是主体,链表和红黑树是辅助,当链表长度超过8时,链表转换为红黑树。Syncronized只能锁住当前链表或红黑树的首节点,支持并发访问,修改。
select, poll, epoll的区别
- select,poll,epoll都是IO多路复用的机制,epoll是Linux下的,select和poll是Unix下的。本质都是同步IO,都是一个线程处理多个连接。他们都需要在读写时间就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步IO则不同,异步IO的读写过程是由内核完成的,阻塞只是将数据从内核拷贝到用户空间。
- select的时间复杂度是O(n),poll的时间复杂度是O(n),epoll的时间复杂度是O(1)。其中select仅仅只知道有I/O事件发生了,但是并不知道是哪几个流(可能有一个,多个,甚至全部),所以每次都需要轮询。poll和select基本上没有区别,只是采用了链表的方式,没有最大连接数的限制,原理都是一样的。epoll使用了回调的方式,当某个流有事件发生时,epoll会采用类似callback的回调机制,将事件对应的流放入就绪队列,唤醒进程,epoll_wait从就绪队列中取出流进行处理。
类加载器
主要作用就是加载Java类的字节码到JVM中,除了加载类以外,还可以加载Java应用所需的资源如文本,图像,配置文件,视频等。
类加载器的加载规则:
- 启动类加载器:加载JDK中的核心类库,如rt.jar,resources.jar,charsets.jar等。
- 扩展类加载器:加载JDK的扩展目录中的jar包。
- 应用程序类加载器:加载应用程序classpath目录下的jar包和class文件。
JVM判断两个Java类是否相同的方式:
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader必须相同。
双亲委派模型
双亲委派模型是指当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
双亲委派模型的好处
- 避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
- 保护程序安全,防止核心API被随意篡改。
程序计数器为什么是私有的
程序计数器的作用:
- 字节码解释器通过改变程序计数器来选取下一条需要执行的字节码指令。
- 多线程情况下,程序计数器用来记录当前线程执行的位置,从而当线程被切换回来的时候,能够知道该线程上次运行到哪里了。
所以程序计数器私有是为了线程切换后能恢复到正确的执行位置,如果是共享的,线程切换后就会出现错误。
虚拟机栈和本地方法栈为什么是私有的
虚拟机栈的作用:每二个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈的作用:为虚拟机使用到的Native方法服务。
为了保证线程中的局部变量不被其他线程访问到,所以虚拟机栈和本地方法栈是私有的。
并发和并行的区别
并发:两个及两个以上的作业在同一时间段内执行,但是任意时刻只有一个作业在CPU上执行。
并行:两个及两个以上的作业在同一时刻执行。
同步和异步的区别
同步:发出一个功能调用时,在没有得到结果之前,该调用就不返回,但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
异步:调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
什么是线程上下文切换
线程上下文切换是指CPU从一个线程中保存该线程的状态信息,然后加载另外一个线程的状态信息,这样就可以实现线程的切换。线程上下文切换是从操作系统层面来看的,所以线程上下文切换的开销是由操作系统来完成的,不同的操作系统线程上下文切换的开销是不一样的。
预防死锁的方法
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 按照顺序获取锁。
sleep和wait的区别
- sleep是Thread类的方法,wait是Object类的方法。
- sleep不会释放锁,wait会释放锁。
- wait需要被唤醒,sleep不需要。
- wait通常用于线程间交互,sleep通常用于暂停执行。
JMM内存模型
本质上是Java定义的并发编程相关的一组规范,除了抽象线程和主内存之间的关系外,还规定了从Java源代码到CPU可执行指令的这个转化过程中要遵守哪些原则和规范,目的是为了简化多线程编程,增强程序可移植性。
JMM的主内存和本地内存
主内存:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量,还是局部变量,类信息、常量、静态变量都是放在主内存中。为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
本地内存:每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。每个线程只能操作自己本地内存中的变量,无法直接访问其他线程的本地内存。如果线程间需要通信,必须通过主内存来进行。本地内存是 JMM 抽象出来的一个概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
ThreadLocal的数据结构
ThreadLocalMap,ThreadLocalMap是ThreadLocal的一个内部类,它是一个定制化的HashMap,key是ThreadLocal对象,value是线程的变量object。
ThreadLocalMap是由数组构成的。数组的每个元素Entry都是一个弱引用,key是ThreadLocal对象,value是线程的变量object。ThreadLocalMap的实现是开放地址法的线性探测法,也就是说,当一个ThreadLocal对象被GC回收后,它对应的Entry的key会被置为null,但是value不会被置为null,这样就会出现内存泄漏的问题,所以使用完ThreadLocal后,要调用remove方法,将Entry的value置为null,这样才能被GC回收。