Java中有几种方式来创建线程执行任务

可以说有一种也可以说有四种,一种是因为Java中创建线程执行的方式本质上都是使用Runnable接口实现;
而四种分别是:

  • 继承Thread类来重写run方法实现
  • 实现Runnable接口重写run方法
  • 实现Callable接口使用futureTast实现call方法来创建线程,这种可以返回线程执行结果
  • 通过线程池创建线程

为什么不建议使用Executors来创建线程池

Executors创建线程池的方式是调用LinkedBlockingQueue,它是一个无界阻塞队列,在线程执行任务时,可能会导致任务过多而不停的添加至队列中,而导致系统内存耗尽,最终导致OOM。
而SingleThreadExector同样是调用LinkedBlockingQueue,所以建议使用ThreadPoolExector来定义线程池

线程池有几种状态?每种状态分别代表什么意思?

  • RUNNING 正常运行
  • SHUTDOWN shutdown() 不会接受新任务,但继续执行
  • STOP shutdownnow() 停止运行
  • TIDYING terminated() 线程池没有线程运行,进入空方法terminated()
  • TERMINATED terminated()执行完

Sychronized和ReentrantLock有那些区别?

Sychronized

  • Java中的一个关键字
  • 自动加锁与释放锁
  • JVM层面的锁
  • 非公平锁
  • 锁的是对象,锁信息保存在对象头中
  • 底层有锁升级过程

ReentrantLock

  • JDK提供的一个类
  • 需要手动加锁与释放锁
  • API层面的锁
  • 公平锁或非公平锁
  • int类型的state标识来标识锁的状态
  • 没有锁升级过程

CAS

CAS是乐观锁,线程执行的时候不会加锁,假设没有冲突去完成某项操作,如果因为冲突失败了就重试,最后直到成功为止。
compare and swap多线程访问,在没有加锁的情况下,保证线程一致性的去改变某个值,比如有个变量初始值0,第一个线程读取过来,想加1,在把1往回写的时候需要先去读最新的值,看看还是不是0,如果是,则把值改成1,如果原来的值已经被别的线程动了,改成2了,那么此时cas失败,值还是2。此时第一个线程虽然cas失败了,但是并不会被挂起,而是自旋,他会把最新的2读取,然后+1,再把3写回去的时候依然去判断原来的值是否被别的线程改变,如果改变了继续重复上述步骤。

ThreadLocal有哪些应用场景?它底层是如何实现的?

ThreadLocal是JAVA内部提供的一个线程本地储存机制,可以将数据缓存到线程内部,应用于多个类传递数据时的场景,底层是通过ThreadLocalMap实现的,每一个Thread对象都存在一个ThreadLocalMap,其对象名为Map的Key,缓存的数据为Value,但要注意的是ThreadLocal内部使用的是强引用指向的Map,导致使用完以后不会自动回收内存,需要在应用场景结束后手动remove,手动清除Entry对象

ReentrantLock分为公平锁和非公平锁,那底层分别是如何实现的?

首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于线程在使用lock()方法加锁时:

  1. 如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队
  2. 如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。
    另外,不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。

Sychronized的锁升级过程是怎样的?

  1. 偏向锁:在锁对象的对象头中记录当前获取到的锁的线程ID,线程下次来获取直接获取;
  2. 轻量级锁:由偏向锁升级而来,在有一个线程获取锁以后,另一个线程来竞争锁,会导致锁进入自旋状态;
  3. 重量级锁:在多次自旋无果以后依旧没有获取到锁,会从轻量级升级为重量级锁,导致线程阻塞;
  4. 自旋锁:自旋就是在线程获取锁的时候,不会去阻塞线程,而是通过CAS不停获取标记,省去了阻塞和唤醒两个消耗系统资源的步骤,因为CAS是不停的循环获取标记,直到获取成功。

Tomcat中为什么要使用自定义类加载器?

因为Tomcat中可以部署多个应用,而多个应用中可能会出现同名的类,尽管功能不同,但Tomcat启动以后是作为一个进程存在的,所以进程中不允许出现同类名,所以要为每一个应用都生成一个类加载实例WebAppClassLoarder,不同的类加载器可以隔离每个应用的同名类,同时自定义类加载器可以实现热加载。

什么是面向对象

面向对象是一种开发方式,相比与面向过程的连续,它更多的注重于对象本身以及对象所要进行的操作,将相关的数据与方法组合成一个整体来看,虽然不如面向过程简单便捷,但更易于维护与管理。
面向对象的三个特性

  1. 封装:对象中的数据与代码可以被封装起来,私有数据与代码不给外部访问,公有的则是内部给外部留下使用方法,对象的内部细节外部无需了解,外部的调用也无需关心与修改内部实现;
  2. 继承:子类可以继承父类的方法,好处是无需对基类的通用方法重复造轮子,同时可以在子类自定义及扩展自己需要的方法,重写是例子;
  3. 多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同,多种逻辑可以用一个方法调用实现,构造方法重载是例子。

JDK、JRE、JVM的区别

  • JVM 虚拟机
  • JRE 运行环境 lib类库与jvm
  • JDK 开发环境 包含JRE与java工具
  • Servlet 经典web开发
  • Spring web开发脚手架(框架) 配置地狱
  • Spring boot 大部分用默认配置代替的优化版Spring
  • Swagger 多人开发中的日志 与前端接口交流
  • Redis 缓存数据库
  • Mybatis 代替JDBC的数据库整合
  • Druid alibaba的数据库监管
  • Shiro || Security 用户名密码验证安全框架
  • Thymeleaf 前端模板
  • zookeeper 分布式的注册中心 需要Dubbo去连接
  • Dubbo 分布式开发的工具 可以让B去注册中心使用A注册的服务 负载均衡

final的作用

  • 修饰类时,类不可被继承
  • 修饰方法时,不可被子类覆盖,但可以重载
  • 修饰变量时,赋值以后不可改变
    • 成员变量:
      • 类变量,只能在静态初始块中指定初始值 或声明变量指定初始值;
      • 成员变量,可以在非静态初始块中初始化或声明变量或构造器初始化;
    • 局部变量:系统不会初始化,需要手动指定,只要在使用前指定默认值即可;
    • 基本数据类型:赋值不可更改;
    • 引用类型:引用对象不可更改,但引用的值可以变;

      为什么局部内部类和匿名内部类只能访问局部final变量?

  • 内部类:写在一个类中的独立类,可以调用外部类数据及代码,外部类需要内部类对象来访问成员;
  • 匿名内部类:只使用一次的匿名内部类,继承父类或实现父类接口,将使用接口的定义子类,重写接口方法,new子类对象,调用重写后方法整合成一个步骤:匿名内部类;
  • 局部内部类:局部内部类与局部变量一样,不能使用访问控制修饰符(public、private 和 protected)和 static 修饰符修饰。局部内部类只在当前方法中有效。局部内部类中只可以访问当前方法中 final 类型的参数与变量。

首先需要知道的一点是:内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。
这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的”copy”。这样就好像延长了局部变量的生命周期
将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?
就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类内建立的拷贝保持一致。
总结:本质上是因为内部类在运行结束外部类销毁时,内部类不会因为外部类的销毁而不存在,而内部类依旧存在的情况下仍然在调用之前使用的局部变量,但是变量已经随着方法的销毁而不存在了,所以内部类使用的本质上是变量的copy,所以需要保证两个变量的值相同,使用final关键字使变量的值不会更改。

List和Set的区别

  • List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用lterator取出所有元素,在逐—遍历,还可以使用get(int index)获取指定下表的元素;
  • Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用lterator接口取得所有元素,在逐一遍历各个元素

HashMap的扩容机制

HashMap的扩展原理是HashMap用一个新的数组替换原来的数组。重新计算原数组的所有数据并插入一个新数组,然后指向新数组。如果阵列在容量扩展前已达到最大值,阈值将直接设置为最大整数返回。
hashMap扩容就是重新计算容量,向hashMap不停的添加元素,当hashMap无法装载新的元素,对象将需要扩大数组容量,以便装入更多的元素。

  • 1.7版本
  1. 先生成新数组
  2. 遍历老数组中的每个位置上的链表上的每个元素
  3. 取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
  4. 将元素添加到新数组中去
  5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
  • 1.8版本(新增了红黑树)
  1. 先生成新数组
  2. 遍历老数组中的每个位置上的链表或红黑树
  3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
  4. 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
    a. 统计每个下标位置的元素个数
    b. 如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置
    c.如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置
    5.所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

HashMap和HashTable的区别? 底层实现是什么?

  1. 区别:
    (1) HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;
    (2) HashMap允许key和value为null,而HashTable不允许
  2. 底层实现:数组+链表实现
    jdk8开始链表高度到8、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在;
  • 计算key的hash值,二次hash然后对数组长度取模,对应到数组下标,
  • 如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组,
  • 如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表
  • key为null,存在下标0的位置数组扩容

ArrayList和LinkedList区别

  • ArrayList:基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚至超过linkedList(需要创建大量的node对象);
  • LinkedList:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过geti取得某一元素时都需要对list重新进行遍历,性能消耗极大。另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexlof对list进行了遍历,当结果为空时会遍历整个列表。

ConcurrentHashMap的扩容机制

1.7版本: ConcurrentHashMap是由一个个Segment对象实现,而扩容则是对在Segment对象中的数组实现扩容操作;

  1. 1.7版本的ConcurrentHashMap是基于Segment分段实现的
  2. 每个Segment相对于一个小型的HashMap
  3. 每个segment内部会进行扩容,和HashMap的扩容逻辑类似
  4. 先生成新的数组,然后转移元素到新数组中
  5. 扩容的判断也是每个segment内部单独判断的,判断是否超过阈值
    1.8版本: ConcurrentHashMap不再基于Segment实现,而是线程生成数组扩容,且支持多个线程同时扩容,扩容前会先判断是否正在进行扩容操作,如果没有,先将key-value放入ConcurrentHashMap中再判断是否超过阀值。
  6. 1.8版本的ConcurrentHashMap不再基于Segment实现
  7. 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
  8. 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
  9. ConcurrentHashMap是支持多个线程同时扩容的
  10. 扩容之前也先生成一个新的数组
  11. 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作

ConcurrentHashMap原理,jdk7和jdk8版本的区别

jdk7:
数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构;

元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部;

锁: Segment分段锁Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segment;

get方法无需加锁,volatile保证
jdk8:
数据结构: synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性查找,替换,赋值操作都使用CAS;

锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容;

读操作无锁:
Node的val和next使用volatile修饰,读写线程对该变量互相可见数组用volatile修饰,保证扩容时被读线程感知

volatile:
volatile是Java中的关键字,用来修饰会被不同线程访问和修改的变星。JMM(Java内存模型)是围绕并发过程中如何处理可见性、原子性和有序性这3个特征建立起来的,而volatile可以保证其中的两个特性。

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排(保证有序性)

CopyOnWriteArrayList的底层原理是怎样的

因为ArrayList本身线程不安全的缘故,并发过程中会导致数据丢失,使用CopyOnWriteArrayList;

  1. 首先CopyOnWiteArrayList内部也是用过数组来实现的,在向CopyOnWriteAraylist添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行
  2. 并且,写操作会加锁,防止出现并发写入丢失数据的问题
  3. 写操作结束之后会把原数组指向新数组
  4. CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArraylit会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景

String、 StringBuffer、StringBuilder的区别

  1. String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的
  2. StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下stringBuilder效率会更高
    总结:区别就是String是字符串,StringBuffer和StringBuilder是操作字符串,但是StringBuffer线程安全,有锁Synchronized。

String、StringBuffer、StringBuilder区别及使用场景

  • string是final修饰的,不可变,每次操作都会产生新的String对象
  • stringBuffer和stringBuilder都是在原对象上操作
  • stringBuffer是线程安全的,StringBuilder线程不安全的
  • stringBuffer方法都是synchronized修饰的
  • 性能: StringBuilder > stringBuffer > string
  • 场景:经常需要改变字符串内容时使用后面两个
  • 优先使用StringBuilder,多线程使用共享变量时使用StringBuffer

阿里二面:Jdk1.7到Jdk1.8 HashMap发生了什么变化(底层)?

  1. 1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率;数组超过64,链表超过8就会启用红黑树
  2. 1.7中链表插入使用的是头插法,1.8中链表插入使用的是尾插法,因为1.8t中插入key和Vale时需要判断链表示素个数,所以需要遍历锥表统计铤表元索个数,所以正好就直接使用尾插法;
  3. 1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为负载的哈希算法的目的就是提高散列性,来提供HasthMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPu资源。

阿里二面:Jdk1.7到Jdk1.8 java虚拟机发生了什么变化?

1.7中存在永久代,1.8中没有永久代,替换它的是元空间,元空间所占的内存不是在虚拟机内部,而是本地内存空间,这么做的原因是,不管是永久代还是元空间,他们都是方法区的具体实现,之所以元空间所占的内存改成本地内存,官方的说法是为了和Rocit统一,不过额外还有一些原因,比如方法区所存储的类信息通常是比较难确定的,所以对于方法区的大小是比较难指定的,太小了容易出现方法区溢出,太大了又会占用了太多虚拟机的内存空间,而转移到本地内存后则不会影响虚拟机所占用的内存

阿里一面:说一下HashMap的Put方法

先说HashMap的Put方法的大体流程;
1.根据Key通过哈希算法与与运算得出数组下标
⒉如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是Node对象)并放入该位晋
3.如果数组下标位置元素不为空,则要分情况讨论
a.如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entry对象,并使用头插法添加到当前位置的链表中b.如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红黑树Node,还是链表Node
b.如果是红黑树Node,则将key和value封装为一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value
c.如果此位置上的Node对象是链表节点,则将key和value封装为一个链表Node并通过尾插法插入到镞表的最后位置去,因为是尾插法,所以需要遍历镞表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完)表后,将新链表Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果超过了8,那么则会将该链表转成红黑树
d.将key和value封装位Node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就介绍PUT方法
总结:本质上就是key根据哈希算法和与算法得出数组下标,无元素存放就封装为1.7:entry对象或1.8:node对象直接放进去,有元素存放1.7直接头插法,1.8则是需要判断链表与红黑树:

  • 红黑树就封装为红黑树节点放进去,同时判断是否存在当前key来更新value;
  • 链表封装成链表node尾插法放入,尾插法需要遍历,同时判断是否存在当前key来更新value,放完以后判断链表数量是否超过8,超过则转为红黑树
  • 最后再判断一边是否扩容

重载和重写的区别

重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。
总结:

  • 重载的参数一定要不同,可以是个数,次序,类型;
  • 重写与父类的方法名参数都要一样,返回值与异常小于等于,访问修饰符大于等于,private禁止重写;

ReentrantLock中tryLock()和lock()方法的区别

  • tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false,通常用于自旋锁;
  • lock()表示阻塞加锁。线程会阻塞直到加到锁,方法也没有返回值;

sleep()、wait()、join()、yield()的区别

  1. 锁池
    所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配。
  2. 等待池
    当我们调用wait ()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify ()或notifyAll)后等待池的线程才会开始去竞争锁,notify ()是随机从等待池选出一个线程放到锁池,而notifyAllp)是将等待池的所有线程放到锁池当中

sleep和wait的区别

  1. sleep是Thread类的静态本地方法,wait则是Object类的本地方法。
  2. sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
  3. sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
  4. sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
  5. sleep一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。
  6. sleep会让出CPU执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行的。

yield () 执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行。

join () 执行后线程进入阻塞状态,例如在线程B中调用线程A的join (),那线程B会进入到阻塞队列,直到线程A结束或中断线程

总结:

  • sleep是直接强制休眠线程直到指定的时间结束
  • wait是让线程进入等待池直到手动notify唤醒或者设置时间
  • yield是进入就绪状态,跟sleep差不多强制释放cpu但是保留了接下来可能获取cpu执行的权利
  • join是强制一个线程优先插队到另一个线程中,直到插队线程结束或中断

ThreadLocal的底层原理

  1. ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据绥存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLlocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
  3. 如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用Threadlocll的remove方法,手动清楚Entry对象
  4. ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)

ThreadLocal的原理和使用场景

每一个Thread对象均含有一个ThreadLoca1Map类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及其对应的值

ThreadLoca1Map由一个个Entry对象构成

Entry继承自weakReference>,一个Entry由 ThreadLoca1对象和object构成。由此可见,Entry的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收

当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。

get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。

由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。

使用场景:

  1. 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  2. 线程间数据隔离
  3. 进行事务操作,用于存储线程事务信息。

ThreadLocal内存泄露原因,如何避免

内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空问不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。

如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使VM在合适的时间就会回收该对象。

弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本
threadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null,而value还存在着强引用,只有thread线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为nul的Entry的value就会一直存在一条强引用链;

key使用强引用
当threadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

key使用弱引用
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(), remove()方法的时候会被清除value值。

因此,,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal正确的使用方法

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

阿里一面:如何查看线程死锁

  1. 可以通过jstack命令来进行查看,jstack命令中会显示发生了死锁的线程
  2. 或者两个线程去操作数据库时,数据库发生了死锁,这是可以查询数据库的死锁情况

阿里一面:线程之间如何进行通讯的

  1. 线程之间可以通过共享内存或基于网络来进行通信
  2. 如果是通过共享内存来进行通信,则需要考虑并发问题,什么时候阻塞,什么时候唤醒
  3. 像Java中的wait0、notify0就是阻塞和唤醒
  4. 通过网络就比较简单了,通过网络连接将通信数据发送给对方,当然也要考虑到并发问题,处理方式就是加锁等方式

并发、并行、串行的区别

  • 串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着 串联
  • 并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行。 并联
  • 并发允许两个任务彼此干扰。统一时间点、只有一个任务运行,交替执行 并联加开关

京东二面:并发编程三要素?

  1. 原子性:不可分割的操作,多个步骤要保证同时成功或同时失败
  2. 有序性:程序执行的顺序和代码的顺序保持—致
  3. 可用性:—个线程对共享变量的修改,另个线程能立马看到

Java死锁如何避免?

造成死锁的几个原因:

  1. 一个资源每次只能被一个线程使用
  2. —个线程在阻塞等待某个资源时,不释放已占有资源
  3. —个线程已经获得的资源,在未使用完之前,不能被强行剥夺4.若干线程形成头尾相接的循环等待资源关系

这是造成死锁必须要达到的4个条件,如果要避兔死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为救要符合的条件,所以要避兔死赖僦需要打破第4个条件,不出现循环等待锁的关系。

在开发过程中:

  1. 要注意加锁顺序,保证每个线程按同样的顺序进行加锁
  2. 要注意加锁时限,可以针对所设置一个超时时间
  3. 要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决

volatile关键字,他是如何保证可见性,有序性

  1. 对于加了iuolatle关键字的成员变量,在对这个变量进行修改时,会直接将CPU高级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性
  2. 在对volatile修饰的成员变量进行读写时,会插入内存屏障,而内存屏障可以达到禁止重排序的效果,从而可以保证有序性

线程池的底层工作原理

线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:

  1. 如果此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
  3. 如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueuc满,并且线程池中的数量等于maximumPoolSize,那么通过handler新指定的策略来处理此任务。
  5. 当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数

JAVA类加载

JAVA类加载的全过程是怎样的?

  • JAVA的类加载器:父子关系AppClassLoader <- ExtClassLoader <- Bootstrap classloader
  • Bootstrap classloader 加载java基础类。默认负责加载%JAVA_HOME%lib下的jar包和class文件;
  • Extention classloader 加载JAVA_HONE/ext下的jar包和class类。可通过-D java.ext.dirs另行指定目录;
  • AppClassLoader 加载CLASSPATH应用下的Jar包,是自定义加载器的父类。可通过-D java.class.path另行指定目录,是系统类加载器,线程上下文加载器;
  • BootStrap classloader 由C++开发,是JVA虚拟机的一部分,本身不是JAVA类。
  • string , Int等基础类出BootStrap classloader加载。

类加载过程:加载-》连接-》初始化
加载:把Java的字节码数据加载到JVM内存当中,并映射成JVM认可的数据结构。
连接:分为三个小的阶段:

  1. 验证:检查加载到的字节信息是否符合JVM规范。
  2. 准备:创建类或接口的静态变量,并赋初始值半初始化状态
  3. 解析:把符号引用转为直接引用
    初始化: 与JVM机制无关了,是自己的代码执行过程。

什么是双亲委派机制?

JAVA的类加载器: AppClassloader -> ExtClassloader ->BootStrap Classloader
每种类加载器都有他自己的加载目录。
JAVA中的类加载器: AppClassLoader, ExtClassLoader -> URLClassLoader ->SecureClassLoader -> ClassLoader
每个类加载器对他加载过的类,都是有一个缓存的。
双亲委派:向上委托在缓存中查找直到最底层的Bootstrap classloader,还没有找到就向下委托在加载器的路径下查找加载直到返回classpath路径。

有什么作用?

作用:保护JAVA底层的类不会被应用程序覆盖。

一个对象从加载到JVM,再到被GC清除,都经历了什么过程?

  1. 用户创建一个对象,首先通过内存中class point找到方法区中的类型信息(元数据区中的class)。
  2. 然后在JVM中实例化对象,在堆中开辟空间,半初始化对象(存在指令重排)。
  3. 对象会分配在堆内存中新生代Eden.然后经过一次Minor GC,对象如果存活就会进入s区,在后续每次GC中,如果对象一直存活就会在S区来回拷贝,每移动一次年龄加一,最大年龄为15,超过一定年龄就会移入老年代。
  4. 直到方法执行结束后栈中指针先移除掉﹔
  5. 堆中的对象经过Full GC,则会被标记为垃圾,然后被GC进程清除。

JAVA内存模型

code

  • JVM Stack:存放执行java方法的栈帧;
  • Native Stack:存放执行本地方法的栈帧;
  • 程序计数器:每一个线程执行到哪一步,在JVM Stack记录指令的内存地址,在Native Stack中永远为0;
  • 栈:定义变量的指针,每执行一个java方法都会生成一个栈帧
  • 堆:存放所有的对象数组;
  • 新生代:占堆内存的三分之一,默认对象年龄达到15以后进入老年代;
  • 老年代:占堆内存的三分之二;
  • Eden:占新生代的十分之八;
  • Survivor:分S1,S2分别十分之一,
  • 元数据区(MetaSpace):存放常量、静态变量等,1.8以前属于堆内存,1.8以后移出到操作系统中;
  • DirectBuffer:JDK1.4的NIO中引用,调用native本地的方法去操作JVM以外的操作系统的内存。

JVM有哪些垃圾回收器?他们都是怎么工作的?什么是STW?他都发生在哪些阶段?什么是三色标记?如何解决错标记和漏标记的问题?为什么要设计这么多的垃圾回收器?

STW: Stop-The-World。是在垃圾回收算法执行过程当中,需要将JVM内存冻结的一种状态。在STW状态下,JAVA的所有线程都是停止执行的-GC线程除外,native方法可以执行,但是不能与JVM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。

  • 分代算法
    • Serial:串行 整体过程简单,需要GC时暂停,GC结束后继续运行;属于早期垃圾回收期,只有一个线程执行,多CPU架构下性能严重不足。
    • Parallel:并行 在串行的基础上,增加多线程GC。PS+PO组合是1.8默认的垃圾回收器。多CPU架构下,性能更高。
    • CMS:Concurrent Mark Sweep 核心思想是将STW打散,让一部分GC线程与用户线程并发执行,整个GC过程分为四个部分:
      • 初始标记阶段:STW只标记出根对象直接引用的对线。
      • 并发标记:继续标记其他对象,与应用程序是并发执行。
      • 重新标记:STW对并发执行阶段的对象进行重新标记。
      • 并发清除:并行。将产生的垃圾清除。清除过程中,应用程序又会不断产生新的垃圾,叫做浮动垃圾,留到下一次GC过程中清除。
  • 不分代算法
    • G1 Garbage First 垃圾优先:他的内存模型是实际不分代,但是逻辑上是分代的。在内存模型中,对于堆内存就不再分老年代和新生代,而是划分成一个一个的小内存块,叫做Region。每个Region可以隶属于不同的年代。GC分为四个阶段:
      • 第一:初始标记标记出GCRoot直接引用的对象。STW
      • 第二:标记Region,通过RSet标记出上一个阶段标记的Region引用到的Old区Region,
      • 第三∶并发标记阶段:跟CMS的步骤是差不多的。只是遍历的范围不再是整个Old区,而只需要遍历第二步标记出来的Region。
      • 第四:重新标记:跟CMK中的重新标记过程是差不多的。
      • 第五:垃圾清理:与CMS不同的是,G1可以采用拷贝算法,直接将整个Region中的对象拷贝到另一个Region。而这个阶段,G1只选择垃圾较多的Region来清理,并不是完全清理。|
    • shennandoah:G1机制类似,了解即可。
    • ZGC:与内存大小无关,完全不分代。

CMS的核心算法就是三色标记。
三色标记:是一种逻辑上的抽象。将每个内存对象分成三种颜色:黑色:表示自己和成员变量都已经标记完毕。灰色:自己标记完了,但是成员变量还没有完全标记完。白色:自己未标记完。

怎么确定一个对象到底是不是垃圾?什么是GC Root?

有两种定位垃圾的方式:

  1. 引用计数:这种方式是给堆内存当中的每个对象记录一个引用个数。引用个数为O的就认为是垃圾。这是早期JDK中使用的方式。引用计数无法解决循环引用的问题。
  2. 根可达算法:这种方式是在内存中,从引用根对象向下一直找引用,找不到的对象就是垃圾。
    哪些是GC
    Root? Stack ->JVM Stack, Native Stack,class类,run-time constant pool 常量池, static reference静态变量。

JVM有哪些垃圾回收算法?

  • MarkSweep标记清除算法:
    这个算法分为两个阶段,标记阶段:把垃圾内存标记出来,清除阶段:直接将垃圾内存回收。这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片,因为
  • Copying拷贝算法:
    为了解决标记清除算法的内存碎片问题,就产生了拷贝算法。拷贝算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。
    这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存货对象的个数有关。
  • MarkCompack标记压缩算法:
    为了解决拷贝算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将端边界以外的所有内存直接清除。
    这三种算法各有利弊,各自有各自的适合场景。

JVM中哪些可以作为gc root

什么是gc root,NM在进行垃圾回收时,需要找到“垃圾”对象,也就是没有被引用的对象,但是直接找“垃圾”对象是比较耗时的,所以反过来,先找“非垃圾”对象,也就是正常对象,那么就需要从某些“根”开始去找,根据这些“根”的引用路径找到正常对象,而这些“根”有一个特征,就是它只会引用其他对象,而不会被其他对象引用,例如:栈中的本地变量、方法区中的静态变量、本地方法栈中的变量、正在运行的线程等可以作为gc root。

如何进行JVM调优?JVM参数有哪些?怎么查看一个JAVA进程的JVM参数?谈谈你了解的JVM参数。如果一个java程序每次运行一段时间后,就变得非常卡顿,你准备如何对他进行优化?

JVM调优主要就是通过定制JVM运行参数来提高JAVA应用程度的运行数据
JVM参数大致可以分为三类:

  1. 标注指令:-开头,这些是所有的HotSpot都支持的参数。|
  2. 非标准指令:-X开头,这些指令通常是跟特定的HotSpot版本对应的。可以用java -X打印出来。
  3. 不稳定参数:-XX开头,这一类参数是跟特定HotSpot版本对应的,并且变化非常大。详细的文档资料非常少。
    java -XX:+PrintCommandLineFlags :查看当前命令的不稳定指令。
    java -XX:+PrintFlagslnitial :查看所有不稳定指令的默认值。
    java -XX:+PrintFlagsFinal:查看所有不稳定指令最终生效的实际值。

什么是字节码?采用字节码的好处是什么?

java中的编译器和解释器:

  • Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。
  • 编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。
  • 每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了Java的编译与解释并存的特点。
  • Java源代码——>编译器—->jvm可执行的Java字节码(即虚拟指令)—->jvm—->jvm中解释器——>机器可执行的二进制机器码——>程序运行。
    采用字节码的好处:

Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。