1,数据结构:
1.1,ArrayList和LinkedList的区别:
1. **ArrayList的实现是基于数组,LinkedList的实现是基于双向链表。**1. 对于随机访问(查询元素)ArrayList要优于LinkedList。(ArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问,而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,查找某个元素的时间复杂度是O(N)。) ArrayList的查询性能要优于LinkedList;1. 对于插入和删除操作,LinkedList要优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,不需要像ArrayList那样重新**计算大小或者是更新索引**。 LinkedList的插入和删除要优于ArrayList;1. **LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。**
- List 和 map 体系:
List:
1. **ArrayList**: ArrayList是基于数组实现的,它的内部封装了一个Object[]数组。 通过默认构造器创建容器时,该数组先被初始化为空数组,之后在首次添加数据时再将其初始化成长度为10的数组。我们也可以使用有参构造器来创建容器,并通过参数来显式指定数组的容量,届时该数组被初始化为指定容量的数组。 如果向ArrayList中添加数据会造成超出数组长度限制,则会触发自动扩容,然后再添加数据。扩容就是数组拷贝,将旧数组中的数据拷贝到新数组里,而新数组的长度为原来长度的1.5倍。 ArrayList支持缩容,但不会自动缩容,即便是ArrayList中只剩下少量数据时也不会主动缩容。如果我们希望缩减ArrayList的容量,则需要自己调用它的trimToSize()方法,届时数组将按照元素的实际个数进行缩减。 加分回答 Set、List、Queue都是Collection的子接口,它们都继承了父接口的iterator()方法,从而具备了迭代的能力。但是,相比于另外两个接口,List还单独提供了listIterator()方法,增强了迭代能力。iterator()方法返回Iterator迭代器,listIterator()方法返回ListIterator迭代器,并且ListIterator是Iterator的子接口。ListIterator在Iterator的基础上,增加了向前遍历的支持,增加了在迭代过程中修改数据的支持。 初始长度为10,扩容量为1.5倍;1. **LinkedList**:
map:
1. **HashMap**:hashmap呢,主要用来处理具有键值对特征的数据;hashmap是基于哈希表对map接口的实现,具有较快的访问数据,但是遍历顺序是不确定的,因为遍历的位置是由hash算法得出hash值后才能确定的,因此,在每次遍历的时候,hash值都可能完全不一样;在hashmap中,key和value值是允许为null; Hashmap是线程不安全的,因为在hashmap中并没有利用到锁进行对数据写入的限制,因此在多线程中进行put操作可能导致数据不一致;在hashmap中,负债因子是为0.75,默认是容量大小为16;1. hashmap的底层采用了**数组,链表加红黑树**的存储结构,值得一提的是在jdk1.7之前是没有加入红黑树的,在jdk1.8后才加入该存储结构;1. 在hashmap中呢,数组部分称为哈希桶,当数组的一个位置已经被占据后,后续的数据如果定位到该位置要进行存储的情况下呢,会在该位置下挂载一个链表进行数据的存储,在链表长度**大于等于8**的时候呢,链表将会变成红黑树的结构进行存储,**长度降到6**又变成链表; 链表的复杂度为 O(n),而 红黑树 的复杂度为 O(log n);1. 在hashmap中呢,有一个node节点,该节点存储用来定位数据**索引位置的hash值,key值,value值,以及指向下一个node的next值**; node其实就是实现了entry接口;本质就是一个键值对;1. 需要在hashmap中插入数据的话,那就要先确定node节点的位置,那么在hashmap中是如何确定node节点的位置的呢?在hashmap中会先调用hashcode方法来计算hash值,然后对该hash值进行高位运算从而获取node的下标;1. **解决哈希冲突的问题**:哈希冲突并不能完全解决,只能减少hash冲突的风险;因为这是hash算法本身导致的,那么在hashmap中是利用二次hash,来使hash表中的槽位尽量地分散,或者说是让hash后的结果尽可能地分散,避免hash碰撞,提高hashmap的运行效率;1. 初始容量为16,扩容因子0.75,扩容量为2倍;2. **LinkedHashmap**:
1.2,Hashmap、Hashtable、ConcurrentHashMap的区别?
1. HashMap是非线程安全,HashTable线程安全,ConcurrentHashMap也是线程安全。但是需要线程安全的话,建议使用**ConcurrentHashMap**,而不建议使用HashTable(因为没有代码优化)1. ConcurrentHashMap在JDK1.8之前采用分段锁机制,JDK1.8之后采用CAS和synchronized来保证并发安全,synchronized只锁定当前链表或红黑树二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。1. HashMap和ConcurrentHashMap在JDK1.8之后,都是数组+链表+红黑树。
1.3,什么是ArrayList集合:
ArrayList是基于数组实现的,它的内部封装了一个Object[]数组。 通过默认构造器创建容器时,该数组先被初始化为空数组,之后在首次添加数据时再将其初始化成长度为10的数组。我们也可以使用有参构造器来创建容器,并通过参数来显式指定数组的容量,届时该数组被初始化为指定容量的数组。 如果向ArrayList中添加数据会造成超出数组长度限制,则会触发自动扩容,然后再添加数据。扩容就是数组拷贝,将旧数组中的数据拷贝到新数组里,而新数组的长度为原来长度的1.5倍。 ArrayList支持缩容,但不会自动缩容,即便是ArrayList中只剩下少量数据时也不会主动缩容。如果我们希望缩减ArrayList的容量,则需要自己调用它的trimToSize()方法,届时数组将按照元素的实际个数进行缩减。 加分回答 Set、List、Queue都是Collection的子接口,它们都继承了父接口的iterator()方法,从而具备了迭代的能力。但是,相比于另外两个接口,List还单独提供了listIterator()方法,增强了迭代能力。iterator()方法返回Iterator迭代器,listIterator()方法返回ListIterator迭代器,并且ListIterator是Iterator的子接口。ListIterator在Iterator的基础上,增加了向前遍历的支持,增加了在迭代过程中修改数据的支持。
1.4,HashCode和equals的关系:
首先,在java中每个对象都可以调用自己的hashcode()方法得到自己的哈希值,这个哈希值其实就相当于指纹一样,通常来说,是没有两个一样的指纹,但是在java中做不到仅用hashcode来判断两个对象是否相等;不过hashcode依然能做一些提前的判断;
因此,会出现以下几种情况:
1. 如果两个对象的hashcode不同,那么这两个对象一定不相同;1. 如果两个对象的hashcode相同,但是这两个对象不一定相同,还需要用到equals方法来进行后续的判断;1. 如果两个对象相同,那么他们的hashcode一定相同;
在java一些集合类的实现中,比如说hashmap;根据上述的原则,会先调用hashcode判断两个对象的hash值,如果两个对象的hash值不相同,则直接判定这两个对象不相同;如果hash值相同,那么会调用equals方法来进行后置判断,才能最终确定这两个对象是否相同;
因此,也可以说hashcode方法是equals方法的前置处理方法;若要判断对象是否相同,必须要先经过hashcode再执行equals;
1.5,== 和 equals 的区别:
==:如果是基本类型的话,比较的值是否相等;如果是引用类型的话,比较的是 引用地址;
equals:具体看类中各个重写equals方法的逻辑,比如String类型,虽然是引用类型,但是在String类中重写了equals方法,因此方法内部是比较两个String的字符串是否相等;
2,Java语法:
2.1,讲一下JDK 8 / Java 8 的新特性:
- Lambda表达式:该特性可以将功能视为方法参数,或者将代码视为数据。使用 Lambda 表达式,可以更简洁地表示单方法接口(称为功能接口)的实例。
- 方法引用:方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与Lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。 - Java8对接口进行了改进:允许在接口中定义默认方法,默认方法必须使用default修饰。
- - Stream API:新添加的Stream API(java.util.stream)支持对元素流进行函数式操作。Stream API 集成在 Collections API 中,可以对集合进行批量操作,例如顺序或并行的 map-reduce 转换。
- - Date Time API:加强对日期与时间的处理。
2.2,Java的四种引用方式:
在JDK 1.2版之前,一个对象只有“被引用”或者“未被引用”两种状态,对于描述一些“不太重要”的对象就显得无能为力。
那么在JDK 1.2之后呢,Java对引用的概念进行了扩充,将引用方式扩充到了四种:
1. **强引用**:强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似**“Object obj=new Object()”**这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。1. **软引用**:软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。1. **弱引用**:弱引用也是用来描述那些非必须的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。1. **虚引用**:虚引用是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
2.3,说说重载和重写的区别?构造方法能不能重写?
- **重载**:重载的是发生在同一个类中,多个方法名必须一致,但是参数列表要不同,如:参数个数不同,参数类型不同;- **重写**:重写是指子类可以重写父类的方法,方法名相同,且参数列表也要相同;另外,返回值要小于或等于父类方法,访问修饰符权限要大于等于父类;如果父类方法被private修饰,则该方法不能被子类重写;- 构造方法不能被重写,如果允许重写构造方法的话,那么子类中将会存在与类名不同的构造方法,这与构造方法的要求是矛盾的。 但是构造方法能被重载;
2.4,什么是反射?什么是动态代理?
- **反射:**其实就是把java中的各个成分映射为对应的Java类;告诉传入字节码文件某个类要调用的对象在哪里,就好比在MyBatis中,我们只写接口,不用去写实现类就能执行SQL;这里就用到了反射;- **动态代理:**动态代理其实是就是代理模式的一种;动态代理就是让程序来帮我们实现接口的方法,并通过invokeHandler来对需要的方法来进行增强,常用的是jdk的动态代理,jdk的动态代理用到的是反射的机制;在Spring AOP,Mybatis都有用到动态代理;
3,* 多线程:
3.1,什么是多线程(说说多线程):
线程是操作系统调度的最小单元,它可以让一个进程并发地处理多个任务,也叫轻量级进程。所以,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈、局部变量,并且能够共享进程内的资源。由于共享资源,处理器便可以在这些线程之间快速切换,从而让使用者感觉这些线程在同时执行。 总的来说,操作系统可以同时执行多个任务,每个任务就是一个进程。进程可以同时执行多个任务,每个任务就是一个线程。一个程序运行之后至少有一个进程,而一个进程可以包含多个线程,但至少要包含一个线程。
3.2,如何保证线程安全:
在Java中提供了多种方案给我们使用,常用的有三种方式:
- **原子类**:遵循CAS即“比较和替换”规则,比较要更新的值是否等于期望值,如果是则更新,如果不是则失败(单共享变量)- **volatile关键字**:轻量级的synchronized,在多处理器开发中保证了共享变量的“可见性”,从而可以保证单个变量读写时的线程安全(单共享变量)- **锁**:java中常用的锁有两种:**synchronized+juc**包下的lock锁。支持响应中断、支持超时机制、支持以非阻塞的方式获取锁、支持多个条件变量
3.3,Java中常用的锁及原理:
- Java中有两种加锁方式:分别是synchronized关键字和Lock接口,而Lock接口的经典实现是ReentrantLock
- 而 synchronized关键字:synchronized的实现依赖于对象头,Lock接口的实现则依赖于AQS。 synchronized的底层是采用Java对象头来存储锁信息的,对象头包含三部分,分别是Mark Word、Class Metadata Address、Array length。其中,Mark Word用来存储对象的hashCode及锁信息,Class Metadata Address用来存储对象类型的指针,而Array length则用来存储数组对象的长度。 AQS是队列同步器,是用来构建锁的基础框架,Lock实现类都是基于AQS实现的。
3.4,synchronized 和 Lock 的区别?
1)语法不同:
synchronized 是Java的关键字或修饰符,在jvm层面上,修饰方法或代码块。
Lock不是修饰符,是一个接口(加锁的工具类,该类提供很多方法加锁、释放锁等)tryLock() unLock()
2)释放锁不同(*): **
synchronized 以获取锁的线程执行完同步代码,释放锁,且线程执行发生异常,jvm会自动让线程释放锁(不管成功还是失败,都会自动释放锁)
Lock必须手动在finally中释放锁(必须手动释放锁)
3)死锁情况不同(*):
synchronized 在发生异常时候会自动释放占有的锁,因此不会出现死锁
Lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生
4)锁判断不同:
synchronized 无法判断当前线程的上锁状态
Lock可以判断当前线程的上锁状态 tryLock unLock isLock
5)锁类型不同:**
synchronized是可重入 不可判断 非公平
Lock:是可重入 可判断 可公平
公平性(线程等待时间长,优先获取锁)
3.5,* 线程是如何进行创建:
- 创建线程有三种方式,分别是**继承Thread类、实现Runnable接口、实现Callable接口**。1. **继承Thread类:**通过继承Thread类来创建线程的步骤如下 - 定义Thread类的子类,并重写该类的**run()**方法,该方法将作为**线程执行体**。 - 创建Thread子类的**实例**,即创建了**线程对象**。 - 调用线程对象的**start()**方法来启动该线程。1. **实现Runnable接口:**通过实现Runnable接口来创建线程的步骤如下:创建一个定义类实现Runnable接口,在实现类中重写**run方法**,然后创建实现类对象,再创建Thread类的实例,即创建了线程对象,并将实现类对象作为Thread类线程对象的**构造器参数**传入,最后利用线程对象调用**start方法**启动该线程; 无 线程执行结束的 返回值1. **实现Callable接口:**通过实现Callable接口来创建线程的步骤如下 - 定义Callable接口的实现类,并实现call()方法,该方法将作为线程执行体。- 创建Callable实现类的实例,并以该实例作为参数,创建FutureTask对象。 - 使用FutureTask对象作为参数,创建Thread对象,然后启动线程。 - 调用FutureTask对象的get()方法,获得子线程执行结束后的返回值。 归纳起来,创建线程的方式实际只有两种:继承父类和实现接口。 有 线程执行结束的 返回值- **而使用Runnable接口和Callable接口的方式,区别在于前者不能获得线程执行结束的返回值,后者可以获得线程执行结束的返回值。**- **(后面是补充点)**- 而继承父类和实现接口这两种方式的优缺点是: - 采用接口的方式创建线程,优点是线程类还可以继承于其他类,并且多个线程可以共享一个线程体,适合多个线程处理同一份资源的情况。缺点是编程稍微麻烦一点点。 - 采用继承的方式创建线程,优点是编程稍微简单一点点。缺点是因为线程类已经继承了Thread类,所以就不能继承其他的父类了。 所以,通常情况下,更推荐采用接口的方式来创建线程。如果需要返回值,就使用Callable接口,否则使用Runnable接口即可。
3.6,* Java中有哪些线程安全的集合:
java.util包下的集合类中,大部分都是非线程安全的,但也有少数的线程安全的集合类,例如Vector、Hashtable,它们都是非常古老的API。虽然它们是线程安全的,但是性能很差,已经不推荐使用了。对于这个包下非线程安全的集合,可以利用Collections工具类,该工具类提供的synchronizedXxx()方法,可以将这些集合类包装成线程安全的集合类。
从JDK 1.5开始,并发包下新增了大量高效的并发的容器,这些容器按照实现机制可以分为三类。
- **第一类:是以降低锁粒度来提高并发性能的容器,它们的类名以Concurrent开头,如ConcurrentHashMap。**- **第二类:是采用写时复制技术实现的并发容器,它们的类名以CopyOnWrite开头,如CopyOnWriteArrayList。**- **第三类:是采用Lock实现的阻塞队列,内部创建两个Condition分别用于生产者和消费者的等待,这些类都实现了BlockingQueue接口,如ArrayBlockingQueue。 **- **(加分回答)** Collections还提供了如下三类方法来返回一个不可变的集合,这三类方法的参数是原有的集合对象,返回值是该集合的“只读”版本。通过Collections提供的三类方法,可以生成“只读”的Collection或Map。 emptyXxx():返回一个空的不可变的集合对象 singletonXxx():返回一个只包含指定对象的不可变的集合对象 unmodifiableXxx():返回指定集合对象的不可变视图
3.7,知道ThreadLocal吗?讲讲你对ThreadLoacl的理解(项目中多线程并发安全问题如何解决?)
先讲作用:
在同一个项目中的同一个线程返回内共享数据(一个线程存入数据,另一个线程无法读取或修改)
再讲使用方式:
其实ThreadLocal底层使用一种Map结构,key是存储线程标记,value是我们写入的值.
set(Object object):往当前线程存入数据,底层map.put(‘当前线程唯一标记’,object)
Object get():从当前线程取出之前存入的数据,底层Object map.get(‘当前线程唯一标记’)
remove():移除当前线程存入的数据,底层map.remove(‘当前线程唯一标记’)
最后讲注意事项:
(ThreadLocal可能出来内存泄露的问题(可能存在并发数据问题),怎么解决?) OOM
在使用完TheadLoacl的数据后,建议手动移除线程数据,防止内存泄露和防止并发数据问题.
3.8,说说悲观锁和乐观锁?有哪些悲观锁和乐观锁?
- 悲观锁(写):每次获取数据的时候,都担心数据被修改,因此,在每次获取数据的时候都会进行加锁,确保别人在使用的时候不会被别人修改;获取数据完毕后进行数据解锁;在此期间其他线程都会等待;
- 常见的悲观锁实现:Java中synchronized关键字和Lock的实现类,mysql中的表锁,读锁,写锁等;
- 乐观锁(读):每次获取数据的时候,不担心数据被修改,因此,没有加锁,但是在获取数据前要先进行判断数据是否被修改过,如果数据被其他线程修改,那么则不会进行数据更新,没有则更新,期间数据可以被其他线程操作;如:数据库设置一个版本号 version,在每次更新数据前先对该版本号进行校验,判断当前数据是否被修改;
- 常见的乐观锁实现:版本号控制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数.当数据被修改时,version值会+1.当线程A要更新数据时,在读取数据的同时也会读取version值,在提交更新是,若刚才读取到的version值与当前数据库中的version值相等值才更新,否则重试更新操作,知道更新成功.
- 应用场景:
- 悲观锁:适合写入操作比较频繁的场景如果出现大量的读取操作,每次读取的时候都会进行加锁,会增加锁的开销,降低了性能,而且具有强一致性;
- 乐观锁:适合读取操作比较频繁的场景如果出现大量的写入操作,可能会出现数据冲突,不能保证数据一致性。而为了保证数据的一致性,上层应用需要不断重获数据,会大大增加查询操作,降低性能
3.9,线程池的7个参数:
//**1,配置核心线程数;**<br /> tptx.setCorePoolSize(15);<br /> //**2,设置最大线程数**<br /> tptx.setMaxPoolSize(30);<br /> //**3,配置队列大小**<br /> tptx.setQueueCapacity(1000);<br /> //**4,线程的名称前缀**<br /> tptx.setThreadNamePrefix("executor-");<br /> //**5,线程活跃时间(存活时间)**<br /> tptx.setKeepAliveSeconds(30);<br /> //**6,设置所有任务结束后是否关闭线程池;**<br /> tptx.setWaitForTasksToCompleteOnShutdown(true);<br /> //**7,设置拒绝策略(4种)**<br /> tptx.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());//执行初始化<br /> tptx.initialize();
3.1.1,wait和sleep的区别:
1. 属于不同的两个类,Sleep是线程类的静态方法,而wait是Object类中的方法;1. Sleep方法不会释放锁,wait方法会释放锁,但是前提是需要notify(唤醒)后重新获取对象锁资源才能进行执行;1. sleep方法可以在任何地方使用,而wait方法则只能在同步方法或者 同步代码块中使用;1. sleep会使线程进入阻塞状态(线程睡眠),wait方法会使线程进入到等待队列(线程挂起);
3.1.2,* 乐观锁,悲观锁,分布式锁:
4,IO:
4.1,Java中如何实现序列化?意义?:
- 序列化就是一种用**来处理对象流**的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会存在数据乱序的问题)。- 要实现序列化,需要让一个类实现Serializable接口,该接口是一个标识性接口,标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object obj)方法就可以将实现对象写出(即保存其状态);如果需要反序列化则可以用一个输入流建立对象输入流,然后通过readObject方法从流中读取对象。序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆;- 比如说在Mybatis中开启了二级缓存,那么在实体类中就要去实现Serializable接口;
4.2
5,JVM:
5.1,内存回收算法:
5.1.1,标记清除算法:
在JVM中,有一个GC root对象的引用链,如果有个对象没有被引用链所引用,那么该对象就标记为垃圾;
如何清除呢:
清除不是说把这些对象给删除掉,而是将这些对象放到一个空的地址列表中;
这种算法的一个优点呢就是执行速度快,但是内存的数据碎片化程度较高,如果在遇到超过最大空内存的数据那么就会发生内存溢出的风险;
5.1.2,标记整理算法:

- 这种算法的标记垃圾的方式和标记清除算法是一样的,但是呢,**这种算法对数据碎片化的处理进行了改进,在清除掉垃圾后,会对内存的空间进行整理,将其他内存对象整理成一块连续的内存空间;因此,这种算法是没有数据碎片化的情况出现的,但是,缺点是执行速度要慢,因为要重新计算内存的地址进行;**
5.1.3:标记复制:

- 顾名思义,复制算法其实就是开辟了两个内存空间来对内存垃圾进行整理,将非垃圾的内存对象复制到另外一个空内存中,然后清除原来的内存中的垃圾对象即可,但是这种算法的缺点是:要占用双倍的内存空间;
5.2,分代回收机制:
在Java中,将堆内存划分了两个区,分别是:新生代 和 老年代 ;
- **新生代:**在里面又划分了三个区:伊甸(dian)园,幸存区FROM,幸存区TO;在新生代中,垃圾回收的频率会比老年代高,因为,新生代中存储的是频繁使用到的对象,而老年代的是高价值,要长期使用的内存对象;- **在幸存区FROM和TO中**:当伊甸园内存不足时,就会采用吧标记负责算法将回收后的幸存对象放到幸存区;- **老年代:**在新生代的幸存区中熬过15次回收的话,就能晋升到老年代(不过如果幸存区中的内存不够用的话也会导致提前晋升);
