1、简述一下JVM加载class文件的原理机制。
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
1.1、类装载方式
类装载方式,有两种 :
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
2.显式装载, 通过class.forname()等方法,显式加载需要的类
隐式加载与显式加载的区别:两者本质是一样?
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
1.2、Java的类加载器
Java的类加载器有三个,对应Java的三种类:(java中的类大致分为三种: 1.系统类 2.扩展类 3.由程序员自定义的类 )
Bootstrap Loader // 负责加载系统类 (指的是内置类,像是String,对应于C#中的System类和C/C++标准库中的类)
|
- - ExtClassLoader // 负责加载扩展类(就是继承类和实现类)
|
- - AppClassLoader // 负责加载应用类(程序员自定义的类)
三个加载器各自完成自己的工作,但它们是如何协调工作呢?哪一个类该由哪个类加载器完成呢?为了解决这个问题,Java采用了委托模型机制。
1.3、类加载器工作原理
委托模型机制的工作原理很简单:当类加载器需要加载类的时候,先请示其Parent(即上一层加载器)在其搜索路径载入,如果找不到,才在自己的搜索路径搜索该类。这样的顺序其实就是加载器层次上自顶而下的搜索,因为加载器必须保证基础类的加载。之所以是这种机制,还有一个安全上的考虑:如果某人将一个恶意的基础类加载到jvm,委托模型机制会搜索其父类加载器,显然是不可能找到的,自然就不会将该类加载进来。
1.4 JVM加载class文件的原理机制
-
装载:查找和导入class文件;
-
连接:
-
检查:检查载入的class文件数据的正确性;
-
准备:为类的静态变量分配存储空间;
-
解析:将符号引用转换成直接引用(这一步是可选的)
-
-
初始化:初始化静态变量,静态代码块。
这样的过程在程序调用类的静态成员的时候开始执行,所以静态方法main()才会成为一般程序的入口方法。类的构造器也会引发该动作。
2、多线程有几种实现方法?同步有几种实现方法?
多线程有两种实现方法,分别是继承Thread类与实现Runnable接口;也可以使用实现Callable接口,重写call()方法,这实际上是Executor框架中的功能类
同步的实现方面有两种,分别是synchronized,wait与notify
3、什么是分布式事务,如何解决分布式事务
分布式事务:单体应用拆分为分布式系统后,进程间的通讯机制和故障处理措施变的更加复杂。系统微服务化后,一个看似简单的功能,内部可能需要调用多个服务并操作多个数据库实现,服务调用的分布式事务问题变的非常突出。
如何解决:
1、 基于XA协议的两阶段提交方案:交易中间件与数据库通过 XA 接口规范,使用两阶段提交来完成一个全局事务, XA 规范的基础是两阶段提交协议。
第一阶段是表决阶段,所有参与者都将本事务能否成功的信息反馈发给协调者;第二阶段是执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地在所有分支上提交或者回滚。
但是两阶段提交方案锁定资源时间长,对性能影响很大,基本不适合解决微服务事务问题。
2、TCC方案:其将整个业务逻辑的每个分支显式的分成了Try、Confirm、Cancel三个操作。Try部分完成业务的准备工作,confirm部分完成业务的提交,cancel部分完成事务的回滚。
事务开始时,业务应用会向事务协调器注册启动事务。之后业务应用会调用所有服务的try接口,完成一阶段准备。之后事务协调器会根据try接口返回情况,决定调用confirm接口或者cancel接口。如果接口调用失败,会进行重试。
TCC方案让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。 当然TCC方案也有不足之处,集中表现在以下两个方面:
- 对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。
- 实现难度较大。需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。
3、基于消息的最终一致性方案
消息一致性方案是通过消息中间件保证上、下游应用数据操作的一致性。基本思路是将本地操作和发送消息放在一个事务中,保证本地操作和消息发送要么两者都成功或者都失败。下游应用向消息系统订阅该消息,收到消息后执行相应操作。
4、GTS–分布式事务解决方案
-
性能超强:
GTS通过大量创新,解决了事务ACID特性与高性能、高可用、低侵入不可兼得的问题。单事务分支的平均响应时间在2ms左右,3台服务器组成的集群可以支撑3万TPS以上的分布式事务请求。 -
应用侵入性极低:
GTS对业务低侵入,业务代码最少只需要添加一行注解(@TxcTransaction)声明事务即可。业务与事务分离,将微服务从事务中解放出来,微服务关注于业务本身,不再需要考虑反向接口、幂等、回滚策略等复杂问题,极大降低了微服务开发的难度与工作量。 -
完整解决方案:
GTS支持多种主流的服务框架,包括EDAS,Dubbo,Spring Cloud等。
有些情况下,应用需要调用第三方系统的接口,而第三方系统没有接入GTS。此时需要用到GTS的MT模式。GTS的MT模式可以等价于TCC模式,用户可以根据自身业务需求自定义每个事务阶段的具体行为。MT模式提供了更多的灵活性,可能性,以达到特殊场景下的自定义优化及特殊功能的实现。 -
容错能力强:
GTS解决了XA事务协调器单点问题,实现真正的高可用,可以保证各种异常情况下的严格数据一致。
参考文档:
- https://www.cnblogs.com/jiangyu666/p/8522547.html
4. HashMap与Hashtable的异同?
相同点:键不可重复,值可以重复。底层都是哈希表(单链Node构成的数组)。
不同点:
- HashMap:线程不安全,允许key和value为null。
- Hashtable:线程安全,key和value都不能为null,否则会抛NullPointerException异常。
5. ArrayList和Vector和LinkedList
相同点:都是List的子类,因此排列有序,值可重复。
特点:
- ArrayList :底层使用数组,因此查找速度快,增删速度慢。线程不安全。当容量不足时,扩容方案是 当前容量 * 1.5 +1
- Vector:底层使用数组,因此查找速度快,增删速度慢。因为add方法使用了synchronized关键字,所以线程安全,但效率低。当容量不足时,扩容方案是当前容量的一倍。
- LinkedList:底层使用的是双向循环链表结构,因此查询慢,增删快。线程不安全。
6. HashMap实现(数组 + 链表 + 红黑树red-black tree)
HashMap源码剖析:
可以看到,其数据结构是一个单项链表Node数组构成。根据Node的数据结构,可以看出,HashMap中存储了一个hash(算法生成,后面具体讲)、key、value,以及一个Node指针。
// @since 1.2
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
transient Node<K,V>[] table; //数组Node
//Node是一个单项链表,静态内部类
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// ...
}
}
put方法如下,该方法的主要目的是要确定插入节点的位置,如果节点已存在,则替换value值。
可以看到,(h = key.hashCode()) ^ (h >>> 16)
用来计算出hash值后,(n - 1) & hash
来计算出这个key、value应该存储再数组的下表的那个链表下,n = table.length
。所以tab[i = (n - 1) & hash]
实际上是链表头节点位置。然后顺着这个头节点遍历下去,如果节点已存在,则替换value值;否则讲节点放在链尾上。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//hash值生成算法
static final int hash(Object key) {
int h;
//取key得hashCode码 异或 key得hashCode码无符号右移16的结果,这个值算出来是可重复的。也就是不同的key,可能算出来相同的值,这个值就是数组的下标,相同值得key会放在这个数据下表下面得链表中。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//具体的算法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) //如果table为空,就初始化table
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //如果数组下标下,没有链表
tab[i] = newNode(hash, key, value, null); //新建一个Node节点,放在指定位置上
else {//如果数组下标下,存在链表
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) //判断是不是和头节点的key相同,相同则位置确定
e = p;
else if (p instanceof TreeNode) //看是不是红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //遍历链表,
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //节点遍历完时,即链表最后一个了,还没有找到与key相同的节点,则把该节点放在最后。
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); //如果节点超过8个,降链表结构转换为红黑树结构
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) //如链表中有key值相等的,则目标位置确定
break;
p = e;
}
}
if (e != null) { // existing mapping for key ,则值替换
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value; //如果key已经存在,替换value值
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
get方法:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value; //hash生成和上面put方法一样。
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {//数组部位空,并且头节点不为空
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))//如果头节点就是该key,则目标找到
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null); //遍历找出
}
}
return null;
}
7. volatile 关键字的作用 (变量可见性、禁止重排序)
Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。
volatile 变量具备两种特性,volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。
- 变量可见性:其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。
- 禁止重排序:volatile 禁止了指令重排。但是使用volatile将使得JVM优化失去作用,导致效率较低,所以要在必要的时候使用。
原理:
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有多个 CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的 CPUcache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache这一步。
适用场景:
值得说明的是对 volatile 变量的单次读/写操作可以保证原子性的,如 long 和 double 类型变量,但是并不能保证 i++这种操作的原子性,因为本质上 i++是读、写两次操作。在某些场景下可以代替 Synchronized。但是,volatile 不能完全取代 Synchronized 的位置,只有在一些特殊的场景下,才能适用 volatile。
总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:
(1)对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean flag = true)。
(2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的 volatile 变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用volatile。
8. Synchronized 同步锁
synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。
Synchronized 作用范围:
- 作用于方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
- synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。
9. AtomicInteger原子类
首先说明,此处 AtomicInteger,一个提供原子操作的 Integer 的类,常见的还有
AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他们的实现原理相同,区别在与运算对象类型的不同。令人兴奋地,还可以通过AtomicReference将一个对象的所有操作转化成原子操作。
我们知道,在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。通常我们会使用 synchronized 将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。
部分源码如下:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
//......
}
在这里说下其中的value,这里value使用了volatile关键字,volatile在这里可以做到的作用是使得多个线程可以共享变量,但是问题在于使用volatile将使得JVM优化失去作用,导致效率较低,所以要在必要的时候使用。
(ps:本人在集成微信发红包功能中,使用过该类。)