java重难点

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 作用范围:

  1. 作用于方法时,锁住的是对象的实例(this);
  2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
  3. 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:本人在集成微信发红包功能中,使用过该类。)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值