集合
集合的结构
linkedlist和arraylist有什么区别,底层是什么,插入哪个效率更高,哪个更显占内存
区别
1、数据结构不同
ArrayList是Array(动态数组)的数据结构,LinkedList是Link(链表)的数据结构。
2、效率不同
当随机访问List(get和set操作)时,ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
当对数据进行增加和删除的操作(add和remove操作)时,LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。
3、自由性不同
ArrayList自由性较低,因为它需要手动的设置固定大小的容量,但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;而LinkedList自由性较高,能够动态的随数据量的变化而变化,但是它不便于使用。
4、主要控件开销不同
ArrayList主要控件开销在于需要在lList列表预留一定空间;而LinkList主要控件开销在于需要存储结点信息以及结点指针信息。
内存占用
表面上看,LinkedList的Node存储结构似乎更占空间,但ArrayList扩容的时候,它会默认把数组的容量扩大到原来的1.5倍的,如果你只添加一个元素的话,那么会有将近原来一半大小的数组空间被浪费了,如果原先数组很大的话,那么这部分空间的浪费也是不少的
所以,如果数据量很大又在实时添加数据的情况下,ArrayList占用的空间不一定会比LinkedList空间小
linkedList、ArrayList、hashMap的区别
list的初始化大小
jdk7:默认大小是10,也可以手动指定容量
jdk8:直接new的话容量是0,调用add方法后扩容为10
hashmap的底层数据结构
HashMap在jdk7中实现原理:
HashMap map = new HashMap():
在实例化以后,底层创建了长度是16的一维数组Entry[] table。
...可能已经执行过多次put...
map.put(key1,value1):
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
如果此位置上的数据为空,此时的key1-value1添加成功。 ----情况1
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:
如果equals()返回false:此时key1-value1添加成功。----情况3
如果equals()返回true:使用value1替换value2。
补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原的数据复制过来。
HashMap在jdk8中相较于jdk7在底层实现方面的不同:
1. new HashMap():底层没创建一个长度为16的数组
2. jdk 8底层的数组是:Node[],而非Entry[]
3. 首次调用put()方法时,底层创建长度为16的数组
4. jdk7底层结构只:数组+链表。jdk8中底层结构:数组+链表+红黑树。
4.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
4.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
hashmap的遍历方式
iterator迭代器遍历keyset或entryset
hashmap和hashtable有什么区别
相同点:
实现原理相同,功能相同,底层都是哈希表结构,查询速度快,在很多情况下可以互用
不同点:
1、Hashtable是早期提供的接口,HashMap是新版JDK提供的接口。
2、Hashtable继承Dictionary类,HashMap实现Map接口。
3、Hashtable线程安全,HashMap线程非安全。
4、Hashtable不允许null值,HashMap允许null值。
线程
wait和sleep的区别?
wait | sleep | |
---|---|---|
同步 | 只能在同步上下文中调用wait方法,否则或抛出illegalMonitorStateException异常 | 不需要在同步方法或同步块中调用 |
作用对象 | wait方法定义在Object类中,作用于对象本身 | sleep方法定义在Java.lang.Thread中,作用于当前线程 |
释放锁资源 | 是 | 否 |
唤醒条件 | 其他线程调用对象的notify()或notifyAll()方法 | 超时或者调用interrupt()方法体 |
方法属性 | wait是实例方法 | sleep是静态方法 |
线程的实现方式有几种?Runnable和Callable的区别
实现方式
- 继承Thread方法
- 实现Runnable接口
- 实现Callable接口
- 线程池
callable和runnable的区别
- callable方法是call,runnable方法时run
- callable可以有返回值,runnable没有
- callable可以抛出异常,runnable不能
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
线程池参数有哪些
1.corePoolSize:线程池中的常驻核心线程数
- 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近视理解为今日当值线程
- 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放入到缓存队列当中.
相当于银行当前开放的柜台数
2.maximumPoolSize
线程池能够容纳同时执行的最大线程数,此值大于等于1
相当于银行能够开放的最大柜台数
3.keepAliveTime
多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止
默认情况下:
只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,知道线程中的线程数不大于corepoolSIze
相当于当银行柜台闲置时间过长,且超过keepAliveTime时,就将该柜台关闭
4.unit
keepAliveTime的单位
5.workQueue
任务队列,被提交但尚未被执行的任务.
相当于银行的候客区
6.threadFactory
表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可
相当于银行统一的东西,比如制服等
7.handler
拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示 数(maxnumPoolSize)时如何来拒绝.
当柜台全部开放,候客区已满,但还是有用户上门,业务量已超过银行的最大处理业务量,就会劝返上门的用户
线程池的拒绝策略
- AbortPolicy(默认):直接抛出RejectedException异常阻止系统正常运行
- CallerRunPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常.如果允许任务丢失,这是最好的拒绝策略
JUC
单例模式的最终版需不需要加volatile,为什么
需要加
- 保证内存可见性
- 禁止指令重排
- 不保证原子性
为什么要禁止指令重排
计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一般分以下3种
- 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致.
- 处理器在进行重新排序是必须要考虑指令之间的数据依赖性(比如有父才有子)
- 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测
volatile实现了禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
JVM
栈溢出的情况,如何调节栈的大小
StackOverFlow
内存溢出是由于没被引用的对象(垃圾)过多造成JVM没有及时回收,造成的内存溢出
一)、是否有递归调用
二)、是否有大量循环或死循环
三)、全局变量是否过多
四)、 数组、List、map数据是否过大
五)使用DDMS工具进行查找大概出现栈溢出的位置
修改栈的大小使用参数-Xss(-XX:ThreadStackSize)
GCRoot对象有哪些
- 虚拟机栈中引用的对象(栈帧中的本地方法表)。
- 方法区中(1.8称为元空间)的类静态属性引用的对象,一般指被static修饰的对象,加载类的时候就加载到内存中。
- 方法区中的常量引用的对象。
- 本地方法栈中的JNI(native方法)引用的对象。
异常
异常的类型,遇到的异常有哪些
java.lang.NullpointerException(空指针异常)
java.lang.ClassNotFoundException(指定的类不存在)
java.lang.IndexOutOfBoundsException(数组下标越界异常)
java.lang.IllegalArgumentException(方法的参数错误)
java.lang.IllegalAccessException(没有访问权限)
java.lang.ClassCastException(数据类型转换异常)
java.lang.NoSuchMethodException(方法不存在异常)
java.lang.InterruptedException(被中止异常)
java.lang.OutOfMemoryException (内存不足错误)
项目中异常怎么处理
系统对异常的处理使用统一的异常处理流程:
- 自定义异常类型。
- 自定义错误代码及错误信息。
- 对于可预知的异常由程序员在代码中主动抛出,由SpringMVC统一捕获。
可预知异常是程序员在代码中手动抛出本系统定义的特定异常类型,由于是程序员抛出的异常,通常异常信息比较
齐全,程序员在抛出时会指定错误代码及错误信息,获取异常信息也比较方便。 - 对于不可预知的异常(运行时异常)由SpringMVC统一捕获Exception类型的异常。
不可预知异常通常是由于系统出现bug、或一些不要抗拒的错误(比如网络中断、服务器宕机等),异常类型为RuntimeException类型(运行时异常)。 - 可预知的异常及不可预知的运行时异常最终会采用统一的信息格式(错误代码+错误信息)来表示,最终也会随请求响应给客户端。
ps:其中定义为runtimeException是因为这个异常在写入代码块中的时候不会强制要求捕获,这样方便在一个类中进行捕获。
原文链接:https://blog.csdn.net/qq_38116774/article/details/88036645
反射
反射如何填充属性、前提是什么
Class clazz = Person.class;
//getMethods():获取当前运行时类及其所父类中声明为public权限的方法
Method[] methods = clazz.getMethods();
for(Method m : methods){
System.out.println(m);
}
System.out.println();
//getDeclaredMethods():获取当前运行时类中声明的所方法。(不包含父类中声明的方法
Method[] declaredMethods = clazz.getDeclaredMethods();
for(Method m : declaredMethods){
System.out.println(m);
}
//创建运行时类的对象
Person p = (Person) clazz.newInstance();
//1. getDeclaredField(String fieldName):获取运行时类中指定变量名的属性
Field name = clazz.getDeclaredField("name");
//2.保证当前属性是可访问的
name.setAccessible(true);
//3.获取、设置指定对象的此属性值
name.set(p,"Tom");
关键字
final,使用场景
修饰属性:该属性不可更改
修饰方法:该方法不可被重写
修饰类:该类不可被继承
static
static:静态的
static可以用来修饰:属性、方法、代码块、内部类
静态变量随着类的加载而加载,可以通过“类.静态变量”的方式进行调用,由于类只会加载一次。则静态变量在内存中也只会存在一份,存在方法区的静态域中
finally的使用场景
一般用于资源的关闭还有锁的释放
IO
IO的类型有哪些
- 阻塞IO(bloking IO)
- 同步非阻塞IO(synchronous non-blocking IO)
- 信号驱动式IO(signal-driven IO)
- 多路复用IO(multiplexing IO)
- 异步IO(asynchronous IO)
前四种为同步IO,最后一种为异步IO。
File的方法有哪些
File类的获取功能
public String get AbsolutePath() :获取绝对路径public String getPath() :获取路径public String getName() :获取名称public String getParent() :获取上层文件目录路径。若无, 返回nullpublic long length() :获取文件长度(即:字节数)。不能获取目录的长度。public long lastModified() :获取最后一次的修改时间, 毫秒值public String[] list() :获取指定目录下的所有文件或者文件目录的名称数组(该方法应用于目录)public File[] listFiles() :获取指定目录下的所有文件或者文件目录的File数组(该方法应用于目录)
File类的重命名功能
public boolean renameTo(Filed est) :把文件重命名为指定的文件路径 注:调用该方法的文件应存在,参数文件不存在
File类的判断功能
public boolean isDirectory() : 判断是否是文件目录public boolean isFile() : 判断是否是文件public boolean exists() :判断是否存在public boolean canRead() :判断是否可读public boolean canWrite() :判断是否可写public boolean isHidden() : 判断是否隐藏
File类的创建功能
public boolean createNewFile() :创建文件。若文件存在, 则不创建, 返回fpublic boolean mkdir() :创建文件目录。如果此文件目录存在, 就不创建了;如果此文件目录的上层目录不存在,也不创建。public boolean mkdirs() :创建文件目录。如果上层文件目录不存在, 一并创建注意事项:如果你创建文件或者文件目录没有写盘符路径,那么,默认在项目路径下。
File类的删除功能
public boolean delete() :删除文件或者文件夹删除注意事项:Java中的删除不走回收站。要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录
IO相关的类有哪些
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
抽象基类 | InoutStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArayReader | CharArrayWriter |
访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | OutputStreamWriter | ||
对象流 | ObjectInputStream | ObjectOutputStream | ||
FilterInputStream | FilterOutputStream | FilterReader | FilterWriter | |
打印流 | PrintStream | PrintWriter | ||
推回输入流 | PushbackInputStream | PushbackReader | ||
特殊流 | DataInputStream | DataOutputStream |
网络
TCP 连接的“ 三次握手 ”与“ 四次挥手 ”
三次握手
- 首先客户端向服务器端发送一段TCP报文
- 服务器端接收到来自客户端的TCP报文之后,结束LISTEN阶段。并返回一段TCP报文
- 客户端接收到来自服务器端的确认收到数据的TCP报文之后,明确了从客户端到服务器的数据传输是正常的,结束SYN-SENT阶段。并返回最后一段TCP报文
四次挥手
- 首先客户端想要释放连接,向服务器端发送一段TCP报文
- 服务器端接收到从客户端发出的TCP报文之后,确认了客户端想要释放连接,随后服务器端结束ESTABLISHED阶段,进入CLOSE-WAIT阶段(半关闭状态)并返回一段TCP报文
- 服务器端自从发出ACK确认报文之后,经过CLOSED-WAIT阶段,做好了释放服务器端到客户端方向上的连接准备,再次向客户端发出一段TCP报文
- 客户端收到从服务器端发出的TCP报文,确认了服务器端已做好释放连接的准备,结束FIN-WAIT-2阶段,进入TIME-WAIT阶段,并向服务器端发送一段报文
其他
Java的基本数据类型
整型 byte、short、int、long
浮点型 float、double
字符型 char
布尔型 boolean
Java中的乐观锁和悲观锁
一、悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
二、乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
三、两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
四、乐观锁常见的两种实现方式
乐观锁一般会使用版本号机制或CAS(Compare-and-Swap,即比较并替换)算法实现。
4.1 版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
举一个简单的例子: 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
- 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
- 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
- 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
- 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。
4.2 CAS算法
即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
关于自旋锁,大家可以看一下这篇文章,非常不错:《 面试必备之深入理解自旋锁》
五、乐观锁的缺点
ABA 问题是乐观锁一个常见的问题。
- ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
JDK 1.5 以后的 AtomicStampedReference 类
就提供了此种能力,其中的 compareAndSet 方法
就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
- 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
- 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类
来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类
把多个共享变量合并成一个共享变量来操作。
六、CAS与synchronized的使用情景
简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)
- 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
- 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
string,stringbuffer,stringbuilder的区别,stringbuilder为什么比buffer效率高
String | StringBuffer | StringBuilder |
---|---|---|
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间 | StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量 | 可变类,速度更快 |
不可变 | 可变 | 可变 |
线程安全 | 线程不安全 | |
多线程操作字符串 | 单线程操作字符串 |
StringBuffer的方法都加了Synchronized,线程安全,所以效率较低
效率:String < StringBuffer < StringBuilder
string的equals比较什么?internal方法。
equals比较的是具体的值,==比较内存地址
s1=“abc”和s2=new String("abc"),equals和==分别是什么
判断这个常量是否存在于常量池。如果存在,则返回,如果不存在,将当前对象的引用复制到常量池,并且返回的是当前对象的引用
注:jdk1.6和jdk1.6之前,是将该对象赋值到常量池,然后返回。jdk1.7之后,不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。
hashcode方法有什么意义,equals和==的区别,两个对象的哈希值可能一样吗?
ashCode() 方法的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode() 方法是用来在散列存储结构中确定对象的存储地址的;
两个对象的哈希值可能一样。若两个对象相等,则他们的哈希值一定相等;若两个对象的哈希值相等,这两个对象不一定相等