进程和线程的区别?
- 进程是程序的一次执行,是系统进行资源分配和调度的一个独立单位。
- 线程是进程的一个实体,是CPU调度和分派的基本单位。它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
- 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
synchronized原理?
- synchronized是java提供的原子性内置锁,也被称为监视器锁。
- 使用synchronized之后,编译之后会在同步的代码块前后加上monitorenter和monitorexit字节码指令,它的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。
- 执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。
- 执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。
- synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁。
- 由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。
- 从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,而释放锁的过程则是将工作内存中的共享变量写回主内存。
锁的优化机制了解吗?
- 从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。
- 锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。
- 自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。
- 自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。
- 锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。
- 锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。
- 偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。
- 轻量级锁:JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。
- 简单点说,偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,而轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。
对象头具体都包含哪些内容?
- 在我们常用的Hotspot虚拟机中,对象在内存中布局实际包含3个部分:
对象头
实例数据
对齐填充 - 而对象头包含两部分内容,Mark Word中的内容会随着锁标志位而发生变化。
1)对象自身运行时所需的数据,也被称为Mark Word,也就是用于轻量级锁和偏向锁的关键点。具体的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程ID、偏向锁时间戳。
2)存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。
3)如果是数组的话,则还包含了数组的长度。
ReentrantLock原理?他和synchronized有什么区别?
相比于synchronized,ReentrantLock需要显式的获取锁和释放锁,他们的主要区别有以下几点:
- 等待可中断,当持有锁的线程长时间不释放锁的时候,等待中的线程可以选择放弃等待,转而处理其他的任务。
- 公平锁:synchronized和ReentrantLock默认都是非公平锁,但是ReentrantLock可以通过构造函数传参改变。只不过使用公平锁的话会导致性能急剧下降。
- 绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象。
ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现。
- AQS内部维护一个state状态位,尝试加锁的时候通过CAS(CompareAndSwap)修改值,如果成功设置为1,并且把当前线程ID赋值,则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空。
AQS
- 技术解释:
- 相关的锁:
- 源码解析
CPU负载和CPU利用率的区别是什么?
CAS是什么?
CAS的缺点
HashMap的原理
- HashMap主要由数组和链表组成,他不是线程安全的。核心的点就是put插入数据的过程,get查询数据以及扩容的方式。JDK1.7和1.8的主要区别在于头插和尾插方式的修改,头插容易导致HashMap链表死循环,并且1.8之后加入红黑树对性能有提升。
- put插入数据流程:
- get查询数据:
查询数据相对来说就比较简单了,首先计算出hash值,然后去数组查询,是红黑树就去红黑树查,链表就遍历链表查询就可以了。 - resize扩容过程
扩容的过程就是对key重新计算hash,然后把数据拷贝到新的数组。
多线程环境怎么使用Map呢?ConcurrentHashmap了解过吗?
- 多线程环境可以使用Collections.synchronizedMap同步加锁的方式,还可以使用HashTable,但是同步的方式显然性能不达标,而ConurrentHashMap更适合高并发场景使用。
- ConcurrentHashmap在JDK1.7和1.8的版本改动比较大,1.7使用Segment+HashEntry分段锁的方式实现,1.8则抛弃了Segment,改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表过长导致性能的问题。
- 1.7分段锁
从结构上说,1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment数组,Segment继承与ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是一个链表的结构,具有保存key、value的能力能指向下一个节点的指针。
实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程的并发写,Segment之间相互不会受到影响。
put流程
其实发现整个流程和HashMap非常类似,只不过是先定位到具体的Segment,然后通过ReentrantLock去操作而已。
1)计算hash,定位到segment,segment如果是空就先初始化。
2)使用ReentrantLock加锁,如果获取锁失败则尝试自旋,自旋超过次数就阻塞获取,保证一定获取锁成功。
3)遍历HashEntry,就是和HashMap一样,数组中key和hash一样就直接替换,不存在就再插入链表,链表同样。
get流程
get也很简单,key通过hash定位到segment,再遍历链表定位到具体的元素上,需要注意的是value是volatile的,所以get是不需要加锁的。 - 1.8CAS+synchronized
1.8抛弃分段锁,转为用CAS+synchronized来实现,同样HashEntry改为Node,也加入了红黑树的实现。主要还是看put的流程。
put流程
1)首先计算hash,遍历node数组,如果node是空的话,就通过CAS+自旋的方式初始化
2)如果当前数组位置是空则直接通过CAS自旋写入数据
3)如果hash==MOVED,说明需要扩容,执行扩容
4)如果都不满足,就使用synchronized写入数据,写入数据同样判断链表、红黑树,链表写入和HashMap的方式一样,key hash一样就覆盖,反之就尾插法,链表长度超过8就转换成红黑树
get查询
get很简单,通过key计算hash,如果key hash相同就返回,如果是红黑树按照红黑树获取,都不是就遍历链表获取。
volatile原理知道吗?
- volatile是Java虚拟机提供的轻量级的同步机制,是基本上遵守了JMM的规范,主要是保证可见性和禁止指令重排,但是它并不保证原子性。
- 可见性:
- 禁止指令重排:
- 线程安全性的保证:
- volatile的使用场景:
单例模式DCL代码,可以保证多线程下的安全问题。如下所示:
public class SingletonDemo {
private static volatile SingletonDemo instance=null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
/**
* 双重检测机制
* @return
*/
public static SingletonDemo getInstance(){
if(instance==null){
synchronized (SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 1; i <=10; i++) {
new Thread(() ->{
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
说说你对JMM内存模型的理解?为什么需要JMM?
- 到底工作内存和主内存是什么?
主内存可以认为就是物理内存,Java内存模型中实际就是虚拟机内存的一部分。而工作内存就是CPU缓存,他有可能是寄存器也有可能是L1\L2\L3缓存,都是有可能的。
如何保证i++的线程安全?
ThreadLocal原理?
- 购物车微服务:用来保存用户信息。
引用类型有哪些?有什么区别?
面试-JVM
线程池原理?拒绝策略?
JUC-线程池