Java面试题

MyBatis

1. MyBatis有几种分页方式?

Mybatis分页分为两种方式:

  1. 逻辑分页:通过RowBounds对象进行分页,它是一次性查询很多数据,然后在查询出来的数据中再次进行检索。
  2. 物理分页:通过分页插件PageHelper或者使用关键字limit的方式实现的分页。

注意:逻辑分页需要消耗大量的内存、有内存溢出的风险、对数据库压力较大。 物理分页是从数据库查询指定条数的数据,弥补了一次性全部查出的所有数据的种种缺点,比如需要大量的内存,对数据库查询压力较大等问题。

2.RowBounds 是一次性查询全部结果吗?

RowBounds 表面是在“所有”数据中检索数据,其实并非是一次性查询出所有数据,因为MyBatis 是对 jdbc 的封装,在 jdbc 驱动中有一个 Fetch Size 的配置,它规定了每次最多从数据库查询多少条数据,假如你要查询更多数据,它会在你执行 next()的时候,去查询更多的数据。

就好比你去自动取款机取 10000 元,但取款机每次最多能取 2500 元,所以你要取 4 次才能把钱取完。

只是对于 jdbc 来说,当你调用 next()的时候会自动帮你完成查询工作。这样做的好处可以有效的防止内存溢出。

3. #{}和${}的区别是什么?

  1. #{}表示的预编译处理,会将 sql 中的#{}替换为?,可以预防sql注入,提高系统安全

  2. ${}表示的是字符串替换,有sql注入的风险。比如传入 or 1=1,条件就会一直成立。

4. 如何预防SQL注入?

  1. 使用预编译的方式:使用预编译的SQL语句语义不会发生改变,黑客本事再大,也无法改变SQL语句的结构。

  2. 限制变量数据类型和格式,因为攻击者会因为不清楚数据类型和格式而加大注入难度

  3. 过滤特殊符号:对于无法确定固定格式的变量,一定要进行特殊符号过滤或转义处理。

5. 实体类属性名和数据表字段名不一致如何解决?

  1. 使用as语句对数据表自动进行重命名

  2. 通过<resultMap>来映射字段名和实体类属性名的一一对应的关系。

6. Mybatis分页插件的原理

自定义mybatis插件的本质就是使用拦截器去干预mybatis的执行:我们可以通过一个Interceptor拦截接口去管理mybatis的四个对象

  1. Executor:Executor调用SqlSession的增删改查操作并给增删改查操作传入SQL标签的详细信息及参数等信息,还负责创建另外三个对象来参与执行SQL语句。

  2. StatementHandler:拦截 SQL 语法构建的处理,用于执行SQL语句,另外它也实现了 MyBatis 的一级缓存;

  3. ParameterHandler:拦截参数的处理。

  4. ResultSetHandler:拦截结果集的处理。

7. Mybatis的一级缓存和二级缓存的区别

  1. 一级缓存:基于 SQLSession 的本地缓存,它的生命周期和 SQLSession 是一致的,当 Session flush 或 close 之后,该 Session 中的所有 Cache 将清空。有多个 SQLSession 或者分布式的环境中,可能会出现脏数据。一级缓存是默认开启的。

  2. 二级缓存:也是基于nameSpace(一个mapper.xml就是一个命名空间)的本地缓存,不同在于其存储作用域为 Mapper 级别的,如果多个SQLSession之间需要共享缓存,则需要使用到二级缓存,当一个SQLSession断开,共享的缓存还在。并且二级缓存可自定义存储源,如 Ehcache。默认不打开二级缓存。

8. Mybatis如何开启二级缓存

  1. yml配置文件中开启二级缓存

    mybatis:
      configuration:
        cache-enabled: true
  2. mapper.xml中使用cach标签

    <!-- 开启本mapper所在namespace的二级缓存 -->
    <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
  3. 如果是只用了mybatis的话,就找到mybatis-config.xml,然后加入上面两步的代码

  4. pojo实现Serializable接口

9. Mybatis中如何进行模糊查询

  1. Java代码执行的时候传递通配符%%

    List<User> userList=mapper.getUserLike("%李%");
  2. 在sql拼接中使用通配符!

    select * from mybatis.user where name like "%"#{value}"%"
  3. 可以使用cancat函数

    select * from mybatis.user where name concat("%",#{value},"%")  

Redis

1. 使用Redis有那些好处?

  1. 速度快:因为Redis是基于内存工作的数据库,所以读写速度非常快。

  2. 数据类型丰富:list,hash,set,zset,string等。

  3. 支持事务操作:单条命令都是原子性操作,对数据的更改要么全部成功,要么全部失败。

  4. 其它特性:可用于缓存,设置数据过期时间等。

2. 说说常用的Redis数据类型

  1. string :字符串

  1. set:无序集合(成员不能重复)

  2. zset :有序集合(成员不能重复,score可以重复)

  3. list :列表(可以重复,按照插入的顺序排序,可以在头部和尾部插入成员)

  4. hash:哈希,常用于存储对象

  5. geospatial:地理位置(可用于计算城市之间的距离,附件的人等)

  6. hyperloglog:基数统计(常用于统计网页的访问量:一人访问多次自会被统计一次)

  7. BitMap:位图(使用位存储,信息状态只有 0 和 1)

3. Redis是单进程单线程的?

Redis 是单进程单线程的,redis 利用队列技术将并发访问变为串行访问。

4. 一个字符串类型的值能存储最大容量是多少?

512MB

5. Redis 的持久化机制是什么?各自的优缺点?

Redis有RDB和AOF两种持久化机制。

RDB持久化:

  1. 解释:在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。

  2. 优点:RDB 非常适用于灾难恢复:它只有一个文件(dump.rdb),并且内容都非常紧凑(保存了某个时间点的所有数据集)。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。(RDB适合大规模数据恢复)

  3. 缺点:数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。fork进程的时候,会占用一定的内容空间。

AOF持久化

  1. 解释:将所有执行过的命令都记录下来,恢复的时候就把这个文件全部再执行一遍的方式。

  2. 优点:AOF 的默认策略为每秒钟执行一次,就算发生故障停机,也最多只会丢失一秒钟的数据。Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。AOF 文件是一个只进行追加操作的日志文件(append only log),即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机,等等), redis-check-aof 工具也可以轻易地修复这种问题。

  3. 缺点:AOF 文件比 RDB 文件大,且恢复速度慢。数据集大的时候,比 rdb 启动效率低。

6. Redis过期健的删除策略有那些?

  1. 定时删除

    • 在设置键的过期时间的同时,创建一个定时器timer,当定时器检测到键的过期时间来临的时候,对键进行立即删除的操作。

    • 这种删除策略对内存是比较友好的,能够保证内存中的key一旦过期就立即从内存中删除。但是当过期的键比较多的时候,占用的CPU的时间也比较多,对服务器的吞吐量也会造成影响。

  1. 惰性删除

    • 当发现键过期的时候不会立即删除,但是当每次从键空间中获取键时,检查该键是否过期。如果过期就删除该键,否则,就返回该键。

    • 这种情况下,如果出现大量过期的键,会占用大量的内存。

    • 惰性删除为redis服务器内置策略

  1. 定期删除

    • 每隔一段时间,程序就会对数据库进行一次扫描,删除一定数量的过期的键。实际应用中可以设置每隔多久扫描一次或者一次扫描多久来平衡CPU和内存的资源。

    • 配置方式:配置redis.conf 的hz选项,默认为10 (即1秒执行10次,100ms一次,值越大说明刷新频率越快,最Redis性能损耗也越大)配置redis.conf的maxmemory最大值,当已用内存超过maxmemory限定时,就会触发主动清理策略。

redis主要使用了惰性删除、定期删除的策略来解决redis中的过期的键删除问题。

7. Redis 的回收策略(淘汰策略)

Redis中并不是所有键都会设置过期时间,那这些键会一直的保存在内存中? 内存不会炸?

因此,当Redis所用内存达到maxmemory.上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制,Redis支持6种策略,如下所示:

  1. noeviction: 默认策略,当内存不足以容纳新写入数据时,新写入操作会报错, 应该也没人用吧。

  2. allkeys-lru: 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的Key。推荐使用,目前项目在用这种。[LRU: Least recently used]

  3. allkeys-random: 当内存不足以容纳新写入数据时,在键空间中,随机移除某个Key。 应该也没人用吧,你不删最少使用Key,去随机删。

  4. volatile-lru: 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的Key。这种情况一般是把Redis既当缓存,又做持久化存储的时候才用。不推荐。

  5. volatile-random: 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个Key,依然不 推荐。

  6. volatile-ttl: 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的Key优先移除。不推荐。如果没有对应的键,则回退到noeviction策略。[TTL:time to live]

8. 什么是缓存击穿、缓存穿透、缓存雪崩?

缓存击穿

  1. 缓存击穿: 指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到数据库。

缓存穿透

  1. 读请求来了,先查下缓存,缓存有值命中,就直接返回;缓存没命中,就去查数据库,然后把数据库的值更新到缓存,再返回。

  2. 缓存穿透:通俗点说,读请求访问时,缓存和数据库都没有某个值,这样就会导致每次对这个值的查询请求都会穿透到数据库,这就是缓存穿透。

  3. 如何避免缓存穿透:

    • 如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有有写请求进来的话,需要更新缓存哈,以保证缓存一致性,同时,最后给缓存设置适当的过期时间。

    • 使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。

缓存雪崩

  1. 指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接访问数据库,引起数据库压力过大甚至down机。

  2. 缓存雪崩一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如采用一个较大固定值+一个较小的随机值。

  3. Redis 故障宕机也可能引起缓存雪奔。这就需要构造Redis高可用集群啦。

9. 常见Redis集群方案

  1. Redis Sentinal(哨兵模式) 着眼于高可用,在 master 宕机时会自动将 slave 提升为master,继续提供服务。

  2. Redis Cluster 着眼于扩展性,在单个 redis 内存不足时,使用 Cluster 进行分片存储。

10. Pipeline 有什么好处,为什么要用 pipeline?

管道(pipeline)可以一次性发送多条命令并在执行完后一次性将结果返回,pipeline 通过减少客户端与 redis 的通信次数来实现降低往返延时时间

而且 Pipeline 实现的原理是队列,而队列的原理是时先进先出,这样就保证数据的顺序性。

11. Redis 集群最大节点个数是多少?

16384个

Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。

12. Redis 事务相关的命令有哪几个?

常见的命令如下:

  1. MULTI 开启事务

  2. EXEC 执行事务

  3. DISCARD 放弃事务

  4. WATCH 监视事务:被WATCH的键会被监视, 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消。

13. 如何查询相同前缀的key?

有两种方式:

  1. 可以使用scan命令:不会阻塞其它命令,但耗时长,还有一定的重复概率

  2. 可以使用keys命令:会阻塞其它命令的执行,因为redis都是单线程,但耗时短

14. 使用过 Redis 做异步队列么,你是怎么用的?

  1. 一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep 一会再重试。

    • 如果不使用sleep:list 还有个指令叫 blpop,在没有消息的时候,它会阻塞住直到消息到来。

  2. 如果想生产一次消费多次:

    • 使用 pub/sub 主题订阅者模式,可以实现1:N 的消息队列。

    • 缺点:在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 RabbitMQ 等

  3. 如果想实现延时队列

    • 可以使用zset,拿时间戳作为 score,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。

15. 使用过 Redis 分布式锁么,它是什么回事?

  1. 先拿 setnx 来争抢锁,抢到之后,再用 expire 给锁加一个过期时间防止锁忘记了释放。

  2. 最好是将setnx和expire合成一条命令来用,这样才不会违背原子性。这样当在setnx设置之后,expiere设置之前,进程意外崩溃也不用怕

SETEX key seconds value 就类似下面两个命令

SET key value
EXPIRE key seconds  # 设置生存时间

MySQL

1. MySQL 中有哪些不同的表格

共有 5 种类型的表格:

  1. MyISAM

  2. Heap

  3. Merge

  4. INNODB

  5. ISAM

2. MyISAM 和 InnoDB 的区别

名称MyISAMInnoDB
事务支持(ACID)不支持支持
行级锁不支持支持
外键约束不支持支持
全文索引支持不支持(在mysql5.6之后支持了全文索引)
空间大小比较小约为MyISAM的两倍

经验 ( 适用场合 ) :

  • 适用 MyISAM : 节约空间及相应速度

  • 适用 InnoDB : 安全性 , 事务处理及多用户操作数据

3. MyISAM Static 和 MyISAM Dynamic 有什么区别?

  • 在 MyISAM Static 上的所有字段有固定宽度。动态 (dynamic)MyISAM 表将具有像TEXT, BLOB 等字段,以适应不同长度的数据类型。

  • MyISAM Static 在受损情况下更容易恢复。

4. MySQL 中有哪几种锁

  1. 表级锁

    • 每次操作锁住整张表。

    • 开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

  2. 行级锁:

    • 行级锁,每次操作锁住对应的行数据

    • 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

5. InnoDB 四种事务的隔离级别

SQL 标准定义的四个隔离级别为:

  1. read uncommited :读到未提交数据

  2. read committed:脏读,不可重复读

  3. repeatable read:可重复读:MySQL的默认隔离级别

  4. serializable :串行事物

事务隔离级别脏读不可重复读幻读
读未提交
读已提交不会
可重复读不会不会
可串行化不会不会不会

6. 如何理解脏读、不可重复读、幻读

脏读

有两个事务A和B,A事务由于某些问题发生了错误,进行了回滚。B事务读取到了A事务回滚前的数据,就发生了脏读。

事务A修改了数据还未提交,就被事务B读取到了A事务修改后的数据,也会发生脏读。

不可重复读

在事务A中,读取到张三的工资为5000,操作没有完成,事务还没提交。

与此同时,事务B把张三的工资改为8000,并提交了事务。

随后,在事务A中,再次读取张三的工资,此时工资变为8000。在一个事务中前后两次读取的结果并不致,导致了不可重复读。

幻读

select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。

目前工资为5000的员工有10人,事务A读取所有工资为5000的人数为10人。此时,事务B插入一条工资也为5000的记录。这时,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。

7. 数据库中的事务是什么?

  • 事务(transaction)是作为一个单元的一组有序的数据库操作。如果组中的所有操作都成功,则认为事务成功,即使只有一个操作失败,事务也不成功。

  • 如果所有操作完成,事务则提交,其修改将作用于所有其他数据库进程。如果一个操作失败,则事务将回滚,该事务所有操作的影响都将取消。

事务特性:

  1. 原子性:即不可分割性,事务要么全部被执行,要么就全部不被执行。

  2. 一致性或可串性。事务的执行使得数据库从一种正确状态转换成另一种正确状态

  3. 隔离性在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务,

  4. 持久性。事务正确提交后,其结果将永久保存在数据库中,即使在事务提交后有了其他故障,事务的处理结果也会得到保存。

或者这样理解:

事务就是被绑定在一起作为一个逻辑工作单元的 SQL 语句分组,如果任何一个语句操作失败那么整个操作就被失败,以后操作就会回滚到操作前状态,或者是上一个节点。为了确保要么执行,要么不执行,就可以使用事务。要将一组语句作为事务考虑,就需要通过 ACID 测试,即原子性,一致性,隔离性和持久性。

8. char和vaChar的区别?

  1. 最大长度:char最大长度是255字符,varchar最大长度是65535个字节。

  2. 定长:char是定长的,不足的部分用隐藏空格填充,varchar是不定长的。

  3. 空间使用:char会浪费空间,varchar会更加节省空间。

  4. 查找效率:char查找效率会很高,varchar查找效率会更低。

  5. 尾部空格:char插入时可省略,varchar插入时不会省略,查找时省略。

9. 如果一个表有一列定义为 TIMESTAMP,将发生什么?

字段使用timeStamp时每当行被更改时,时间戳字段将获取当前时间戳。

10. 列设置为 AUTO INCREMENT 时,如果在表中达到最大值,会发生什么情况?

它会停止递增,任何进一步的插入都将产生错误,因为密钥已被使用。

11. 什么是非标准字符串类型?

  1. TINYTEXT 微文本字符串

  2. TEXT 文本串

  3. MEDIUMTEXT

  4. LONGTEXT

12. MySQL 里记录货币用什么字段类型好

numeric和 decimal类型被 MySQL 实现为同样的类型,这在 SQL92 标准允许。他们被用于保存值,该值的准确精度是极其重要的值,例如与金钱有关的数据。

当声明一个类是这些类型之一时,精度和规模的能被指定。 例如: salary DECIMAL(9,2) 在这个例子中,9(precision)代表将被用于存储值的总的小数位数,而 2(scale)代表将被用于存储小数点后的位数。 因此,在这种情况下,能被存储在 salary 列中的值的范围是从-9999999.99 到 9999999.99。

13. MySQl中关于日期的字段

  1. Datatime:以 YYYY-MM-DD HH:MM:SS 格式存储时期时间,精确到秒, 占用 8 个字节得存储空间,datatime 类型与时区无关

  2. Timestamp:以时间戳格式存储,占用 4 个字节,范围小 1970-1-1 到 2038-1-19, 显示依赖于所指定得时区,默认在第一个列行的数据修改时可以自动得修改 timestamp 列得值

  3. Date:(生日)使用 date 只 需要 3 个字节,存储日期月份,还可以利用日期时间函数进行日期间得计算 Time:存储时间部分得数据

  4. 注意:不要使用字符串类型来存储日期时间数据(通常比字符串占用得储存空间小, 在进行查找过滤可以利用日期得函数)

  5. 使用 int 存储日期时间不如使用 timestamp 类型

14. 超键、候选键、主键、外键分别是什么?

  1. 超键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。

  2. 候选键:是最小超键,即没有冗余元素的超键。

  3. 主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。主键也是候选键

  4. 外键:在一个表中存在的另一个表的主键称此表的外键。

15. 主键、外键和索引的区别?

主键、外键和索引的区别

定义:

  • 主键:唯一标识一条记录,不能有重复的,不允许为空

  • 外键:表的外键是另一表的主键, 外键可以有重复的, 可以是空值

  • 索引:该字段没有重复值,但可以有一个空值

作用:

  • 主键:用来保证数据完整性

  • 外键:用来和其他表建立联系用的

  • 索引:是提高查询排序的速度

个数:

  • 主键:主键只能有一个

  • 外键:一个表可以有多个外键

  • 索引:一个表可以有多个唯一索引

16. MySQL 如何优化 DISTINCT?

  • DISTINCT 在所有列上转换为 GROUP BY,并与 ORDER BY 子句结合使用。

  • 使用索引优化DISTINCT

17. NOW()和 CURRENT_DATE()有什么区别?

  • NOW()命令用于显示当前年份,月份,日期,小时,分钟和秒。

  • CURRENT_DATE()仅显示当前年份,月份和日期。

18. 你怎么看到为表格定义的所有索引?

索引是通过以下方式为表格定义的: SHOW INDEX FROM tablename;

19. 可以使用多少列创建索引?

任何标准表最多可以创建 16 个索引列。

20. 索引的底层实现原理和优化

B+树,经过优化的 B+树 主要是在所有的叶子结点中增加了指向下一个叶子节点的指针,因此 InnoDB 建

议为大部分表使用默认自增的主键作为主索引。

21. 什么情况下设置了索引但无法使用?

  1. 以“%”开头的 LIKE 语句,模糊匹配

  2. OR 语句前后没有同时使用索引

  3. 数据类型出现隐式转化(如 varchar 不加单引号的话可能会自动转换为 int 型)

22. 对于关系型数据库而言,索引是相当重要的概念,请回答有关索引的几个问题:

  1. 索引的目的是什么?

    • 快速访问数据表中的特定信息,提高检索速度

    • 创建唯一性索引,保证数据库表中每一行数据的唯一性。

    • 加速表和表之间的连接

    • 使用分组和排序子句进行数据检索时,可以显著减少查询中分组和排序的时间

  2. 索引对数据库系统的负面影响是什么?

    • 创建索引和维护索引需要耗费时间,这个时间随着数据量的增加而增加;

    • 索引需要占用物理空间,不光是表需要占用数据空间,每个索引也需要占用物理空间;

    • 当对表进行增、删、改、的时候索引也要动态维护,这样就降低了数据的维护速度。

  3. 为数据表建立索引的原则有哪些?

    • 在最频繁使用的、用以缩小查询范围的字段上建立索引。

    • 在频繁使用的、需要排序的字段上建立索引

  4. 什么情况下不宜建立索引?

    • 对于查询中很少涉及的列或者重复值比较多的列,不宜建立索引。

    • 对于一些特殊的数据类型,不宜建立索引,比如文本字段(text)等

23. LIKE 声明中的%和_是什么意思

%对应于 0 个或更多字符,_只是 LIKE 语句中的一个字符。

# 匹配包含"yyds"的记录
SELECT FROM tableName WHERE name like '%yyds%';
#匹配结果为: 像"yyds"这样记录
SELECT FROM tableName WHERE name like 'y_ds';  

24. 什么叫视图?游标是什么?

视图

  • 视图是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改, 查,操作,视图通常是有一个表或者多个表的行或列的子集。

  • 视图并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的。通俗的讲,视图就是一条SELECT语包执行后返回的结果集。所以我们在创建视图的时候,主要的工作 就落在创建这条SQL查询语句上。

  • 对视图的修改不影响基本表。它使得我们获取数据更容易,相比多表查询。

游标

  • 游标:是对查询出来的结果集作为一个单元来有效的处理。游标可以定在该单元中的特定行,从结果集的当前行检索一行或多行。可以对结果集当前行做修改。

  • 一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。

25. 视图的优势

  • 简单:使用视图的用户完全不需要关心后面对应的表的结构、关联条件和筛选条件,对用户来说已经是过滤好的复合条件的结果集。

  • 安全:使用视图的用户只能访问他们被允许查询的结果集,对表的权限管理并不能限制到某个行某个列,但是通过视图就可以简单的实现。

  • 数据独立:一旦视图的结构确定了,可以屏蔽表结构变化对用户的影响,源表增加列对视图没有影响;源表修改列名,则可以通过修改视图来解决,不会造成对访问者的影响。

26. 什么是存储过程?用什么来调用?

答:存储过程是一个预编译的 SQL 语句,优点是允许模块化的设计,就是说只需创建一次,以后在该程序中就可以调用多次。如果某次操作需要执行多次 SQL, 使用存储过程比单纯 SQL 语句执行要快。可以用一个命令对象来调用存储过程。

27. 如何通俗地理解三个范式?

  1. 第一范式:1NF 是对属性的原子性约束,要求属性具有原子性,不可再分解;(列不可再分)

  2. 第二范式:2NF 是对记录的惟一性约束,要求记录有惟一标识,即实体的惟一性;(表不可再分)

  3. 第三范式:3NF 是对字段冗余性的约束,即任何字段不能由其他字段派生出来,它要求字段没有冗余。也就是说表中的字段和主键直接对应不依靠其他中间字段,说白了就是,决定某字段值的必须是主键。(不应该:主键-A字段-B字段)(应该:主键-A,主键-B)(所有字段只与主键相关,与其他字段无关)

28. 范式化设计优缺点:

优点: 可以尽量得减少数据冗余,使得更新快,体积小

缺点:对于查询需要多个表进行关联,减少写得效率增加读得效率,更难进行索引

29. 反范式化

优点:可以减少表得关联,可以更好得进行索引优化

缺点:数据冗余以及数据异常,数据得修改需要更多的成本

多线程

1. 线程与进程的区别?

  • 进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。

  • 一个程序至少有一个进程,一个进程至少有一个线程。

  • 一个进程可以有多个线程,如视频中同时听声音,看图像,显示字幕。

2. 什么是饥饿?

  • 一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

3. Java 中导致饥饿的原因?

  1. 高优先级线程吞噬所有的低优先级线程的 CPU 时间。

  2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。

  3. 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

4. 线程的状态有那些?

5. 如何停止一个正在运行的线程?

  • 提供了一个boolean型的终止变量,当这个变量置为false,则线程就终止运行。

  • 使用 interrupt 方法终止线程:如果一个线程由于等待某些事件的发生而被阻塞使线程处于处于不可运行状态时,即使主程序中将该线程 的共享变量设置为 true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。这里我们给出的建议是,不要使用 stop()方法,而是使用 Thread 提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一 个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出堵塞代码。

  • 使用stop方法:不建议使用

6. 简单说说volatile关键字

  • synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁。而volatile就可以说是java虚拟机提供的最轻量级的同步机制。

  • synchronized保证了并发和同步,volatile保证了同步的数据可见。由此可知volatile实现了synchronized的部分功能。

  • 在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令

    • Lock前缀的指令会引起处理器缓存写回内存;

    • 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;

  • volatile保证了数据的可见性:通过volatile我们就可以及时读取到最新的数据

    • 在处理线程问题时我们可能会遇到读数据的时候并没有最新的数据,可能会读到工作内存中的旧数据,这是因为工作内存还没来得及从主内存中获取到最新的数据。

    • 工作内存的作用就是拷贝主内存中的数据,提高效率。

    • 线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。

  • volatile不能够保证原子性。

  • volatile可以避免指令重排

    • 添加内存屏障可以阻止指令重排

    • volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障

7. Java 中用到的线程调度算法是什么?

采用时间片轮转的方式。可以设置线程的优先级,会映射到下层的系统上面的优先级上,如非特别需要,尽量不要用,防止线程饥饿

8. 什么是线程组,为什么在 Java 中不推荐使用?

  • ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象, 也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。

  • 为什么不推荐使用?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。

9. 为什么我们调用 start()方法时会执行 run()方法,为什么我们不能直接调用 run()方法?

  • 当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码。

  • 但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码, 只会把 run 方法当作普通方法去执行。

10. 什么是不可变对象,它对写并发应用有什么帮助?

  • 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。

  • 不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。

  • 不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们的状态无法修改,这些常量永远不会变。

11. Java Concurrency API 中的 Lock 接口是什么?对比同步它有什么优势?

Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。 他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

它的优势有

  • 可以使锁更公平

  • 可以使线程在等待锁的时候响应中断

  • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间

  • 可以在不同的范围,以不同的顺序获取和释放锁

  • 整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的 (tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。

  • 另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非 公平锁是高效的选择。

12. 什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?

  • 原子(atom)本意是“不能被进一步分割的小粒子”,而原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。就像是我们的mysql里面的提到的ACID,原子性,也是不可分割的操作,最小的单位。

  • 原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。

  • int++并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程有可能会读到之前的值,这就会引发错误。

  • 为了解决这个问题,必须保证增加操作是原子的,在 JDK1.5 之前我们可以使用同步技术来做到这一点。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和 long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。

  1. 原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

  2. 原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

  3. 原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

13. 如何在两个线程间共享数据?

  • 在两个线程间共享变量即可实现共享。

  • 一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如果有对共享变量的复合操作,那么也得保证复合操作的线程安全性。

14. 为什么你应该在循环中检查等待条件?

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。 【使用 if 判断存在虚假唤醒,所以使用while】

15. 同步方法和同步块,哪个是更好的选择?

  • 同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。

  • 同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。

16. ExcutorService类的作用

  • ExecutorService是Java提供的线程池,也就是说,每次我们需要使用线程的时候,可以通过ExecutorService获得线程。

  • 如下面的创建方式:

    //可缓存的线程池
    ExecutorService cachePool = Executors.newCachedThreadPool();
    //单线程池
    ExecutorService SinglePool = Executors.newSingleThreadExecutor();
    //创建一个定长线程池,可控制线程最大并发数。
    ExecutorService singlePool = Executors.newFixedThreadPool(3);   
    //创建一个定长线程池,支持定时及周期性任务执 
    newScheduledThreadPool 

17. 为什么使用 Executor 框架创建和管理线程好?

  • Executor框架由三大部分组成

    1. 任务:即被执行任务需要实现的接口:Runnable接口或Callable接口

    2. 线程池:主要通过ExecutorService接口调用线程池,有2个关键实现类ThreadPoolExecutor和ScheduledThreadPoolExecutor

    3. 异步计算的结果:Future接口及其实现类FutureTask

  • 为什么要使用 Executor 线程池框架

    1. 每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。

    2. 调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。

    3. 直接使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、 定时定期执行、线程中断等都不便实现。

  • 使用 Executor 线程池框架的优点

    1. 能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开销。

    2. 可有效控制最大并发线程数,提高系统资源使用率,同时避免过多资源竞争

    3. 框架中已经有定时、定期、单线程、并发数控制等功能。

综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。 下面阿里巴巴规范建议:

18. 什么是线程池?有哪几种创建方式?

  • 线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。

  • 由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。

  • java 提供了一个 java.util.concurrent.Executor 接口的实现用于创建线程池。

19. 线程池的优点?

  1. 重用存在的线程,减少对象创建销毁的开销

  2. 可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞 争,避免堵塞。

  3. 提供定时执行、定期执行、单线程、并发数控制等功能。

20 . 什么是 Callable 和 Future?

  • Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常

  • 而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。 可以认为是带有回调的 Runnable。

  • Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable 用于产生结果,Future 用于获取结果。

  • Future接口获取结果是阻塞式的,并且如果不想提供可用的结果,可用返回一个null作为结果。

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

  1. 采用实现 Runnable、Callable 接口的方式创建多线程。

    • 优势是: 线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。 在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

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

  2. 使用继承 Thread 类的方式创建多线程

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

    • 劣势是: 线程类已经继承了 Thread 类,所以不能再继承其他父类。

  3. Runnable 和 Callable 的区别

    • Callable 规定(重写)的方法是 call(),Runnable 规定(重写)的方法是 run()。

    • Callable 的任务执行后可返回值,而 Runnable 的任务是不能返回值的。

    • Call 方法可以抛出异常,run 方法不可以。

  4. 运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

22. 线程之间是如何通信的?

  • 当线程间是可以共享资源时,线程间通信是协调它们的重要的手段。

  • Object 类中 wait()\notify()\notifyAll()方法可以用于线程间通信关于资源的锁的状态。

23. 在 java 中 wait 和 sleep 方法的不同?

  • sleep方法

    1. 指定当前线程阻塞的毫秒数

    2. 存在异常InterruptedException

    3. sleep指定的时间过去后,线程进入就绪状态。

    4. sleep可以用来模拟网络延时,倒计时等

    5. 每一个对象都有一个锁,sleep不会释放锁。

  • wait方法

    1. wait():表示线程一直等待,直到其它线程通知,与sleep不同,会释放锁。

    2. wait(ling timeout):指定等待的毫秒数

  • 最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。Wait 通常被用于线 程间交互,sleep 通常被用于暂停执行。

24. 说说线程的优先级

  • java 中的线程优先级的范围是1~10,默认的优先级是5。

  • 10级最高。

  • 在时间片轮循机制下“高优先级线程”被分配CPU的概率高于“低优先级线程”。但是这并不能保证高优先级的线程会在低优先级的线程前执行。

//获取线程的优先级
Thread.currentThread().getPriority()
//设置线程的优先级
Thread t=new Thread(); 
t.setPriority(3);

25. 守护线程和用户线程区别?

  • 线程分为守护线程和用户线程。

  • 虚拟机必须确定用户线程执行完成,不用等待守护线程是否执行完毕。

  • 守护线程使用场景:后台记录操作日志,监控内存,垃圾回收等

  • 守护线程服务于用户线程,JVM的停止不用等待守护线程执行完毕。

  • 一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作

//默认false表示是用户线程,正常的线程都是用户线程
 thread.setDaemon(true);

26. notify()和 notifyAll()有什么区别?

  • 当一个线程进入 wait 之后,就必须等其他线程 notify/notifyall

  • 使用 notifyall,可以唤醒所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个。

27. 怎么检测一个线程是否拥有锁?

在 java.lang.Thread 中有一个方法叫 holdsLock(),它返回 true 如果当且仅当当前线程拥有某个具体对象的锁。

28. Thread 类中的 yield 方法有什么作用?

  • 线程的礼让就是让当前正在执行的线程暂停。

  • 不是阻塞线程,而是将线程的运行状态转入就绪状态。让cpu调度器重新调度。

  • 礼让不一定成功,因为礼让后仍然为就绪状态,还是会进行竞争cpu资源。

  • 写在那个线程体中那个线程就进行礼让。

29. Java 线程池中 submit() 和 execute()方法有什么区别?

  • 两个方法都可以向线程池提交任务,execute()方法的返回类型是 void,它定义在 Executor 接口中。

  • 而 submit()方法可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。

30. 你如何确保 main()方法所在的线程是 Java 程序最后结束的线程?

我们可以使用 Thread 类的 join()方法来确保所有程序创建的线程在 main()方法退出前结束。

  • join合并线程,待此线程执行完成后,再执行其它的线程。其它的线程就处于阻塞状态

  • 可以理解为线程的插队,当一辆车(线程)插队后,被插队的车辆(其它线程)就只能让插队车辆先行,因为被插队,所以在插队的过程中,被插队的车辆处于阻塞的状态(线程阻塞),当插队完成(线程执行完毕),其它车辆继续驾驶(执行其它线程)。

  • 写在那个线程体中那个线程就被阻塞了。

31. 说说你对锁的理解

  1. 现时生活中,我们会面临一个资源,多个人都想使用的问题。解决方法当然就是排队。

  2. 处理多线程的时候,多个线程访问同一个对象,并且还需要修改这个对象,这时我们就需要用到线程同步

  3. 线程同步起始就是一种等待机制,多个需要访问同一个对象的线程进入对象的等待池形成队列。等待前面的线程使用完毕,下一个线程再使用。

  4. 使用资源的线程将资源锁定,此时只有它自己能够使用,直到结束,下一个再进行锁定,使用资源。

  5. 由于同一个进程的多个线程共享同一块存储空间,会造成访问冲突的问题,因此为了保护访问时的正确性,我们在访问的时候加入了锁机制

  6. 当线程获得了锁机制就能独占资源,其它线程必须等待。

32. 锁带来的问题

  1. 一个线程使用锁,会导致其它需要此锁的线程挂起。

  2. 多线程的竞争下,频繁的加锁和释放锁会造成较多的上下文切换和调度延时,引起性能问题。

  3. 一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。

33. 什么是并发

并发是指同一个对象被多个线程同时操作。【下面的两个例子会导致线程不安全】

  1. 买火车票:多个人同时买一张火车票

  2. 同一个银行账户,一个人在柜台取钱的同时,另一个人在机器上面取钱。

34. 并发编程三要素?

  1. 原子性:要么全部执行,要么就全部都不执行。

  2. 可见性:可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

  3. 有序性:有序性,即程序的执行顺序按照代码的先后顺序来执行。

35. 实现可见性的方法有哪些?

synchronized 或者 Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。

36. 什么是死锁

  • 多个线程各自占有一些共有的资源,并且互相等待其它线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情景。

  • 死锁的简单理解:甲:一手交钱,一手交货。乙:一手交货,一手交钱。

37. 产生死锁的必要条件

  1. 互斥条件:一个资源每次只能被一个进程使用。

  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放

  3. 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。

  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

38. 什么是活锁?

  • 任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试, 失败,尝试,失败。

  • 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待

  • 活锁有可能自行解开,死锁则不能。

39. 如何解决集合不安全的问题

  1. 在集合修改数据的时候加锁

  2. 使用线程安全的集合

    • list-------CopyOnWriteArrayList

    • set-------CopyOnWriteArraySet

40. 什么是并发容器?

  • 何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable, 以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。 可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。

  • 并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性, 例如在ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。

41. 什么是可重入锁(ReentrantLock)?

举例来说明锁的可重入性

public class UnReentrant{ 
    Lock lock = new Lock(); 
    public void outer(){ 
        lock.lock(); 
        inner(); 
        lock.unlock(); 
    } 
    public void inner(){ 
    lock.lock(); 
    //do something 
    lock.unlock(); 
​
    } 
} 
  • outer 中调用了 inner,outer 先锁住了 lock,这样 inner 就不能再获取 lock。其实调用 outer 的线程已经获取了 lock 锁,但是不能在 inner 中重复利用已经获取的锁资源,这种锁即称之为不可重入。

  • 可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。 如:synchronized、ReentrantLock 都是可重入的锁,可重入锁相对来说简化了并发编程的开发。

42. 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

  • 悲观锁

    1. 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

    2. 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。

  • 乐观锁

    1. 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据, 可以使用版本号等机制。

    2. 乐观锁适用于多读的应用类型,这样可以提高吞吐量。

    3. 像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

  • 乐观锁的实现方式

    1. 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。

    2. java 中的 Compare and Swap 即 CAS (比较再交换),当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

    3. 对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

  • CAS 缺点

    1. ABA 问题: 比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A, 这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。

    2. 循环时间长开销大: 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。

    3. 只能保证一个共享变量的原子操作: 当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作, 但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

43. ReadWriteLock 是什么

  • 首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在 读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据, 读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。 因为这个,才诞生了读写锁 ReadWriteLock。

  • ReadWriteLock 是一个读写锁接口, ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、 写和写之间才会互斥,提升了读写的性能。

44. synchronized 和 ReentrantLock 的区别

  • synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类, 这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的 类变量,ReentrantLock 比 synchronized 的扩展性体现在几点上:

  1. ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁

  2. ReentrantLock 可以获取各种锁的信息

  3. ReentrantLock 可以灵活地实现多路通知

45. 什么是阻塞队列?阻塞队列的实现原理是什么?如何使用 阻塞队列来实现生产者-消费者模型?

  1. 写入(put):如果队列满了就必须阻塞等待。

  2. 取(take): 如果队列是空的,就必须阻塞等待生产。

  3. 队列遵循FIFO原则,即first input first output 先进先出原则。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

JDK7 提供了 7 个阻塞队列。分别是

  1. ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

  2. LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

  3. PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

  4. DelayQueue:一个使用优先级队列实现的无界阻塞队列。

  5. SynchronousQueue:一个不存储元素的阻塞队列。

  6. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

  7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

46. 在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别

  • CyclicBarrier 可以重复使用,而 CountdownLatch 不能重复使用。

  • Java 的 concurrent 包里面的 CountDownLatch 其实可以把它看作一个减法计数器, 只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器。

  • 你可以向 CountDownLatch 对象设置一个初始的数字作为计数值,任何调用这个对象上的 await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为 0 为止。

  • 所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。

  • 这如果需要重置计数,请考虑使用 CyclicBarrier。

  • CyclicBarrier 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

47. SynchronizedMap 和 ConcurrentHashMap 有什么区别?

  • SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map

  • ConcurrentHashMap 使用分段锁来保证在多线程下的性能。

  • ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。 这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。

  • 另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当 iterator 被创建后集合再发生改变就不再是抛出 ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据 ,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。

48. CopyOnWriteArrayList 可以用于什么应用场景?

  • CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。

  • 在 CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。

  • 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc;

  • 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;

CopyOnWriteArrayList 透露的思想

  1. 读写分离,读和写分开

  2. 最终一致性

  3. 使用另外开辟空间的思路,来解决并发冲突

49. 什么是 ThreadLocal 变量?

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

50. 什么是 Java Timer 类?如何创建一个有特定时间间隔的任务?

  • java.util.Timer 是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。

  • Timer 类可以用安排一次性任务或者周期任务。

  • java.util.TimerTask 是一个实现了 Runnable 接口的抽象类,我们需要去继承这 个类来创建我们自己的定时任务并使用 Timer 去安排它的执行。

Java

1. 面向对象的特征有哪些方面?

  • 继承

  • 封装

  • 多态性

2. 访问修饰符 public,private,protected,以及不写(默认)时的区别?

3. String 是最基本的数据类型吗?

  • 不是。Java 中的基本数据类型只有 8 个:byte、short、int、long、float、double、 char、boolean;

  • 除了基本类型(primitive type),剩下的都是引用类型(reference type),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。

4. float f=3.4;是否正确?

  • 不正确:3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型,也称为窄化,会造成精度损失。

  • 因此需要强制类型转换 float f =(float)3.4; 或者写成 float f =3.4F;

5. short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗?

  • 对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。

  • 而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short)(s1 + 1);其中有隐含的强制类型转换。

6. Java 有没有 goto?

  • goto 是 Java 中的保留字,在目前版本的 Java 中没有使用。

7. int 和 Integer 有什么区别?

  • Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,

  • 从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。

  • Java 为每个原始类型提供了包装类型:

    1. 原始类型: boolean,char,byte,short,int,long,float,double

    2. 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float, Double

class AutoUnboxingTest { 
    public static void main(String[] args) { 
        Integer a = new Integer(3); 
        // 将 3 自动装箱成 Integer 类型 
        Integer b = 3; 
        int c = 3; 
        // false 两个引用没有引用同一对象 
        System.out.println(a == b); 
        // true a 自动拆箱成 int 类型再和 c 比较 
        System.out.println(a == c); 
    } 
​
} 

最近还遇到一个面试题,也是和自动装箱和拆箱有点关系的,代码如下所示:

public class Test03 { 
    public static void main(String[] args) { 
        Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150; 
        System.out.println(f1 == f2); //true
        System.out.println(f3 == f4);//false
    } 
}
//简单的说,如果整型字面量的值在-128 到 127 之间,那么不会 new 新的 Integer对象,而是直接引用常量池中的 Integer 对象,所以上面的面试题中 f1f4 的结果是 false。

8. &和&&的区别?

  • &&会造成短路:当一边的条件不成立(false),另一边就不会再运算

  • &不会造成短路,当一边的条件不成立,另一个还是会进行运算

9. 解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。

  1. 堆的作用

    • Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,是虚拟机所管理的内存中最大的一块。

    • 此内存区域的唯一目的就是存放对象实例(new 关键字和构造器创建的对象)和数组,几乎所有的对象实例和数组都在这里分配内存。

    • Java堆是垃圾收集器管理的主要区域,也称为GC垃圾堆。

  2. 栈的作用

    • 通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用 JVM 中的栈空间。

  3. 方法区的作用:

    • 方法区又被称为静态区,是程序中永远唯一的元素存储区域。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

    • 和堆一样,是各个线程共享的内存区域

    • 运行时常量池是方法区的一部分。

示例

String str = new String("hello");

上面的语句中变量 str 放在栈上,用 new 创建出来的字符串对象放在堆上,而” hello”这个字面量是放在方法区的。

10. Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?

Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。

11. switch 是否能作用在 byte 上,是否能作用在 long 上, 是否能作用在 String 上?

  • 在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。

  • 从 Java 5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,

  • 从 Java 7 开始, expr 还可以是字符串(String)

  • 但是长整型(long)在目前所有的版本中都是不可以的。

12. 用最有效率的方法计算 2 乘以 8?

2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次方)。

13. 数组有没有 length()方法?String 有没有 length()方法?

  • 数组没有 length()方法,有 length 的属性。

  • String 有 length()方法。JavaScript中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。

14. 在 Java 中,如何跳出当前的多重嵌套循环?

  • 在最外层循环前加一个标记如 A,然后用 break A;可以跳出多重循环。

15. 构造器(constructor)是否可被重写(override)?

  • 构造器不能被继承,因此不能被重写,但可以被重载。

16. 两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?

  • 不对,如果两个对象 x 和 y 满足 x.equals(y) == true,它们的哈希码(hash code)应当相同。

  • Java 对于 eqauls 方法和 hashCode 方法是这样规定的

    • 如果两个对象相同(equals 方法返回 true),那么它们的 hashCode 值一定要相同;

    • 如果两个对象的 hashCode 相同,它们并不一定相同。

补充

  • equals 方法必须满足自反性(x.equals(x)必须返回 true)

  • 对称性(x.equals(y)返回 true 时,y.equals(x)也必须返回 true)

  • 传递性 (x.equals(y)和 y.equals(z)都返回 true 时,x.equals(z)也必须返回 true)

  • 一致性(当 x 和 y 引用的对象信息没有被修改时,多次调用 x.equals(y)应该得到同样的返回值

  • 而且对于任何非 null 值的引用 x,x.equals(null)必须返回 false。

17. 是否可以继承 String 类?

String 类是 final 类,不可以被继承。

18. 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?

是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。

19. String 和 StringBuilder、StringBuffer 的区别?

  • 其中 String 是只读字符串,也就意味着 String 引用的字符串内容是不能被改变的。

  • 而 StringBuffer/StringBuilder 类表示的字符串对象可以直接进行修改。

  • StringBuilder 和 StringBuffer 的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方面都没有被synchronized 修饰,因此它的效率也比 StringBuffer 要高。

  • 字符串的+操作其本质是创建了 StringBuilder 对象进行 append 操作,然后将拼接后的 StringBuilder 对象用 toString 方法处理成 String 对象。

20. 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?

  • 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性, 而后者实现的是运行时的多态性。

  • 重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;

  • 重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。

  • 重载对返回类型没有特殊的要求。

21. 抽象类(abstract class)和接口(interface)有什么异同</font>

  • 抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。

  • 一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。

  • 接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法

  • 抽象类中的成员可以是 private、默认、protected、 public 的,而接口中的成员全都是 public 的。抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。

  • 有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。

22. 静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同?

  • Static Nested Class 是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。

  • 而一般的内部类需要在外部类实例化后才能实例化

23. 抽象的(abstract)方法是否可同时是静态的(static),是否可同时是本地方法(native),是否可同时被 synchronized修饰

  • 都不能。

  • 抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。

  • 本地方法是由本地代码(如 C 代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。

  • synchronized 和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。

24. 是否可以从一个静态(static)方法内部发出对非静态non-static)方法的调用?

不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在调用静态方法时可能对象并没有被初始化。

25. 如何实现对象克隆?

有两种方式:

  1. 实现 Cloneable 接口并重写 Object 类中的 clone()方法;

  2. 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下。

26. 深克隆可浅克隆的区别

  • 浅克隆的成员变量hash值是相同的,修改对象的成员变量,克隆对象的成员变量也会跟着变

  • 浅克隆的成员变量hash值是不相同的,修改对象的成员变量,克隆对象的成员变量不会跟着变

  • 成员变量:比如Teacher类中申明了Student类变量,student就是Teacher的成员变量。

    Class Teacher{
        Student student;
    }

27. String s = new String(“xyz”);创建了几个字符串对象?

两个对象,一个是静态区的”xyz”,一个是用 new 创建在堆上的对象。

28. 接口是否可继承(extends)接口?抽象类是否可实现(implements)接口?抽象类是否可继承具体类

  • 接口可以继承接口,而且支持多重继承。

  • 抽象类可以实现(implements)接口,抽象类可继承具体类也可以继承抽象类。

29. Java 中的 final 关键字有哪些用法?

  • 修饰类:表示该类不能被继承;

  • 修饰方法:表示方法不能被重写;

  • 修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。

30. 什么时候用断言(assert)?

断言不应该以任何方式改变程序的状态。简单的说,如果希望在不满足某些条件时阻止代码的执行,就可以考虑用断言来阻止它。

31. 阐述 JDBC 操作数据库的步骤。

  1. 加载驱动:Class.forName("oracle.jdbc.driver.OracleDriver");

  2. 创建连接。 Connection con = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl", "scott", "tiger");

  3. 创建语句:PreparedStatement ps = con.prepareStatement("select * from emp where sal between ? and ?"); ps.setInt(1, 1000); ps.setInt(2, 3000);

  4. 执行语句 :ResultSet rs = ps.executeQuery();

  5. 处理结果:while(rs.next()) { System.out.println(rs.getInt("empno") + " - " + rs.getString("ename")); }

  6. 关闭资源:con.close();

32. 事务的 ACID 是指什么

  • 原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;

  • 一致性(Consistent):事务结束后系统状态是一致的;

  • 隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;

  • 持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步备份可以在故障发生后重建数据。

33. JDBC 中如何进行事务处理

  • Connection 提供了事务处理的方法,通过调用 setAutoCommit(false)可以设置手动提交事务;

  • 当事务完成后用 commit()显式提交事务;如果在事务处理过程中发生异常则通过 rollback()进行事务回滚。

  • 除此之外,从 JDBC 3.0 中还引入了 Savepoint(保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保存点。

34. 获得一个类的类对象有哪些方式?

  1. 方法 1:类型.class,例如:String.class

  2. 方法 2:对象.getClass(),例如:”hello”.getClass()

  3. 方法 3:Class.forName(),例如:Class.forName(“java.lang.String”)

35. 如何通过反射创建对象?

  1. 方法 1:通过类对象调用 newInstance()方法,例String.class.newInstance()

  2. 方法 2:通过类对象的 getConstructor()或 getDeclaredConstructor() 方法获得构造器(Constructor)对象并调用其 newInstance()方法创建对象, 例String.class.getConstructor(String.class).newInstance(“Hello”);

集合

1. List、Set、Map 是否继承自 Collection 接口?

  • List、Set 是,Map 不是。

  • Map 是键值对映射容器,与 List 和 Set 有明显的区别, 而 Set 存储的零散的元素且不允许有重复元素(数学中的集合也是如此),List 是线性结构的容器,适用于按数值索引访问元素的情形。

2. 阐述 ArrayList、Vector、LinkedList 的存储性能和特性。

  • ArrayList 和 Vector 都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector 中的方法由于添加了 synchronized 修饰,因此 Vector 是线程安全的容器,但性能上较 ArrayList 差,因此已经是 Java 中的遗留容器。

  • LinkedList 使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。但是由于

  • ArrayList 和 LinkedListed 都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类 Collections 中的 synchronizedList 方法将其转换成线程安全的容器后再使用

3. Collection 和 Collections 的区别?

  • Collection 是一个接口,它是 Set、List 等容器的父接口

  • Collections 是个一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、 排序、线程安全化等等。

4. List、Map、Set 三个接口存取元素时,各有什么特点?

  • List :有序,可以有重复元素,可以根据元素的索引来访问这些元素。

    • ArrayList:基于数字实现,查询效率高,增删效率低,线程不安全。

    • LinkedList:底层是使用双向链表实现的存储,查询效率低,增删效率高,线程不安全。

    • Vector:底层是用数组实现的List,线程安全,效率低。

  • Set :无序不能存放重复元素

    • HashSet:采用哈希算法实现的,底层实际是用HashMap实现。查询和增删效率度挺高。线程不安全。

    • TreeSet:底层实际是TreeMap实现,是一个有序的集合,支持序列化

  • Map 保存键值对(key-value pair)映射, 映射关系可以是一对一或多对一。

    • HashMap:底层实现采用了哈希表,哈希表的基本结构就是数组+链表。结构 hash-(key,value)-next

    • TreeMap是红黑二叉树的典型实现。

    • HashMap的效率高于TreeMap,但在需要排序Map的时候需要使用TreeMap。

数组:占用空间连续。寻址容易,查询速度快,但增加和删除效率低

链表:占用空间不连续,寻址困难,查询速度慢,但是增加和删除效率高。

5. TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素

  • TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。

  • TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。

  • Collections 工具类的 sort 方法有两种重载的形式

    • 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;

    • 第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是 Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于 一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值