2024年最新Java面试题及答案整理(持续完善中…)

面试题总结

其它面试题(springboot、mybatis、并发、java中高级面试总结等)

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

String c=new Integer(12).toString();//通过包装类的toString()也可以

String d=new Double(2.3).toString();

再举例下。比如我现在要用泛型

List nums;

这里<>需要类。如果你用int。它会报错的。

重载和重写的区别

override(重写)

1. 方法名、参数、返回值相同。

2. 子类方法不能缩小父类方法的访问权限。

3. 子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。

4. 存在于父类和子类之间。

5. 方法被定义为final不能被重写。

overload(重载)

1. 参数类型、个数、顺序至少有一个不相同。

2. 不能重载只有返回值不同的方法名。

3. 存在于父类和子类、同类中。

抽象类和接口有什么区别

接口是公开的,里面不能有私有的方法或变量,是用于让别人使用的,而抽象类是可以有私有方法或私有变量的,

另外,实现接口的一定要实现接口里定义的所有方法,而实现抽象类可以有选择地重写需要用到的方法,一般的应用里,最顶级的是接口,然后是抽象类实现接口,最后才到具体类实现。

还有,接口可以实现多重继承,而一个类只能继承一个超类,但可以通过继承多个接口实现多重继承,接口还有标识(里面没有任何方法,如Remote接口)和数据共享(里面的变量全是常量)的作用。

说说反射的用途及实现

Java反射机制主要提供了以下功能:在运行时构造一个类的对象;判断一个类所具有的成员变量和方法;调用一个对象的方法;生成动态代理。反射最大的应用就是框架

Java反射的主要功能:

  • 确定一个对象的类

  • 取出类的modifiers,数据成员,方法,构造器,和超类.

  • 找出某个接口里定义的常量和方法说明.

  • 创建一个类实例,这个实例在运行时刻才有名字(运行时间才生成的对象).

  • 取得和设定对象数据成员的值,如果数据成员名是运行时刻确定的也能做到.

  • 在运行时刻调用动态对象的方法.

  • 创建数组,数组大小和类型在运行时刻才确定,也能更改数组成员的值.

反射的应用很多,很多框架都有用到

spring 的 ioc/di 也是反射….

javaBean和jsp之间调用也是反射….

struts的 FormBean 和页面之间…也是通过反射调用….

JDBC 的 classForName()也是反射……

hibernate的 find(Class clazz) 也是反射….

反射还有一个不得不说的问题,就是性能问题,大量使用反射系统性能大打折扣。怎么使用使你的系统达到最优就看你系统架构和综合使用问题啦,这里就不多说了。

来源:http://uule.iteye.com/blog/1423512

说说自定义注解的场景及实现

(此题自由发挥,就看你对注解的理解了!==)登陆、权限拦截、日志处理,以及各种Java框架,如Spring,Hibernate,JUnit 提到注解就不能不说反射,Java自定义注解是通过运行时靠反射获取注解。实际开发中,例如我们要获取某个方法的调用日志,可以通过AOP(动态代理机制)给方法添加切面,通过反射来获取方法包含的注解,如果包含日志注解,就进行日志记录。

HTTP 请求的 GET 与 POST 方式的区别

GET方法会把名值对追加在请求的URL后面。因为URL对字符数目有限制,进而限制了用在客户端请求的参数值的数目。并且请求中的参数值是可见的,因此,敏感信息不能用这种方式传递。

POST方法通过把请求参数值放在请求体中来克服GET方法的限制,因此,可以发送的参数的数目是没有限制的。最后,通过POST请求传递的敏感信息对外部客户端是不可见的。

参考:https://www.cnblogs.com/wangli-66/p/5453507.html

session 与 cookie 区别

cookie 是 Web 服务器发送给浏览器的一块信息。浏览器会在本地文件中给每一个 Web 服务

器存储 cookie。以后浏览器在给特定的 Web 服务器发请求的时候,同时会发送所有为该服

务器存储的 cookie。下面列出了 session 和 cookie 的区别:

无论客户端浏览器做怎么样的设置,session都应该能正常工作。客户端可以选择禁用 cookie,

但是, session 仍然是能够工作的,因为客户端无法禁用服务端的 session。

JDBC 流程

1、 加载JDBC驱动程序:

在连接数据库之前,首先要加载想要连接的数据库的驱动到JVM(Java虚拟机),

这通过java.lang.Class类的静态方法forName(String className)实现。

例如:

try{

//加载MySql的驱动类

Class.forName(“com.mysql.jdbc.Driver”) ;

}catch(ClassNotFoundException e){

System.out.println(“找不到驱动程序类 ,加载驱动失败!”);

e.printStackTrace() ;

}

成功加载后,会将Driver类的实例注册到DriverManager类中。

2、 提供JDBC连接的URL

  • 连接URL定义了连接数据库时的协议、子协议、数据源标识。

  • 书写形式:协议:子协议:数据源标识

协议:在JDBC中总是以jdbc开始 子协议:是桥连接的驱动程序或是数据库管理系统名称。

数据源标识:标记找到数据库来源的地址与连接端口。

例如:

jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=gbk;useUnicode=true;(MySql的连接URL)

表示使用Unicode字符集。如果characterEncoding设置为 gb2312或GBK,本参数必须设置为true 。characterEncoding=gbk:字符编码方式。

3、创建数据库的连接

  • 要连接数据库,需要向java.sql.DriverManager请求并获得Connection对象, 该对象就代表一个数据库的连接。

  • 使用DriverManager的getConnectin(String url , String username , String password )方法传入指定的欲连接的数据库的路径、数据库的用户名和 密码来获得。

例如: //连接MySql数据库,用户名和密码都是root

String url = “jdbc:mysql://localhost:3306/test” ;

String username = “root” ;

String password = “root” ;

try{

Connection con = DriverManager.getConnection(url , username , password ) ;

}catch(SQLException se){

System.out.println(“数据库连接失败!”);

se.printStackTrace() ;

}

4、 创建一个Statement

•要执行SQL语句,必须获得java.sql.Statement实例,Statement实例分为以下3 种类型:

1、执行静态SQL语句。通常通过Statement实例实现。

2、执行动态SQL语句。通常通过PreparedStatement实例实现。

3、执行数据库存储过程。通常通过CallableStatement实例实现。

具体的实现方式:

Statement stmt = con.createStatement() ; PreparedStatement pstmt = con.prepareStatement(sql) ; CallableStatement cstmt = con.prepareCall(“{CALL demoSp(? , ?)}”) ;

5、执行SQL语句

Statement接口提供了三种执行SQL语句的方法:executeQuery 、executeUpdate 和execute

1、ResultSet executeQuery(String sqlString):执行查询数据库的SQL语句 ,返回一个结果集(ResultSet)对象。

2、int executeUpdate(String sqlString):用于执行INSERT、UPDATE或 DELETE语句以及SQL DDL语句,如:CREATE TABLE和DROP TABLE等

3、execute(sqlString):用于执行返回多个结果集、多个更新计数或二者组合的 语句。 具体实现的代码:

ResultSet rs = stmt.executeQuery(“SELECT * FROM …”) ; int rows = stmt.executeUpdate(“INSERT INTO …”) ; boolean flag = stmt.execute(String sql) ;

6、处理结果

两种情况:

1、执行更新返回的是本次操作影响到的记录数。

2、执行查询返回的结果是一个ResultSet对象。

• ResultSet包含符合SQL语句中条件的所有行,并且它通过一套get方法提供了对这些 行中数据的访问。

• 使用结果集(ResultSet)对象的访问方法获取数据:

while(rs.next()){

String name = rs.getString(“name”) ;

String pass = rs.getString(1) ; // 此方法比较高效

}

(列是从左到右编号的,并且从列1开始)

7、关闭JDBC对象

操作完成以后要把所有使用的JDBC对象全都关闭,以释放JDBC资源,关闭顺序和声 明顺序相反:

1、关闭记录集

2、关闭声明

3、关闭连接对象

if(rs != null){ // 关闭记录集

try{

rs.close() ;

}catch(SQLException e){

e.printStackTrace() ;

}

}

if(stmt != null){ // 关闭声明

try{

stmt.close() ;

}catch(SQLException e){

e.printStackTrace() ;

}

}

if(conn != null){ // 关闭连接对象

try{

conn.close() ;

}catch(SQLException e){

e.printStackTrace() ;

}

}

MVC 设计思想

MVC就是

M:Model 模型

V:View 视图

C:Controller 控制器

模型就是封装业务逻辑和数据的一个一个的模块,控制器就是调用这些模块的(java中通常是用Servlet来实现,框架的话很多是用Struts2来实现这一层),视图就主要是你看到的,比如JSP等.

当用户发出请求的时候,控制器根据请求来选择要处理的业务逻辑和要选择的数据,再返回去把结果输出到视图层,这里可能是进行重定向或转发等.

equals 与 == 的区别

值类型(int,char,long,boolean等)都是用==判断相等性。对象引用的话,判断引用所指的对象是否是同一个。equals是Object的成员函数,有些类会覆盖(override)这个方法,用于判断对象的等价性。例如String类,两个引用所指向的String都是”abc”,但可能出现他们实际对应的对象并不是同一个(和jvm实现方式有关),因此用判断他们可能不相等,但用equals判断一定是相等的。

集合

List 和 Set 区别

List,Set都是继承自Collection接口

List特点:元素有放入顺序,元素可重复

Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉

(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)

Set和List对比:

Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。

List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。

List 和 Map 区别

List是对象集合,允许对象重复。

Map是键值对的集合,不允许key重复。

Arraylist 与 LinkedList 区别

Arraylist:

优点:ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。

缺点:因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低。

LinkedList:

优点:LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove,LinedList比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景

缺点:因为LinkedList要移动指针,所以查询操作性能比较低。

适用场景分析:

当需要对数据进行对此访问的情况下选用ArrayList,当需要对数据进行多次增加删除修改时采用LinkedList。

ArrayList 与 Vector 区别

  1. `public ArrayList(int initialCapacity)//构造一个具有指定初始容量的空列表。`

  2. `public ArrayList()//构造一个初始容量为10的空列表。`

  3. `public ArrayList(Collection<? extends E> c)//构造一个包含指定 collection 的元素的列表`

Vector有四个构造方法:

  1. `public Vector()//使用指定的初始容量和等于零的容量增量构造一个空向量。`

  2. `public Vector(int initialCapacity)//构造一个空向量,使其内部数据数组的大小,其标准容量增量为零。`

  3. `public Vector(Collection<? extends E> c)//构造一个包含指定 collection 中的元素的向量`

  4. `public Vector(int initialCapacity,int capacityIncrement)//使用指定的初始容量和容量增量构造一个空的向量`

ArrayList和Vector都是用数组实现的,主要有这么三个区别:

  1. Vector是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果。而ArrayList不是,这个可以从源码中看出,Vector类中的方法很多有synchronized进行修饰,这样就导致了Vector在效率上无法与ArrayList相比;

  2. 两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。

  3. Vector可以设置增长因子,而ArrayList不可以。

  4. Vector是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。

适用场景分析:

  1. Vector是线程同步的,所以它也是线程安全的,而ArrayList是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用ArrayList效率比较高。

  2. 如果集合中的元素的数目大于目前集合数组的长度时,在集合中使用数据量比较大的数据,用Vector有一定的优势。

HashMap 和 Hashtable 的区别

1.hashMap去掉了HashTable 的contains方法,但是加上了containsValue()和containsKey()方法。

2.hashTable同步的,而HashMap是非同步的,效率上逼hashTable要高。

3.hashMap允许空键值,而hashTable不允许。

注意:

TreeMap:非线程安全基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。

Treemap:适用于按自然顺序或自定义顺序遍历键(key)。

参考:http://blog.csdn.net/qq_22118507/article/details/51576319

HashSet 和 HashMap 区别

set是线性结构,set中的值不能重复,hashset是set的hash实现,hashset中值不能重复是用hashmap的key来实现的。

map是键值对映射,可以空键空值。HashMap是Map接口的hash实现,key的唯一性是通过key值hash值的唯一来确定,value值是则是链表结构。

他们的共同点都是hash算法实现的唯一性,他们都不能持有基本类型,只能持有对象

HashMap 和 ConcurrentHashMap 的区别

ConcurrentHashMap是线程安全的HashMap的实现。

(1)ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的syn关键字锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。

(2)HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

HashMap 的工作原理及代码实现

参考:https://tracylihui.github.io/2015/07/01/Java集合学习1:HashMap的实现原理/

ConcurrentHashMap 的工作原理及代码实现

HashTable里使用的是synchronized关键字,这其实是对对象加锁,锁住的都是对象整体,当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。

ConcurrentHashMap算是对上述问题的优化,其构造函数如下,默认传入的是16,0.75,16。

  1. `public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2) {`

  2. `//…`

  3. `int i = 0;`

  4. `int j = 1;`

  5. `while (j < paramInt2) {`

  6. `++i;`

  7. `j <<= 1;`

  8. `}`

  9. `this.segmentShift = (32 - i);`

  10. `this.segmentMask = (j - 1);`

  11. `this.segments = Segment.newArray(j);`

  12. `//…`

  13. `int k = paramInt1 / j;`

  14. `if (k * j < paramInt1)`

  15. `++k;`

  16. `int l = 1;`

  17. `while (l < k)`

  18. `l <<= 1;`

  19. `for (int i1 = 0; i1 < this.segments.length; ++i1)`

  20. `this.segments[i1] = new Segment(l, paramFloat);`

  21. `}`

  22. `public V put(K paramK, V paramV) {`

  23. `if (paramV == null)`

  24. `throw new NullPointerException();`

  25. `int i = hash(paramK.hashCode()); //这里的hash函数和HashMap中的不一样`

  26. `return this.segments[(i >>> this.segmentShift & this.segmentMask)].put(paramK, i, paramV, false);`

  27. `}`

ConcurrentHashMap引入了分割(Segment),上面代码中的最后一行其实就可以理解为把一个大的Map拆分成N个小的HashTable,在put方法中,会根据hash(paramK.hashCode())来决定具体存放进哪个Segment,如果查看Segment的put操作,我们会发现内部使用的同步机制是基于lock操作的,这样就可以对Map的一部分(Segment)进行上锁,这样影响的只是将要放入同一个Segment的元素的put操作,保证同步的时候,锁住的不是整个Map(HashTable就是这么做的),相对于HashTable提高了多线程环境下的性能,因此HashTable已经被淘汰了。

线程

创建线程的方式及实现

Java中创建线程主要有三种方式:

一、继承Thread类创建线程类

(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

(2)创建Thread子类的实例,即创建了线程对象。

(3)调用线程对象的start()方法来启动该线程。

  1. `package com.thread;`

  2. `public class FirstThreadTest extends Thread{`

  3. `int i = 0;`

  4. `//重写run方法,run方法的方法体就是现场执行体`

  5. `public void run()`

  6. `{`

  7. `for(;i<100;i++){`

  8. `System.out.println(getName()+" "+i);`

  9. `}`

  10. `}`

  11. `public static void main(String[] args)`

  12. `{`

  13. `for(int i = 0;i< 100;i++)`

  14. `{`

  15. `System.out.println(Thread.currentThread().getName()+" : "+i);`

  16. `if(i==20)`

  17. `{`

  18. `new FirstThreadTest().start();`

  19. `new FirstThreadTest().start();`

  20. `}`

  21. `}`

  22. `}`

  23. `}`

上述代码中Thread.currentThread()方法返回当前正在执行的线程对象。getName()方法返回调用该方法的线程的名字。

二、通过Runnable接口创建线程类

(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

(3)调用线程对象的start()方法来启动该线程。

  1. `package com.thread;`

  2. `public class RunnableThreadTest implements Runnable`

  3. `{`

  4. `private int i;`

  5. `public void run()`

  6. `{`

  7. `for(i = 0;i <100;i++)`

  8. `{`

  9. `System.out.println(Thread.currentThread().getName()+" "+i);`

  10. `}`

  11. `}`

  12. `public static void main(String[] args)`

  13. `{`

  14. `for(int i = 0;i < 100;i++)`

  15. `{`

  16. `System.out.println(Thread.currentThread().getName()+" "+i);`

  17. `if(i==20)`

  18. `{`

  19. `RunnableThreadTest rtt = new RunnableThreadTest();`

  20. `new Thread(rtt,"新线程1").start();`

  21. `new Thread(rtt,"新线程2").start();`

  22. `}`

  23. `}`

  24. `}`

  25. `}`

三、通过Callable和Future创建线程

(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

  1. `package com.thread;`

  2. `import java.util.concurrent.Callable;`

  3. `import java.util.concurrent.ExecutionException;`

  4. `import java.util.concurrent.FutureTask;`

  5. `public class CallableThreadTest implements Callable<Integer>`

  6. `{`

  7. `public static void main(String[] args)`

  8. `{`

  9. `CallableThreadTest ctt = new CallableThreadTest();`

  10. `FutureTask<Integer> ft = new FutureTask<>(ctt);`

  11. `for(int i = 0;i < 100;i++)`

  12. `{`

  13. `System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);`

  14. `if(i==20)`

  15. `{`

  16. `new Thread(ft,"有返回值的线程").start();`

  17. `}`

  18. `}`

  19. `try`

  20. `{`

  21. `System.out.println("子线程的返回值:"+ft.get());`

  22. `} catch (InterruptedException e)`

  23. `{`

  24. `e.printStackTrace();`

  25. `} catch (ExecutionException e)`

  26. `{`

  27. `e.printStackTrace();`

  28. `}`

  29. `}`

  30. `@Override`

  31. `public Integer call() throws Exception`

  32. `{`

  33. `int i = 0;`

  34. `for(;i<100;i++)`

  35. `{`

  36. `System.out.println(Thread.currentThread().getName()+" "+i);`

  37. `}`

  38. `return i;`

  39. `}`

  40. `}`

创建线程的三种方式的对比

采用实现Runnable、Callable接口的方式创见多线程时,优势是:

线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是:

编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

使用继承Thread类的方式创建多线程时优势是:

编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

劣势是:

线程类已经继承了Thread类,所以不能再继承其他父类。

sleep() 、join()、yield()有什么区别

1、sleep()方法

在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 让其他线程有机会继续执行,但它并不释放对象锁。也就是如果有Synchronized同步块,其他线程仍然不能访问共享数据。注意该方法要捕获异常

比如有两个线程同时执行(没有Synchronized),一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完成后,低优先级的线程才能执行;但当高优先级的线程sleep(5000)后,低优先级就有机会执行了。

总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。

2、yield()方法

yield()方法和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同。

3、join()方法

Thread的非静态方法join()让一个线程B“加入”到另外一个线程A的尾部。在A执行完毕之前,B不能工作。

Thread t = new MyThread(); t.start(); t.join();

保证当前线程停止执行,直到该线程所加入的线程完成为止。然而,如果它加入的线程没有存活,则当前线程不需要停止。

说说 CountDownLatch 原理

参考:

分析CountDownLatch的实现原理

什么时候使用CountDownLatch

Java并发编程:CountDownLatch、CyclicBarrier和Semaphore

说说 CyclicBarrier 原理

参考:

JUC回顾之-CyclicBarrier底层实现和原理

说说 Semaphore 原理

JAVA多线程–信号量(Semaphore)

JUC回顾之-Semaphore底层实现和原理

说说 Exchanger 原理

java.util.concurrent.Exchanger应用范例与原理浅析

说说 CountDownLatch 与 CyclicBarrier 区别

尽量把CyclicBarrier和CountDownLatch的区别说通俗点

ThreadLocal 原理分析

Java并发编程:深入剖析ThreadLocal

讲讲线程池的实现原理

主要是ThreadPoolExecutor的实现原理

Java并发编程:线程池的使用

线程池的几种方式

newFixedThreadPool(int nThreads)

创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程

newCachedThreadPool()

创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制

newSingleThreadExecutor()

这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行

newScheduledThreadPool(int corePoolSize)

创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

举个栗子

  1. `private static final Executor exec=Executors.newFixedThreadPool(50);`

  2. `Runnable runnable=new Runnable(){`

  3. `public void run(){`

  4. `...`

  5. `}`

  6. `}`

  7. `exec.execute(runnable);`

  8. `Callable<Object> callable=new Callable<Object>() {`

  9. `public Object call() throws Exception {`

  10. `return null;`

  11. `}`

  12. `};`

  13. `Future future=executorService.submit(callable);`

  14. `future.get(); // 等待计算完成后,获取结果`

  15. `future.isDone(); // 如果任务已完成,则返回 true`

  16. `future.isCancelled(); // 如果在任务正常完成前将其取消,则返回 true`

  17. `future.cancel(true); // 试图取消对此任务的执行,true中断运行的任务,false允许正在运行的任务运行完成`

参考:

创建线程池的几种方式

线程的生命周期

新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态

(1)生命周期的五种状态

新建(new Thread)

当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。

例如:Thread t1=new Thread();

就绪(runnable)

线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。例如:t1.start();

运行(running)

线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。

死亡(dead)

当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。

自然终止:正常运行run()方法后终止

异常终止:调用stop()方法让一个线程终止运行

堵塞(blocked)

由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。

正在睡眠:用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。

正在等待:调用wait()方法。(调用motify()方法回到就绪状态)

被另一个线程所阻塞:调用suspend()方法。(调用resume()方法恢复)

参考:

线程的生命周期

锁机制

说说线程安全问题

线程安全是指要控制多个线程对某个资源的有序访问或修改,而在这些线程之间没有产生冲突。

在Java里,线程安全一般体现在两个方面:

1、多个thread对同一个java实例的访问(read和modify)不会相互干扰,它主要体现在关键字synchronized。如ArrayList和Vector,HashMap和Hashtable(后者每个方法前都有synchronized关键字)。如果你在interator一个List对象时,其它线程remove一个element,问题就出现了。

2、每个线程都有自己的字段,而不会在多个线程之间共享。它主要体现在java.lang.ThreadLocal类,而没有Java关键字支持,如像static、transient那样。

volatile 实现原理

聊聊并发(一)——深入分析Volatile的实现原理

悲观锁 乐观锁

乐观锁 悲观锁

是一种思想。可以用在很多方面。

比如数据库方面。

悲观锁就是for update(锁定查询的行)

乐观锁就是 version字段(比较跟上一次的版本号,如果一样则更新,如果失败则要重复读-比较-写的操作。)

JDK方面:

悲观锁就是sync

乐观锁就是原子类(内部使用CAS实现)

本质来说,就是悲观锁认为总会有人抢我的。

乐观锁就认为,基本没人抢。

CAS 乐观锁

乐观锁是一种思想,即认为读多写少,遇到并发写的可能性比较低,所以采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

CAS顶多算是乐观锁写那一步操作的一种实现方式罢了,不用CAS自己加锁也是可以的。

ABA 问题

ABA:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A,当前线程的CAS操作无法分辨当前V值是否发生过变化。

参考:

Java CAS 和ABA问题

乐观锁的业务场景及实现方式

乐观锁(Optimistic Lock):

每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

核心篇


数据存储

MySQL 索引使用的注意事项

参考:

mysql索引使用技巧及注意事项

说说反模式设计

参考:

每个程序员要注意的 9 种反模式

说说分库与分表设计

分表与分库使用场景以及设计方式

分库与分表带来的分布式困境与应对之策

服务端指南 数据存储篇 | MySQL(09) 分库与分表带来的分布式困境与应对之策

说说 SQL 优化之道

sql优化的几种方法

MySQL 遇到的死锁问题

参考:

Mysql并发时经典常见的死锁原因及解决方法

存储引擎的 InnoDB 与 MyISAM

1)InnoDB支持事务,MyISAM不支持,这一点是非常之重要。事务是一种高级的处理方式,如在一些列增删改中只要哪个出错还可以回滚还原,而MyISAM就不可以了。

2)MyISAM适合查询以及插入为主的应用,InnoDB适合频繁修改以及涉及到安全性较高的应用

3)InnoDB支持外键,MyISAM不支持

4)从MySQL5.5.5以后,InnoDB是默认引擎

5)InnoDB不支持FULLTEXT类型的索引

6)InnoDB中不保存表的行数,如select count() from table时,InnoDB需要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。注意的是,当count()语句包含where条件时MyISAM也需要扫描整个表

7)对于自增长的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中可以和其他字段一起建立联合索引

8)清空整个表时,InnoDB是一行一行的删除,效率非常慢。MyISAM则会重建表

9)InnoDB支持行锁(某些情况下还是锁整表,如 update table set a=1 where user like ‘%lee%’

参考:

MySQL存储引擎之MyIsam和Innodb总结性梳理

数据库索引的原理

参考:

http://blog.csdn.net/suifeng3051/article/details/52669644

为什么要用 B-tree

鉴于B-tree具有良好的定位特性,其常被用于对检索时间要求苛刻的场合,例如:

1、B-tree索引是数据库中存取和查找文件(称为记录或键值)的一种方法。

2、硬盘中的结点也是B-tree结构的。与内存相比,硬盘必须花成倍的时间来存取一个数据元素,这是因为硬盘的机械部件读写数据的速度远远赶不上纯电子媒体的内存。与一个结点两个分支的二元树相比,B-tree利用多个分支(称为子树)的结点,减少获取记录时所经历的结点数,从而达到节省存取时间的目的。

聚集索引与非聚集索引的区别

参考:

快速理解聚集索引和非聚集索引

limit 20000 加载很慢怎么解决

LIMIT n 等价于 LIMIT 0,n

此题总结一下就是让limit走索引去查询,例如:order by 索引字段,或者limit前面根where条件走索引字段等等。

参考:

MYSQL分页limit速度太慢优化方法

选择合适的分布式主键方案

参考:

分布式系统唯一ID生成方案汇总

选择合适的数据存储方案

  • 关系型数据库 MySQL

MySQL 是一个最流行的关系型数据库,在互联网产品中应用比较广泛。一般情况下,MySQL 数据库是选择的第一方案,基本上有 80% ~ 90% 的场景都是基于 MySQL 数据库的。因为,需要关系型数据库进行管理,此外,业务存在许多事务性的操作,需要保证事务的强一致性。同时,可能还存在一些复杂的 SQL 的查询。值得注意的是,前期尽量减少表的联合查询,便于后期数据量增大的情况下,做数据库的分库分表。

  • 内存数据库 Redis

随着数据量的增长,MySQL 已经满足不了大型互联网类应用的需求。因此,Redis 基于内存存储数据,可以极大的提高查询性能,对产品在架构上很好的补充。例如,为了提高服务端接口的访问速度,尽可能将读频率高的热点数据存放在 Redis 中。这个是非常典型的以空间换时间的策略,使用更多的内存换取 CPU 资源,通过增加系统的内存消耗,来加快程序的运行速度。

在某些场景下,可以充分的利用 Redis 的特性,大大提高效率。这些场景包括缓存,会话缓存,时效性,访问频率,计数器,社交列表,记录用户判定信息,交集、并集和差集,热门列表与排行榜,最新动态等。

使用 Redis 做缓存的时候,需要考虑数据不一致与脏读、缓存更新机制、缓存可用性、缓存服务降级、缓存穿透、缓存预热等缓存使用问题。

  • 文档数据库 MongoDB

MongoDB 是对传统关系型数据库的补充,它非常适合高伸缩性的场景,它是可扩展性的表结构。基于这点,可以将预期范围内,表结构可能会不断扩展的 MySQL 表结构,通过 MongoDB 来存储,这就可以保证表结构的扩展性。

此外,日志系统数据量特别大,如果用 MongoDB 数据库存储这些数据,利用分片集群支持海量数据,同时使用聚集分析和 MapReduce 的能力,是个很好的选择。

MongoDB 还适合存储大尺寸的数据,GridFS 存储方案就是基于 MongoDB 的分布式文件存储系统。

  • 列族数据库 HBase

HBase 适合海量数据的存储与高性能实时查询,它是运行于 HDFS 文件系统之上,并且作为 MapReduce 分布式处理的目标数据库,以支撑离线分析型应用。在数据仓库、数据集市、商业智能等领域发挥了越来越多的作用,在数以千计的企业中支撑着大量的大数据分析场景的应用。

  • 全文搜索引擎 ElasticSearch

在一般情况下,关系型数据库的模糊查询,都是通过 like 的方式进行查询。其中,like “value%” 可以使用索引,但是对于 like “%value%” 这样的方式,执行全表查询,这在数据量小的表,不存在性能问题,但是对于海量数据,全表扫描是非常可怕的事情。ElasticSearch 作为一个建立在全文搜索引擎 Apache Lucene 基础上的实时的分布式搜索和分析引擎,适用于处理实时搜索应用场景。此外,使用 ElasticSearch 全文搜索引擎,还可以支持多词条查询、匹配度与权重、自动联想、拼写纠错等高级功能。因此,可以使用 ElasticSearch 作为关系型数据库全文搜索的功能补充,将要进行全文搜索的数据缓存一份到 ElasticSearch 上,达到处理复杂的业务与提高查询速度的目的。

ElasticSearch 不仅仅适用于搜索场景,还非常适合日志处理与分析的场景。著名的 ELK 日志处理方案,由 ElasticSearch、Logstash 和 Kibana 三个组件组成,包括了日志收集、聚合、多维度查询、可视化显示等。

ObjectId 规则

参考:

MongoDB学习笔记~ObjectId主键的设计

mongodb中的_id的ObjectId的生成规则

聊聊 MongoDB 使用场景

参考:

什么场景应该用 MongoDB ?

倒排索引

参考:

什么是倒排索引?

聊聊 ElasticSearch 使用场景

在一般情况下,关系型数据库的模糊查询,都是通过 like 的方式进行查询。其中,like “value%” 可以使用索引,但是对于 like “%value%” 这样的方式,执行全表查询,这在数据量小的表,不存在性能问题,但是对于海量数据,全表扫描是非常可怕的事情。ElasticSearch 作为一个建立在全文搜索引擎 Apache Lucene 基础上的实时的分布式搜索和分析引擎,适用于处理实时搜索应用场景。此外,使用 ElasticSearch 全文搜索引擎,还可以支持多词条查询、匹配度与权重、自动联想、拼写纠错等高级功能。因此,可以使用 ElasticSearch 作为关系型数据库全文搜索的功能补充,将要进行全文搜索的数据缓存一份到 ElasticSearch 上,达到处理复杂的业务与提高查询速度的目的。

缓存使用

Redis 有哪些类型

Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。

参考:

Redis 数据类型

Redis 内部结构

参考:

redis内部数据结构深入浅出

聊聊 Redis 使用场景

随着数据量的增长,MySQL 已经满足不了大型互联网类应用的需求。因此,Redis 基于内存存储数据,可以极大的提高查询性能,对产品在架构上很好的补充。例如,为了提高服务端接口的访问速度,尽可能将读频率高的热点数据存放在 Redis 中。这个是非常典型的以空间换时间的策略,使用更多的内存换取 CPU 资源,通过增加系统的内存消耗,来加快程序的运行速度。

在某些场景下,可以充分的利用 Redis 的特性,大大提高效率。这些场景包括缓存,会话缓存,时效性,访问频率,计数器,社交列表,记录用户判定信息,交集、并集和差集,热门列表与排行榜,最新动态等。

使用 Redis 做缓存的时候,需要考虑数据不一致与脏读、缓存更新机制、缓存可用性、缓存服务降级、缓存穿透、缓存预热等缓存使用问题。

Redis 持久化机制

参考:

redis的持久化和缓存机制

Redis 如何实现持久化

参考:

Redis如何实现持久化

Redis 集群方案与实现

参考:

redis集群主流架构方案分析

Redis 为什么是单线程的

单纯的网络IO来说,量大到一定程度之后,多线程的确有优势——但并不是单纯的多线程,而是每个线程自己有自己的epoll这样的模型,也就是多线程和multiplexing混合。

一般这个开头我们都会跟一个“但是”。

但是。

还要考虑Redis操作的对象。它操作的对象是内存中的数据结构。如果在多线程中操作,那就需要为这些对象加锁。最终来说,多线程性能有提高,但是每个线程的效率严重下降了。而且程序的逻辑严重复杂化。

要知道Redis的数据结构并不全是简单的Key-Value,还有列表,hash,map等等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除一个对象,等等。这些操作还可以合成MULTI/EXEC的组。这样一个操作中可能就需要加非常多的锁,导致的结果是同步开销大大增加。这还带来一个恶果就是吞吐量虽然增大,但是响应延迟可能会增加。

Redis在权衡之后的选择是用单线程,突出自己功能的灵活性。在单线程基础上任何原子操作都可以几乎无代价地实现,多么复杂的数据结构都可以轻松运用,甚至可以使用Lua脚本这样的功能。对于多线程来说这需要高得多的代价。

并不是所有的KV数据库或者内存数据库都应该用单线程,比如ZooKeeper就是多线程的,最终还是看作者自己的意愿和取舍。单线程的威力实际上非常强大,每核心效率也非常高,在今天的虚拟化环境当中可以充分利用云化环境来提高资源利用率。多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的,所以单线程、多进程的集群不失为一个时髦的解决方案。

作者:灵剑

链接:https://www.zhihu.com/question/23162208/answer/142424042

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

缓存奔溃

参考:

Redis持久化

缓存降级

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

使用缓存的合理性问题

参考:

Redis实战(一) 使用缓存合理性

消息队列

消息队列的使用场景

主要解决应用耦合,异步消息,流量削锋等问题

消息队列使用的四种场景介绍

消息的重发补偿解决思路

参考:

JMS消息传送机制

消息的幂等性解决思路

参考:

MQ之如何做到消息幂等

消息的堆积解决思路

参考:

Sun Java System Message Queue 3.7 UR1 管理指南

自己如何实现消息队列

参考:

自己动手实现消息队列之JMS

如何保证消息的有序性

参考:

消息队列的exclusive consumer功能是如何保证消息有序和防止脑裂的

框架篇


Spring

BeanFactory 和 ApplicationContext 有什么区别

beanfactory顾名思义,它的核心概念就是bean工厂,用作于bean生命周期的管理,而applicationcontext这个概念就比较丰富了,单看名字(应用上下文)就能看出它包含的范围更广,它继承自bean factory但不仅仅是继承自这一个接口,还有继承了其他的接口,所以它不仅仅有bean factory相关概念,更是一个应用系统的上下文,其设计初衷应该是一个包罗万象的对外暴露的一个综合的API。

Spring Bean 的生命周期

参考:

Spring Bean生命周期详解

Spring IOC 如何实现

参考:

Spring:源码解读Spring IOC原理

说说 Spring AOP

参考:

Spring AOP详解

Spring AOP 实现原理

参考:

Spring AOP 实现原理

动态代理(cglib 与 JDK)

java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。

而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP

2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP

3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

如何强制使用CGLIB实现AOP?

(1)添加CGLIB库,SPRING_HOME/cglib/*.jar

(2)在spring配置文件中加入

JDK动态代理和CGLIB字节码生成的区别?

(1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类

(2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法

因为是继承,所以该类或方法最好不要声明成final

参考:

Spring的两种代理JDK和CGLIB的区别浅谈

Spring 事务实现方式

参考:

Spring事务管理实现方式之编程式事务与声明式事务详解

Spring 事务底层原理

参考:

深入理解 Spring 事务原理

如何自定义注解实现功能

可以结合spring的AOP,对注解进行拦截,提取注解。

大致流程为:

1. 新建一个注解@MyLog,加在需要注解申明的方法上面

2. 新建一个类MyLogAspect,通过@Aspect注解使该类成为切面类。

3. 通过@Pointcut 指定切入点 ,这里指定的切入点为MyLog注解类型,也就是被@MyLog注解修饰的方法,进入该切入点。

4. MyLogAspect中的方法通过加通知注解(@Before、@Around、@AfterReturning、@AfterThrowing、@After等各种通知)指定要做的业务操作。

Spring MVC 运行流程

一、先用文字描述

1.用户发送请求到DispatchServlet

2.DispatchServlet根据请求路径查询具体的Handler

3.HandlerMapping返回一个HandlerExcutionChain给DispatchServlet

HandlerExcutionChain:Handler和Interceptor集合

4.DispatchServlet调用HandlerAdapter适配器

5.HandlerAdapter调用具体的Handler处理业务

6.Handler处理结束返回一个具体的ModelAndView给适配器

ModelAndView:model–>数据模型,view–>视图名称

7.适配器将ModelAndView给DispatchServlet

8.DispatchServlet把视图名称给ViewResolver视图解析器

9.ViewResolver返回一个具体的视图给DispatchServlet

10.渲染视图

11.展示给用户

如何快速更新自己的技术积累?

  • 在现有的项目里,深挖技术,比如用到netty可以把相关底层代码和要点都看起来。
  • 如果不知道目前的努力方向,就看自己的领导或公司里技术强的人在学什么。
  • 知道努力方向后不知道该怎么学,就到处去找相关资料然后练习。
  • 学习以后不知道有没有学成,则可以通过面试去检验。

我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!

以上面试专题的答小编案整理成面试文档了,文档里有答案详解,以及其他一些大厂面试题目

八年CRUD,疫情备战三个月,三面头条、四面阿里拿offer面经分享

八年CRUD,疫情备战三个月,三面头条、四面阿里拿offer面经分享

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

xXzQxNzAxOTU2/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

Spring IOC 如何实现

参考:

Spring:源码解读Spring IOC原理

说说 Spring AOP

参考:

Spring AOP详解

Spring AOP 实现原理

参考:

Spring AOP 实现原理

动态代理(cglib 与 JDK)

java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。

而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP

2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP

3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

如何强制使用CGLIB实现AOP?

(1)添加CGLIB库,SPRING_HOME/cglib/*.jar

(2)在spring配置文件中加入

JDK动态代理和CGLIB字节码生成的区别?

(1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类

(2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法

因为是继承,所以该类或方法最好不要声明成final

参考:

Spring的两种代理JDK和CGLIB的区别浅谈

Spring 事务实现方式

参考:

Spring事务管理实现方式之编程式事务与声明式事务详解

Spring 事务底层原理

参考:

深入理解 Spring 事务原理

如何自定义注解实现功能

可以结合spring的AOP,对注解进行拦截,提取注解。

大致流程为:

1. 新建一个注解@MyLog,加在需要注解申明的方法上面

2. 新建一个类MyLogAspect,通过@Aspect注解使该类成为切面类。

3. 通过@Pointcut 指定切入点 ,这里指定的切入点为MyLog注解类型,也就是被@MyLog注解修饰的方法,进入该切入点。

4. MyLogAspect中的方法通过加通知注解(@Before、@Around、@AfterReturning、@AfterThrowing、@After等各种通知)指定要做的业务操作。

Spring MVC 运行流程

一、先用文字描述

1.用户发送请求到DispatchServlet

2.DispatchServlet根据请求路径查询具体的Handler

3.HandlerMapping返回一个HandlerExcutionChain给DispatchServlet

HandlerExcutionChain:Handler和Interceptor集合

4.DispatchServlet调用HandlerAdapter适配器

5.HandlerAdapter调用具体的Handler处理业务

6.Handler处理结束返回一个具体的ModelAndView给适配器

ModelAndView:model–>数据模型,view–>视图名称

7.适配器将ModelAndView给DispatchServlet

8.DispatchServlet把视图名称给ViewResolver视图解析器

9.ViewResolver返回一个具体的视图给DispatchServlet

10.渲染视图

11.展示给用户

如何快速更新自己的技术积累?

  • 在现有的项目里,深挖技术,比如用到netty可以把相关底层代码和要点都看起来。
  • 如果不知道目前的努力方向,就看自己的领导或公司里技术强的人在学什么。
  • 知道努力方向后不知道该怎么学,就到处去找相关资料然后练习。
  • 学习以后不知道有没有学成,则可以通过面试去检验。

我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!

以上面试专题的答小编案整理成面试文档了,文档里有答案详解,以及其他一些大厂面试题目

[外链图片转存中…(img-oU6Twhf2-1715775159358)]

[外链图片转存中…(img-KUCSFqAs-1715775159359)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值