1.集合
集合兩大接口:Collection 和 Map,结构图如下
1.1 List和Set的区别
集合 | 区别 |
---|---|
List | 有序,可重复,可插入多个null值 |
Set | 无序,不重复,只可插入1个null值 |
补充:List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变
1.2 集合框架底层数据结构
集合接口 | 集合实现类 | 底层数据结构 |
---|---|---|
List | ArrayList | Object数组 |
Vector | Object数组 | |
LinkedList | 双向循环链表 | |
Set | HashSet | (无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素 |
LinkedHashSet | LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的 。 | |
TreeSet | (有序,唯一):红黑树(自平衡的排序二叉树。) | |
Map | HashMap | JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突 而存在的(“拉链法”解决冲突)JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间 |
LinkedHashMap | LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树 组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 | |
HashTable | 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 | |
TreeMap | 红黑树(自平衡的排序二叉树) |
1.3 如何实现数组和 List 之间的转换?
数组转 List:使用 Arrays. asList(array) 进行转换。
List 转数组:使用 List 自带的 toArray() 方法。
1.4 ArrayList 和 LinkedList 的区别是什么?
(1)数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
(2)随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
(3)增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
(4)内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
(5)线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全
;
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。
补充:数据结构基础之双向链表
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
2.BIO,AIO,NIO区别
BIO:(blocking IO [阻塞IO])
同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
可以使用伪异步的方式改善
package com.zfx.bio;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @program: zfxstuty
* @description:
* 1.2 伪异步 IO
* 为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题
* ,后来有人对它的线程模型进行了优化一一一后端通过一个线程池来处理多个客户端的请求接入,
* 形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,
* 设置线程的最大值,防止由于海量并发接入导致线程耗尽。
*
* TODO 总结:在活动连接数不是特别高(小于单机1000)的情况下,
* 这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。
* 线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,
* 传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量
*
* @author: zheng_fx
* @create: 2021-07-10 18:06
*/
public class IOServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
// 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理
new Thread(()->{
while (true){
try {
//阻塞方法获取新的连接
Socket socket = serverSocket.accept();
//每一个新的连接都创建一个线程,负责读取数据
new Thread(()->{
int len;
byte[] data = new byte[1024];
try {
InputStream inputStream = socket.getInputStream();
// 按字节流方式读取
// read()方法的作用是从输入流读取数据的下一个字节,返回的字节的值是一个0~255之间的整数。到达流的末尾返回-1
// read(b)方法返回的非-1的返回值表示读取的字节的个数。
while ((len=inputStream.read(data))!=-1){
System.out.println(Thread.currentThread().getName()+"--接收消息: "+new String(data,0,len));
}
} catch (IOException e) {
e.printStackTrace();
}
},"SERVER").start();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
NIO:(Non-blocking IO[非阻塞IO])
【同步非阻塞】同时支持阻塞与非阻塞模式
一般面试:NIO和BIO的区别?
Non-blocking IO(非阻塞IO):IO流是阻塞的,NIO是不阻塞的
可以从以下3点回答:
-
Buffer(缓冲区):IO是*
面向流
(Stream oriented)而 NIO 面向缓冲区
*(Buffer oriented) -
Channel (通道):NIO 通过Channel(通道) 进行读写。通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
-
Selectors(选择器):NIO有选择器,而IO没有。
选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。
AIO:异步非阻塞的IO模型
AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的
,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
3.ThreadLocal底层实现原理
ThreadLocal提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现
常见的ThreadLocal用法有:
- 存储单个线程上下文信息。比如存储id等;
- 使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;
- 减少参数传递。比如做一个trace工具,能够输出工程从开始到结束的整个一次处理过程中所有的信息,从而方便debug。由于需要在工程各处随时取用,可放入ThreadLocal。
原理
# 大白话:
底层是有一个ThreadLocalMap(map)存当前线程变量 ,key为当前线程,value为当前需要存储的变量
每个Thread内部都有一个Map,我们每当定义一个ThreadLocal变量,就相当于往这个Map里放了一个key,并定义一个对应的value。每当使用ThreadLocal,就相当于get(key),寻找其对应的value。
每个Thread都有一个{@link Thread#threadLocals}变量,它就是放k-v的map,类型为{@link java.lang.ThreadLocal.ThreadLocalMap}。这个map的entry是{@link java.lang.ThreadLocal.ThreadLocalMap.Entry},具体的key和value类型分别是{@link ThreadLocal}(我们定义ThreadLocal变量就是在定义这个key)和 {@link Object}(我们定义ThreadLocal变量的值就是在定义这个value)。
set(T value)源码
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
4.CurrentHashMap底层数据结构
hash表
哈希表就是一种以 键-值(key-indexed) 存储数据的结构
,我们只要输入待查找的值即key,即可查找到其对应的值
应用场景
我们熟知的缓存技术(比如redis、memcached)的核心其实就是在内存中维护一张巨大的哈希表,还有大家熟知的HashMap、CurrentHashMap等的应用。
ConcurrentHashMap与HashMap等的区别
HashMap
HashMap是线程不安全
的,在多线程环境下,进行put的时候会引起死循环
,导致CPU利用率接近100%,所以在并发环境下不能使用HashMap
HashTable
HashTable和HashMap的实现原理几乎一样(hash表),差别无非是
HashTable不允许key和value为null
HashTable是线程安全的
但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁。
多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差
ConcurrentHashMap
主要就是为了应对hashmap在并发环境下不安全而诞生的,ConcurrentHashMap的设计与实现非常精巧,大量的利用了volatile,final,CAS等lock-free技术来减少锁竞争对于性能的影响
- JDK1.7版本的CurrentHashMap的实现原理
在JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁的方式实现
Segment(分段锁)-减少锁的粒度
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment 继承了ReentrantLock)。
- JDK1.8版本的CurrentHashMap的实现原理
JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作
5.Redis
5.1.几种数据类型
Redis主要有5种数据类型,包括String,List,Set,Zset,Hash,满足大部分的使用要求
数据类型 | 可以存储的值 | 操作 | 应用场景 |
---|---|---|---|
String | 字符串,整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作 | 做简单的键值对缓存 |
List | 列表 | 从两端压入或者弹出元素对单个或者多个元素进行修剪, 只保留一个范围内的元素 | 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的 数据 |
Hash | 包含键值对的无需散列表 | 添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在 | 结构化的数据,比如一个对象 |
Set | 无序集合 | 添加、获取、移除单个元素检查—个元素是否存在于集合中 计算交集、并集、差集从集合里面随机获取元素 | 交集、并集、差集的操作,比如交集, 可以把两个人的粉丝列表整一个交集 |
Zset | 有序集合 | 添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名 | 去重但可以排序,如获取排名前几名的用户 |
5.2.为什么要用 Redis 而不用 map/guava 做缓存?
缓存分为本地缓存和分布式缓存,以java为例,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量、快速
生命周期随着jvm的销毁而结束,并且在多实例的情境下,每个实例都需要各自保存一份缓存,缓存不具一致性
。
5.3.redis为什么这么快
完全基于内存
,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);数据结构简单
,对数据操作也简单,Redis 中的数据结构是专门进行设计的;- 采用
单线程
,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗 使用多路 I/O 复用模型,非阻塞 IO
- 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
5.4.持久化
1.什么是redis持久化?
持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失
2.Redis 的持久化机制是什么?各自的优缺点?
Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制
RDB:是Redis DataBase缩写快照
RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
优点:
- 1、只有一个文件 dump.rdb,方便持久化。
- 2、容灾性好,一个文件可以保存到安全的磁盘。
- 3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能
- 4.相对于数据集大时,比 AOF 的启动效率更高。
缺点:
- 1、数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)
- 2、AOF(Append-only file)持久化方式:是指所有的命令行记录以 redis 命令请 求协议的格式完全持久化存储)保存为 aof 文件。
AOF:持久化
AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
优点:
- 1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。
- 2、通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
- 3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))
缺点:
- 1、AOF 文件比 RDB 文件大,且恢复速度慢。
- 2、数据集大的时候,比 rdb 启动效率低。
优缺点是什么?
AOF文件比RDB更新频率高,优先使用AOF还原数据。
AOF比RDB更安全也更大
RDB性能比AOF好
如果两个都配了优先加载AOF
5.5.过期键的删除策略
我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理
过期策略通常有以下三种:
- 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
- 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
- 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis中同时使用了惰性过期和定期过期两种过期策略
。
5.6.内存相关
MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据
redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
Redis的内存淘汰策略有哪些
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。
全局的键空间选择性移除
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
设置过期时间的键空间选择性移除
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
总结
Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据
。
5.7.事务
1.概念
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行
2.事务管理(ACID)概述
-
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
-
一致性(Consistency)
事务前后数据的完整性必须保持一致。
-
隔离性(Isolation)
多个事务并发执行时,一个事务的执行不应影响其他事务的执行
-
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
Redis的事务
总是具有ACID中的一致性
和隔离性
,其他特性是不支持的
。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性
3.redis事务
Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
4.Redis事务支持隔离性吗
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的
5.Redis事务保证原子性吗,支持回滚吗
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
6.缓存雪崩
缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉
解决方案
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
- 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
7.缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案
- 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力
8.缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
- 设置热点数据永远不过期。
- 加互斥锁,互斥锁
9.缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据
解决方案
- 直接写个缓存刷新页面,上线时手工操作一下;
- 数据量不大,可以在项目启动的时候自动进行加载;
- 定时刷新缓存;
6.在java中String类为什么要设计成final?
https://www.zhihu.com/question/31345592
大白话解释就是: String很多实用的特性,比如说“不可变性”,是工程师精心设计的艺术品!艺术品易碎! 用final就是拒绝继承,防止世界被熊孩子破坏,维护世界和平!
概括【安全、高效】
7.synchronized底层原理
7.1.主要作用
- 原子性:确保线程互斥的访问同步代码;
- 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “
一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值
来保证的; - 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”
- 可重入性:synchronized为可重入锁
7.2.常见3种用法
- 当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this);
- 当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;
- 当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;
package com.zfx.synchronize;
/**
* @description:
* TODO 参考博客
* https://blog.csdn.net/qq_36934826/article/details/95978700
* @author: zheng_fx
* @create: 2021-07-23 11:14
*/
public class SynchronizedDemo2 {
private int i = 0;
private static int j = 0;
//private final SynchronizedDemo2 instance = new SynchronizedDemo2();
//对成员方法加锁,必须获得该 类的实例对象 的锁才能进入
public synchronized void add1(){
i++;
}
//对静态方法加锁,必须获得 类的锁 才可以进入
public static synchronized void add2(){
j++;
}
public void method(){
synchronized (SynchronizedDemo2.class){
//同步块,执行前必须获得SynchronizedDemo2类的锁
System.out.println("Class对象锁");
}
synchronized (new SynchronizedDemo2()){
//同步块,执行前必须先获得实例对象的锁
System.out.println("实例对象锁");
}
}
/**
*
* 首先我们知道被static修饰的静态方法、静态属性都是归类所有,同时该类的所有实例对象都可以访问。
* 但是普通成员属性、成员方法是归实例化的对象所有,必须实例化之后才能访问,
* 这也是为什么静态方法不能访问非静态属性的原因。
* 我们明确了这些属性、方法归哪些所有之后就可以理解上面几个synchronized的锁到底是加给谁的了。
*
*TODO
* 1、首先看第一个synchronized所加的方法是add1(),
* 该方法没有被static修饰,也就是说该方法是归实例化的对象所有,那么这个锁就是加给Test1类所实例化的对象。
* 2、然后是add2()方法,该方法是静态方法,归Test1类所有,所以这个锁是加给Test1类的。
* 3、最后是method()方法中两个同步代码块,第一个代码块所锁定的是Test1.class,
* 通过字面意思便知道该锁是加给Test1类的,而下面那个锁定的是instance,这个instance是Test1类的一个实例化对象,
* 自然它所上的锁是给instance实例化对象的
*
*
* synchronized有两种形式上锁,一个是对方法上锁,一个是构造同步代码块。
* 他们的底层实现其实都一样,在进入同步代码之前先获取锁,获取到锁之后锁的计数器+1,
* 同步代码执行完锁的计数器-1,如果获取失败就阻塞式等待锁的释放。只是他们在同步块识别方式上有所不一样,
* TODO 从class字节码文件可以表现出来,
* 一个(方法上锁)是通过方法flags(ACC_SYNCHRONIZED)标志,一个(同步代码块)是monitorenter和monitorexit指令操作。
*
*/
public static void main(String[] args) {
// SynchronizedDemo2.add2();
// System.out.println(j);
SynchronizedDemo2 demo2 = new SynchronizedDemo2();
demo2.add1();
// demo2.method();
System.out.println(demo2.i);
}
}
class字节码
public com.zfx.synchronize.SynchronizedDemo2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field i:I
9: return
LineNumberTable:
line 8: 0
line 9: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/zfx/synchronize/SynchronizedDemo2;
public synchronized void add1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 15: 0
line 16: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/zfx/synchronize/SynchronizedDemo2;
public static synchronized void add2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #3 // Field j:I
3: iconst_1
4: iadd
5: putstatic #3 // Field j:I
8: return
LineNumberTable:
line 20: 0
line 21: 8
public void method();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: ldc #4 // class com/zfx/synchronize/SynchronizedDemo2
2: dup
3: astore_1
4: monitorenter
5: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #6 // String Class对象锁
10: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: new #4 // class com/zfx/synchronize/SynchronizedDemo2
26: dup
27: invokespecial #8 // Method "<init>":()V
30: dup
31: astore_1
32: monitorenter
33: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
36: ldc #9 // String 实例对象锁
38: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
41: aload_1
42: monitorexit
43: goto 51
46: astore_3
47: aload_1
48: monitorexit
49: aload_3
50: athrow
51: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
33 43 46 any
46 49 46 any
LineNumberTable:
line 25: 0
line 27: 5
line 28: 13
line 30: 23
line 32: 33
line 33: 41
line 34: 51
LocalVariableTable:
Start Length Slot Name Signature
0 52 0 this Lcom/zfx/synchronize/SynchronizedDemo2;
StackMapTable: number_of_entries = 4
frame_type = 255 /* full_frame */
offset_delta = 18
locals = [ class com/zfx/synchronize/SynchronizedDemo2, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
frame_type = 255 /* full_frame */
offset_delta = 22
locals = [ class com/zfx/synchronize/SynchronizedDemo2, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
7.3.底层实现原理
通过7.2的案例和字节码可以分析,可以很清楚的看到其底层是如何实现同步的
jvm基于进入和退出Monitor对象来实现方法同步和代码块同步
-
方法级的同步是隐式
,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。 -
代码块的同步是利用monitorenter和monitorexit这两个字节码指令
。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
注意:1>synchronized是可重入的,所以不会自己把,自己锁死;
2>synchronized锁一旦被一个线程持有,其他试图获取该锁的线程将被阻塞。
8.synchronized和ReentrantLock区别
8.1.区别比较
可重入性:
二者都是可重入锁,区别不大
锁的实现:
Synchronized是依赖于JVM
实现的,而ReenTrantLock是JDK
实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。
性能的区别
在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了
,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞
功能的区别
便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
锁的细粒度和灵活度
:很明显ReenTrantLock优于Synchronized
8.2.其他:
ReenTrantLock独有的能力:
-
<font color='cornflowerblue'>公平锁</font>:ReenTrantLock可以指定是<font color='cornflowerblue'>公平锁还是非公平锁</font>。而<font color='cornflowerblue'>synchronized</font>只能是<font color='red'>非公平锁</font>。所谓的公平锁就是先等待的线程先获得锁。
-
<font color='cornflowerblue'>锁绑定多个条件</font>:ReenTrantLock提供了一个Condition(条件)类,用来实现<font color='cornflowerblue'>分组唤醒需要唤醒的线程们</font>,而不是像<font color='cornflowerblue'>synchronized</font>要么<font color='red'>随机唤醒一个线程要么唤醒全部线程</font>。
-
<font color='cornflowerblue'>等待可中断</font>:ReenTrantLock提供了一种<font color='red'>能够中断等待锁的线程的机制</font>,通过lock.lockInterruptibly()来实现这个机制。
ReenTrantLock实现的原理:
简单来说,ReenTrantLock的实现是一种自旋锁
,通过循环调用CAS操作来实现加锁
。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。
什么情况下使用ReenTrantLock:
如果你需要实现ReenTrantLock的三个独有功能时
9.AQS
9.1.公平锁与非公平锁
- 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁
- 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁
9.2.可重入锁
概念:可重入锁又名递归锁
是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提:
锁对象是同一个对象
),不会因为之前已经获取过还没释放而阻塞
Java中ReentrantLock和Synchronized都是可重入锁
,可重入锁的一个优点可一定程度上避免死锁
synchronized
可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁
public class ReEnterLockDemo {
private static Object objectLockA = new Object();
public static void test1(){
new Thread(()->{
synchronized (objectLockA){
System.out.println(Thread.currentThread().getName()+"\t"+"------外层调用");
synchronized (objectLockA){
System.out.println(Thread.currentThread().getName()+"\t"+"------中层调用");
synchronized (objectLockA){
System.out.println(Thread.currentThread().getName()+"\t"+"------内层调用");
}
}
}
},"T1").start();
}
public static void main(String[] args) {
test1();
}
}
执行结果
Files\Java\jdk1.8.0_162\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_162\jre\lib\rt.jar;D:\2021\project\zfxstuty\demo-lock\target\classes" com.zfx.lock.ReEnterLockDemo
T1 ------外层调用
T1 ------中层调用
T1 ------内层调用
Process finished with exit code 0
9.3.LockSupport
是用来创建锁和其他同步类的基本线程阻塞原语。
下面这句话,后面详细说
LockSupport中的**park()和unpark()**的作用分别是阻塞线程
和解除阻塞线程
问题1:
为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。
问题2:
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;
而调用两次park却需要消费两个凭证,证不够,不能放行
。
9.4.自旋锁
自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就
一直循环检测锁
是否被释放,而不是进入线程挂起或睡眠状态。
10.String为什么要用final修饰
- 为了实现字符串常量池(效率)
- 为了线程安全
- 为了实现String可以创建HashCode不可变性
扩展:
final可以修饰类,方法和变量
- final修饰的类不能被继承,即它不能拥有自己的子类
- 被final修饰的方法不能被重写
- final修饰的变量,无论是类属性、对象属性、形参还是局部变量,都需要进行初始化操作
11.Spring的IOC、AOP
11.1.IOC:
原理:
IoC(Inverse of Control:控制反转)是一种设计思想,就是 将原本在程序中手动创建对象的控制权,交由Spring框架来管理。 IoC 在其他语言中也有应用,并非 Spirng 特有。 IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象
源码:
扩展:
-
将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
-
实现IOC的主要设计模式是工厂模式
11.2.AOP:
概念:
AOP(Aspect-Oriented Programming:面向切面编程) 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性
原理:
Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy
,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib
,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类
来作为代理。
使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。
1.有接口情况,使用JDK动态代理
创建接口实现类代理对象,增强类的方法
2.无接口情况,使用CGLIB动态代理
创建子类的代理对象,增强类的方法
1.AOP术语
切入点表达式
(1)切入点表达式作用:知道对哪个类里面的哪个方法进行增强
(2)语法结构:execution([权限修饰符] [返回类型] [类全路径] [方法名称]([参数列表]))
– 返回类型可以省略
(3)例子如下:
例 1:对 com.atguigu.dao.BookDao 类里面的 add 进行增强
execution(* com.atguigu.dao.BookDao.add(…))
例 2:对 com.atguigu.dao.BookDao 类里面的所有的方法进行增强
execution(* com.atguigu.dao.BookDao.* (…))
例 3:对 com.atguigu.dao 包里面所有类,类里面所有方法进行增强
execution(* com.atguigu.dao.. (…))