集合
1.常用集合类
Collection接口下面的List和Set,Map接口下面的HashMap。ArrayList、LinkedList、HashSet、HashMap。都是线程不安全的,可以使用Collections.synchroinzedList/Set/map来使其变为安全的,但是效率不太高。一般JDK自带的安全的类,比如CopyOnWriteArrayList和CopyOnWriteArraySet还有ConcurrentHashMap。
2.ArrayList和LinkedList
ArrayList底层是数组,初始化的时候是一个空数组,在第一次add的时候会进行第一次扩容,长度是10,之后每add的时候都会对大小进行判断,如果当前长度减去数组的长度大于0的话,就进行扩容,扩容的话是当前容量加上当前容量右移一位,也就是1.5倍。然后copy函数将原数组的数据拷贝到新数组中,这个copy函数是调用的C++库函数。他的特点就是支持随机查找,但是插入删除节点的时候需要移动元素,所以效率不太高。
LinkedList底层是使用双向链表实现的,有一个头节点还有一个尾节点,所以提供两种插入方式,头插和尾插。他的特点就是插入和删除都是O(1)操作比较快,但是查找的话比较慢。
3.HashMap
实现Map接口,继承AbstractMap类。JDK1.7和JDK1.8有一些变化。1.7的时候采用数组+链表的数据结构,链表采用头插法,可能会导致循环引用问题。1.8的时候采用数组+链表+红黑树的数据结构,链表改用尾插法,避免了循环引用问题,当链表长度大于等于8并且哈希桶的数量大于64的时候,会将链表转换为红黑树,当链表长度小于6的时候,会从红黑树转换为链表。初始大小是16,负载因子是0.75,当容量达到负载大小的16*0.75=12的时候,会进行一个扩容,扩容大小为原来的两倍,然后会对原所有元素进行一次rehash。
4.HashMap的Put的过程
首先对添加的元素进行hash函数获取hash值,判断哈希桶是否为空,空的话先进行resize,建立16个哈希桶。
然后循环判断元素是否产生冲突,如果单链表,则循环遍历,如果key相同则覆盖,不相同则插入到最后;如果是红黑树,则在红黑树中进行查找,相同覆盖,不同找到合适点插入。
判断插入元素后,元素数量是否超越阈值,超越后进行扩容机制。
5.HashMap为什么线程不安全
HashMap中的put方法需要多个步骤,在中间步骤中其他线程可能抢占CPU资源,导致其操作还没有完成,其他线程就对HashMap进行了修改,导致覆盖,丢失等问题。
如果多个线程同时对其进行扩容时,会导致链表死循环现象。
在jdk1.7中,HashMap中冲突解决策略是链地址法,使用的是头插法处理的,可能会导致循环引用的问题。在jdk1.8以后采用的尾插法,解决了循环引用的问题,但是可能会导致插入丢失等问题。
所以在多线程中如果使用HashMap的话,可以通过以下方式解决。
- Collections.synchronizedMap()
- ConncurrentHashMap
6.红黑树
7.CopyOnWriteArrayList
在多线程中,如果使用传统的ArrayList可能会导致线程安全的问题,可以使用Vector或者Collections.synchronizedList去解决,但是都是对其进行了加sync关键字解决,虽然sync在jdk1.6以后有了很大的优化,但是锁一旦升级为重量级将不会降级,所以使用CopyOnWriteArrayList来解决这个问题。
采用采用读写分离的思想,将读和写操作分开来进行,读的时候不加锁,线程不会阻塞, 写的时候加锁,线程阻塞。复制思想:我们在向容器中添加一个元素的时候,不直接往容器中添加,而是将当前容器复制一份,在新的容器中添加,然后再将指针指向新的容器。8.HashMap扩容过程
判断是否需要扩容是在每一次put时候发生的,有一个负载因子多线程0.75,如果当前元素数量*0.75得到的阈值被超越时,就会触发扩容机制。
首先创建一个二倍的数组,然后将原数组中的key进行处理放入新数组中,然后调整相关参数。
key处理过程:jdk1.8进行优化,不需要对其进行重新hash,只需要比较最后一位是0或者1即可。如果0,则不需要调整位置,如果是1,就调整新的位置9.ConcurrentHashMap
1.7的时候底层使用的分段数组,有一个Segment内部类继承了ReentrantLock,分段锁来保证线程安全的。他是一个二级Hash表,不同的Segment管理一组Hash桶,如果要找到一个具体的值需要经过两次Hash。
1.8的时候采用数组+链表+红黑树来存储,放弃分段锁,采用CAS+Sycnhronized来保证线程安全的。10.ReentrantLock
Lock 底层是通过 AQS + CAS 机制来实现的。11.CAS
Compare And Swap,就是比较并交换,他是一种乐观锁,在进行一次修改操作的时候,会先将变量值取到线程的工作空间,然后线程工作空间进行修改,修改获取的时候,会先判断原来的值是否发生修改,如果发生变化则将最新数据拉去到工作空间进行修改,然后再进行设置,直到不重复的停止。底层采用UnSafe这个类来进行实现,其方法都是native本地方法,最终C++代码中是使用CPU指令cmpxchg配合一个Lock锁总线指令来实现的。
缺点:高并发会有多个线程处于忙等到状态,对CPU性能有消耗;还可能会产生ABA问题。11.1.ABA问题
线程1修改了地址A的值为B,在未进行修改回去的时候,有其他线程对其进行修改,从A->B->XX->A,也就是说期间可能有其他线程进行修改,但是最后的修改的值和原来的值一样,但是意思可能发生变化了,这个时候的解决方案就是可以使用AtomicMarkableReference来进行解决,就是添加了一个版本号来实现,如果对其修改就需要同时对版本号进行修改。12.AQS
- AQS在内部定义了一个volatile int修饰的state变量,表示同步状态:当线程调用lock方法时,如果state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将state=1;如果state=1,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。
- AQS通过Node内部类构成的一个双向链表结构的同步队列,来完成线程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。
- Node类是对要访问同步代码的线程的封装,包含了线程本身及其状态叫waitStatus(有五种不同取值,分别表示是否被阻塞,是否等待唤醒,是否已经被取消等),每个Node结点关联其prev结点和next结点,方便线程释放锁后快速唤醒下一个在等待的线程,是一个FIFO的过程。
- Node类有两个常量,SHARED和EXCLUSIVE,分别代表共享模式和独占模式。所谓共享模式是一个锁允许多条线程同时操作(信号量Semaphore就是基于AQS的共享模式实现的),独占模式是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待(如ReentranLock)。
- AQS通过内部类ConditionObject构建等待队列(可有多个),当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。
- AQS和Condition各自维护了不同的队列,在使用Lock和Condition的时候,其实就是两个队列的互相移动。
扩展:是怎么实现公平锁和非公平锁的
ReentrantLock有两个内部类,NonfairSync和FairSync,其中两个类最主要的是一个方法的不同,tryAcquire。
非公平锁的核心
lock 基于CAS尝试将state(锁数量)从0设置为1
A、如果设置成功,设置当前线程为独占锁的线程;
B、如果设置失败,还会再获取一次锁数量,
B1、如果锁数量为0,再基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;
B2、如果锁数量不为0或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒。
公平锁的核心
获取一次锁数量,
B1、如果锁数量为0,如果当前线程是等待队列中的头节点,基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;
B2、如果锁数量不为0或者当前线程不是等待队列中的头节点或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒。13.Java底层排序实现是什么
Java中Arrays.sort()方法是由插入排序,快速排序,归并排序三种排序的组合。
有两个阈值:QUICKSORT_THRESHOLD=286,如果不大于286,则使用归并排序。如果小于286,大于INSERTION_SORT_THRESHOLD=47,则使用快速排序的方式,如果小于47,则使用插入排序的。
多线程
1.Synchronized
是Java用于同步的一个关键字,可以应用于类、方法,同步代码块。类锁定的Class对象,方法锁定的是this,同步代码块锁定指定对象。早期sync是重量级锁,线程唤醒、挂起都需要用户态切换到内核态,成本比较高。JDK1.6进行优化,有了一个锁升级的过程,首先是无锁状态,然后是偏向锁,然后是轻量级锁,最后是重量级锁,并且锁升级的过程是不可逆的。
偏向锁的对象头信息Mark Word锁标志位为01,他会偏向于第一个获得锁的线程,会将线程的ID写到对象头信息中,该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需测试Mark Word里线程ID是否为当前线程。
如果发生线程争抢的时候会升级为轻量级锁,Mark Word锁标志位为00,线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间Lock Record,并将对象头的MarkWord复制到锁记录中,即Displaced Mark Word。然后线程会尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁。如果失败,表示其他线程在竞争锁,当前线程使用自旋来获取锁。当自旋次数达到一定次数时,锁就会升级为重量级锁。虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,并将 Lock Record 里的 owner 指针指向对象的 Mark Word。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁
重量级锁需要内核态的介入所以被称为重量级锁,重量级锁通过对象内部的监视器(monitor)实现,底层依赖操作系统的Mutex Lock实现。在同步代码块前会添加一个mointerenter,
在同步代码块后面会添加两个mointerexit。当有一个线程执行的时候,这个对象头信息默认是无锁01,他会先尝试获取锁对象,但是这个锁对象和另一个监视器锁对象monitor关联,他会将monitor的锁定器次数+1,并将这个monitor指针写入这个对象的头中并且修改锁对象的标志位为10,以此完成换锁的过程。在这个过程中他是一个可重入的,就是在进入的时候不需要加锁,只需要将计数器+1即可。当其他线程来尝试获取锁的时候会判断monitor中的计数器是否为0,不为0的话就会阻塞等待这个锁。
如果是同步方法的话他是一个ACC_SYNCHRONIZED标志位,当有线程执行方法的时候会先检查这个标志位,如果有的话就直接走向同步方法调用策略。
2.Synchronized和ReentrantLock区别
相同点:都是加锁方式同步,都是阻塞同步。
不同点:
sync是java的关键字,ReentrantLock是一个类。
sync使用简单,直接写在类、方法、同步块中即可,不用手动获取、释放锁;但是ReentrantLock需要自己Lock和unlock,需要配合try/finally实现。
ReentrantLock有一个高级特性:1.如果线程长期等待不到锁,可以进行锁释放;2.提供公平和非公平两种方式,默认非公平,sync只能是非公平。3.提供了condition,可以实现选择性通知的功能。
推荐使用ReentrantLock,因为sync的锁升级时不可逆的。
3.JUC并发工具
CountDownLatch 适用于单线程执行,一段实现并发执行,然后回归单线程的场景,也就是一个线程等待一批线程达到一个同步点以后在执行。是通过计数器实现的,每次计数器-1,他的计数器是不能重复使用的。计数器的实现是AQS+CAS实现,有一个volatile修饰的state。
CyclicBarrier 它允许一组线程互相等待,直到到达某个公共的屏障点 (common barrier point)在继续运行。可以使用reset方法修改初始值,但是CountDownLatch不能重置。
Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。 Semaphore 可以用来构建一些对象池,资源池之类的, 比如数据库连接池
4.volatile
是java的一个关键字,cpu的速度比内存快很多,所以在中间加了一层缓存cache,线程就是工作在这个基础之上的。
vloatile保证两点:可见性和禁止指令重排。
每个线程想要修改主内存的值,都是先将主存的值取到线程栈帧中,然后对其修改,刷新会主内存,其他线程能够马上看到修改的结果就是可见性。
volatile只能保证单次操作的原子性,不能保证i++这种类似的操作的原子性。想要保证i++原子性,加锁或者可以使用automic包下的工具类去实现。
扩展:JMM
扩展:MESI
扩展:总线嗅探机制/总线风暴
5.线程池
七大参数
- corePoolSize:指定了线程池中的线程数量。
- maximumPoolSize:指定了线程池中的最大线程数量。
- keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。
- unit: keepAliveTime 的单位。
- workQueue:任务队列,被提交但尚未被执行的任务。
- threadFactory:线程工厂,用于创建线程,一般用默认的即可。
- handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
执行流程:提交一个任务,判断核心线程数是否满了,没满创建线程执行,满了的话判断阻塞队列是否满了,没满的话加入阻塞队列中,满了的话判断线程池线程是否达到最大值,没有的话创建线程执行,满了的话根据拒绝策略进行处理。
默认四种线程池:
- FixedThreadPool:创建一个固定线程数的线程池,核心线程数和最大线程数固定相等。
- CachedThreadPool:corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大。阻塞队列使用SynchronousQueue没有存储空间
四种拒绝策略:
- AbortPolicy,直接抛出
- DiscardPolicy,丢弃这个任务
- DiscardOldestPolicy,丢弃最早未执行的任务
- CallerRunPolicy,将任务返回给调用线程池的线程去执行,这种会影响新任务的提交速度
阻塞队列:
- SynchronousQueue,不存储元素的阻塞队列。newCache线程池使用的这种队列。
- ArrayBlockingQueue,用数组实现的有界阻塞队列,FIFO原则对元素进行排序
- LinkedBlockingQueue,基于链表的阻塞队列,FIFO原则对元素进行排序,LinkedBlockingQueue 会默认一个类似无限大小的容量(Integer.MAX_VALUE)
- PriorityBlockingQueue,支持优先级的无界队列,通过compareTo函数比较两个元素的优先级
6.ThreadLocal
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
基础
1.细聊多态
多态性是指允许不同子类型的对象对同一消息作出不同的响应。
简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。
List list1 = new ArrayList();
List list2 = new LinkedList();
虽然list1和list2有同样的方法,但是调用完成的效果却是截然不同的。
另外父类重写子类的方法,同一个类种同一方法名不同参数的重载也是多态。
2.Java对象创建过程

①类加载检查: 虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。
②分配内存: 在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。分配⽅式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配⽅式由 Java 堆是否规整决定,⽽Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。
补充:内存分配并发问题:在创建对象的时候有⼀个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采⽤两种⽅式来保证线程安全:
- CAS+失败重试: CAS 是乐观锁的⼀种实现⽅式。所谓乐观锁就是,每次不加锁⽽是假设没有冲突⽽去完成某项操作,如果因为冲突失败就重试,直到成功为⽌。虚拟机采⽤ CAS 配上失败重试的⽅式保证更新操作的原⼦性。
- TLAB: 为每⼀个线程预先在Eden区分配⼀块⼉内存,JVM在给线程中的对象分配内存时,⾸先在TLAB分配,当对象⼤于TLAB中的剩余内存或TLAB的内存已⽤尽时,再采⽤上述的CAS进⾏内存分配
③初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值
④设置对象头: 初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。
⑤执⾏ init ⽅法: 在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从Java 程序的视⻆来看,对象创建才刚开始,
3.对象的访问定位有哪两种⽅式?
建⽴对象就是为了使⽤对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问⽅式有虚拟机实现⽽定,⽬前主流的访问⽅式有①使⽤句柄和②直接指针两种:
- 句柄: 如果使⽤句柄的话,那么Java堆中将会划分出⼀块内存来作为句柄池,reference 中存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃的具体地址信息;

- 直接指针: 如果使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,⽽reference 中存储的直接就是对象的地址。

使⽤句柄来访问的最⼤好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,⽽ reference 本身不需要修改。使⽤直接指针访问⽅式最⼤的好处就是速度快,它节省了⼀次指针定位的时间开销。
