最近为了准备春招,就复习了一阵子java,下面把我的经验分享给大家.
本文主要包含 同步及多线程.jvm.容器.nio.sql及索引方面的知识.
以下是文章注意的点…
1.收集了大量的前辈们的写的好的博客,以及面经和java面试题.
2.不包含具体的问题,只包含知识点
3.具体知识点建议去提供的连接里,去看大牛写的博客,会受益良多.有些我自己做了总结,有错误欢迎指出.
4.本文没提供算法和计算机网络及linux方面的知识,也没提供特别基础的java知识,具体的请百度查看大牛的经验
——————————-一下是些杂七杂八的东西————————–
工具** fiddler 模拟post之类的
currentHashMap 链表尾部插入节点
hashMap
1.8
- 链表不会倒置
- 尾插法
- 扰动一次,低16位和高16位进行异或计算
1.7
- 链表倒置
- 头插法
- 扰动多次
tcp确保数据可靠性p19
https传输数据p19
对象反射
**tcp **smtp pop3 telnet icmp
ip dns
—————————————————————————————
一些问题
(1)浏览器层请求拦截
1.产品层面,用户点击“查询”或“购票”后,按钮置灰,禁止用户重复提交请求
2.js层面,限制用户在n秒之内只能提交一次请求
(2)站点层请求拦截与页面缓存
1.静态化,将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素
2.限频率,同一个UID,限制访问频率,做页面缓存,n秒内到达站点层的请求,均返回同一页面
(3)服务层请求拦截与数据缓存
1.对于写请求,将所有写请求在缓存(Redis或Memcached)中,做请求单队列排队,每次只透过有限的写请求异步写入到数据层,如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完”
2.对于读请求,用Redis或Memcached
缓存写性能和读性能都远高于MySQL,只有非常少的写和读缓存的请求会透到数据层去
面经网址
设计模式
动态代理
基于接口的代理(jdk)
创建一个和实现目标类接口的新对象
组成:
1.目标类:要被代理的类。
2.目标类接口:要被代理的类实现的接口
3.拦截器类:实现invocationHandler接口的类
4.代理类:通过(T)Proxy.newProxyInstance()方法获得的类。T为接口类型
步骤:
- 获取目标类及目标类接口
- 创建拦截器类,内部重写方法invok,invok方法内部调用method.invok(target,args)。因为需要被代理的对象,通常在类里写private Object target,并通过构造器传入该对象.
- 获取代理类:Proxy.newProxyInstance(x,x,invocationHandler),构造该代理对象需要一个拦截器,并且得到的对象是Object,需要强转为该接口类型,才可以调用。
基于类的代理(cglib)
继承目标类。
组成:
- 目标类:要被代理的类。
- 拦截器类:实现MethodInterceptor 接口的类
- 代理类:通过(T)Proxy.newProxyInstance()方法获得的类。T为接口类型
步骤:
同上
创建拦截器类,内部重写方法intercept,intercept方法内部调用method.invok(target,args)。因为需要被代理的对象,通常在类里写private Object target,并通过构造器传入该对象.
获取代理类: 比jdk少了一个类加载器
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(obj.getClass()); //设置父类,对比设置接口
enhancer.setCallback(intercept); //设置拦截器,对比设置拦截器
Object proxyObj = enhancer.create();//创建代理类
命令模式
概述:在软件设计中,我们经常会遇到某些对象发送请求,然后某些对象接受请求后执行,但发送请求的对象可能并不知道接受请求的对象是谁,执行的是什么动作。此时可通过 命令模式 来实现,让发送者和接受者完全的松耦合,这样可大大增强程序的灵活性。
graph LR
A[Object] -->C0(abstarctCommand)
C0 --> C1[CommandImpl1]
C0 --> C2[CommandImpl2]
C1 --> H1[handler1]
C2 --> H2[handler2]
发出者 –> 命令 – > 接收者
自己理解:
Object:
1.命令的发起者,内部维持一个command类的引用(构造器传入,不写死),根据业务需求,将发起者绑定不同的命令对象,在发起者内的具体的发起方法中(如button按钮里的click),调用command里的execute()方法,或其他方法.
abstarctCommand:
1.声明命令执行方法execute()
2.声明一些其他的方法,所有命令都可能会用到的,类似于命令的切面之类的感觉,如果此方法需要抽象则生命abstract,需要不同命令不一样则不加abstract.(eg,比如每个命令里,都有一个打印当前时间的功能.)
CommandImpl:
1.绑定一个handler ,即具体的处理类.(组合方式写死).即每个command都绑定好了对应的handler,如果有需求,我们需要重新写一个新的command,而不是修改本command,所以写死.
2.execute()方法,调用handler里的具体处理方法.
3.其他方法,根据逻辑需求而写的方法.重写或不重写,用handler或者新的逻辑.
handler
1.命令的处理者,类与类之间不存在依赖.每种处理类都有多种处理方法.command模式不关心这里的抽象.
命令队列的实现(批处理)
有时候我们需要将多个请求排队,当一个请求发送者发送一个请求时,将不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理。
增加commandQueue.
graph LR
A[Object] -->Q(commandQueue)
Q -->C0(abstarctCommand)
C0 --> C1[CommandImpl1]
C0 --> C2[CommandImpl2]
C1 --> H1[handler1]
C2 --> H2[handler2]
修改点 :
1.Object 绑定的是commandQueue(注入绑定),调用commandQueue的execute,每次发出命令前,先构造一个commandQueue对象并绑定,commandQueue对象添加需要的command命令
2.commandQueue 提供对command的增删操作,并提供execute()方法,遍历command并调用他们的execute()方法.
NIO
基本组件
channel
常见类型
FileChannel, SocketChannel(TCP), ServerSocketChannel(TCP),DatagramChannel(UDP);
获得方法
- 流的getChannel()方法。
FileChannel通过RandomAccessFile, FileInputStream, FileOutputStream的getChannel()来初始化。
- 各个通道提供了静态方法open()
FileChannel inChannel = FileChannel.open(Paths.get(“1.jpg”),
StandardOpenOption.READ);
Files工具类的newByteChannels
ByteChannel byteChannel = Files.newByteChannel(Paths.get(“1.jpg”),
StandardOpenOption.READ);
常见方法
read(buffer) -1为读完 一般接下来调用buffer.flip 然后再调用write
write(buffer)
close()
与流的差别
- Channel是双向的,既可以读又可以写,而流是单向的
- Channel可以进行异步的读写
- 对Channel的读写必须通过buffer对象
buffer
ByteBuffer.allocate(1024); 申请缓冲区
常见类型
ByteBuffer 、 MappedByteBuffer 、 CharBuffer 、 DoubleBuffer 、 FloatBuffer 、 IntBuffer 、 LongBuffer 、ShortBuffer 。
常见方法
flip(): 写模式转换成读模式,指针指向最前面
rewind() :将 position 重置为 0 ,一般用于重复读。
clear() :清空 buffer ,准备再次被写入 (position 变成 0 , limit 变成 capacity) 。
compact(): 将未读取的数据拷贝到 buffer 的头部位。
mark() 、 reset():mark 可以标记一个位置, reset 可以重置到该位置。
三个变量
- position:跟踪已经写了多少数据或读了多少数据,它指向的是下一个字节来自哪个位置
- limit:代表还有多少数据可以取出或还有多少空间可以写入,它的值小于等于capacity。
- capacity:代表缓冲区的最大容量,一般新建一个缓冲区的时候,limit的值和capacity的值默认是相等的。
Selector
常见方法
创建
channel.configureBlocking(false);//注册异步模式 只有异步模式才可以使用Selector。FileChannel没有异步模式,不可以,但SocketChannel是可以的。
Selector selector = Selector.open();
SelectionKey key = channel.register(selector,SelectionKey.OP_READ);
register()方法的第二个参数,它是一个“interest set”,意思是注册的Selector对Channel中的哪些时间感兴趣,事件类型有四种:
- Connect SelectionKey.OP_CONNECT
- Accept SelectionKey.OP_ACCEPT
- Read SelectionKey.OP_READ
- Write SelectionKey.OP_WRITE
监听
- int select(): 阻塞到至少有一个通道在你注册的事件上就绪
- int select(long timeout):select()一样,除了最长会阻塞timeout毫秒(参数)
- int selectNow(): 不会阻塞,不管什么通道就绪都立刻返回,此方法执行非阻塞的选择操作。如果自从前一次选择操作后,没有通道变成可选择的,则此方法直接返回零。
- selector.selectedKeys(); 一旦调用了
select()
方法,它就会返回一个数值,表示一个或多个通道已经就绪,然后你就可以通过调用selector.selectedKeys()
方法返回的SelectionKey集合来获得就绪的Channel
SelectionKey
- The interest set selectionKey.interestOps()。获得感兴趣的事件的集合
- The ready set 。通道已经准备就绪的操作的集合
- selectionKey.readyOps()
- selectionKey.isAcceptable();
- selectionKey.isConnectable();
- selectionKey.isReadable();
- selectionKey.isWritable();
- The Channel selectionKey.channel();
- The Selector selectionKey.selector();
- An attached object (optional)
nio.charset
Java.nio.charset 提供了编码解码一套解决方案
数据库(Mysql)
读写分离:1写 4读
分库分表:数据分库
主从复制:数据库数据保持一致
优化相关
show profiles 工具
- set @@prifileing=1;打开语句
- show profiles 查看mysql性能
explain sql语句
慢查询
- 打开查询
- 设置参数,
- 超过x秒的查询会被记录在日志内
- 日志位置
事务
锁
显示使用
共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
适用:从锁的角度来说,表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。
执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预
MySQL的表级锁定对于读和写是有不同优先级设定的,默认情况下是写优先级要大于读优先级。
死锁
例子
sqlServer
T1:
begin tran
select * from table (holdlock) (holdlock意思是加共享锁,直到事物结束才释放)
update table set column1='hello'
T2:
begin tran
select * from table(holdlock)
update table set column1='world'
## 解释 t1 t2加共享锁,之后都准备等对方的共享锁释放,加排他锁执行update
建议预防方式
a)类似业务模块中,尽可能按照相同的访问顺序来访问,防止产生死锁;
b)在同一事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
c)对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率。
InnoDB
意向锁
目的:行级锁定和表级锁定共存。数据库自带的
场景:全表锁定后,插入数据时,可以申请最后一行的行锁,进行插入。
InnoDB行锁实现方式
InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁
间隙锁(Next-Key锁)
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁.对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
检测死锁
当InnoDB检测到系统中产生了死锁之后,InnoDB会通过相应的判断来选这产生死锁的两个事务中较小的事务来回滚,而让另外一个较大的事务成功完成。
MyISAM
MyISAM表锁是deadlock free的,这是因为MyISAM总是一次获得所需的全部锁,要么全部满足,要么等待,因此不会出现死锁。
ConcurrentInsert(并发插入)的特性。
concurrent_insert=2,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录;
concurrent_insert=1,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录。这也是MySQL的默认设置;
concurrent_insert=0,不允许并发插入。
索引
选择B+树而不是其他数据结构的原因主要是因为数据是保存在硬盘上而不是内存中,所以减少磁盘IO次数才是提升效率的关键。
B+树是为磁盘或其他直接存取辅助设备而设计的一种平衡查找树(如果不知道平衡查找树,请自行google),在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶节点中,各叶节点指针进行连接。
- 节点
与二叉树不同的是,B+中的节点可以有多个元素及多个子节点。在B+索引树中,非叶子节点由索引元素和指向子节点的指针组成,他们的作用就是找到叶子节点,因为只有叶子节点中有最终要找的数据信息。从图中可以看出每个节点中指针的数量比索引元素数量多一个,在叶子节点中,因为没有子节点,多出的那个指针指向下一个叶子节点,这样把所有叶子节点串联起来,这对于范围搜索很有用。在实际应用中一个节点的大小是固定的通常等于磁盘一个页的大小,这样存取一个节点只需要一次磁盘IO,一般节点可存上百个元素,所以索引几百万数据B+树高不会超过3。 - 搜索
搜索类似于二叉查找树,从根节点开始自顶向下遍历,直到叶子节点,在节点内部典型的是使用二分查找来确定位置。如果是范围查找,对于B+树而言是非常方便的,因为叶子节点是有序且连续的。 - 插入
首先对树进行搜索找到应该存入的叶子节点。之前我们提到节点的大小是固定的,如果节点内还没放满,则直接插入。如果节点满了,则创建新节点把原节点插入新元素后的一半放入新节点,然后把新节点最小的元素插入父节点。如果父节点满了,进行同样的操作,根节点也不例外。 - 删除
首先对树进行搜索找到叶子节点并删除元素。当删除后的叶子节点不满一半时:如果兄弟节点过半数则借一个过来,并更新父节点中子节点的分界值;如果等于半数则合并,因为父节点中有两个指针指向这两个兄弟节点,所以需要删除多余的一个来更新父节点,如果删除后父节点不满一半,继续递归以上步骤,直到根节点。
从B+树的特点可以看出,虽然B+树索引能够让我们在有限次磁盘IO次数内快速的查询到数据,但是在插入和删除时也要付出维护B+树的代价,这也是为什么在开始说的不能把每列都加上索引的原因之一。
索引选择性与前缀索引
选择性
Index Selectivity =不重复的索引值 / 表中行数
显然选择性的取值范围为(0, 1],选择性越高的索引价值越大,这是由B+Tree的性质决定的
前缀索引
应用 索引太长
用first_name和last_name的前几个字符建立索引,例如
引擎
四种基本引擎及特点
1)MyIsam
静态表:固定长度的记录,存储迅速,容易缓存
动态表:记录不固定长度,占用的空间相对较少
压缩表
缺点:不支持事务、也不支持外键,
优势:是访问速度快,对事务完整性没有
应用: 要求或者以select,insert为主的应用基本上可以用这个引擎来创建表
2)innodb
优势:支持事务,自动灾难恢复,行级锁,外键约束,支持自动增长列,mysql 默认的引擎
应用:使用于更新密集型
3)memory
Hash索引优点:定位快。缺点:不支持Like查询
优势:访问速度快,采用的逻辑存储介质是内存
缺点:因为数据并没有实际写入到磁盘中
4)merge 一组 myisam 表的组合
InnoDB
聚集索引。主索引存的是行记录,辅助索引存的是主键的值
采用自增字段做主键 因为b+tree会排序,不是自增会带来很多麻烦
主键尽量长度小 因为innoDB会的索引会记录主键的值。
MyISAM
非聚集索引。主辅索引存的是数据行的地址。
JVM
类加载过程
加载-验证-准备-解析-初始化-使用-卸载,其中验证-准备-解析称为链接。
在遇到下列情况时,若没有初始化,则需要先触发其初始化(加载-验证-准备自然需要在此之前):
1)1.使用 new 关键字实例化对象 2.读取或设置一个类的静态字段 3.调用一个类的静态方法。
2)使用 java.lang.reflect 包的方法对类进行反射调用时,若类没有进行初始化,则需要触发其初始化
3)当初始化一个类时,若发现其父类还没有进行初始化,则要先触发其父类的初始化。
4)当虚拟机启动时,用户需要制定一个要执行的主类(有 main 方法的那个类),虚拟机会先初始化这个类。
在加载阶段,虚拟机需要完成下面 3 件事:
1)通过一个类的全限定名获取定义此类的二进制字节流;
2)将这个字节流所表示的静态存储结构转化为方法区运行时数据结构
3)在内存中生成一个代表这个类的 class 对象,作为方法区的各种数据的访问入口。
验证的目的是为了确保 clsss 文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全。验证阶段大致会完成下面 4 个阶段的检验动作:1)文件格式验证 2)元数据验证 3)字节码验证 4)符号引用验证{字节码验证将对类的方法进行校验分
析,保证被校验的方法在运行时不会做出危害虚拟机的事,一个类方法体的字节码没有通过字节码验证,那一定有问题,但若一个方法通过了验证,也不能说明它一定安全}。
准备阶段是正式为类变量分配内存并设置变量的初始化值得阶段,这些变量所使用的内存都将在方法区中进行分配。(不是实例变量,且是初始值,若 public static int a=123;准备阶段后 a 的值为 0,而不是 123,要在初始化之后才变为 123,但若被 final 修饰,public static
final int a=123;在准备阶段后就变为了 123)
解析阶段是虚拟机将常量池中的符号引用变为直接引用的过程。
静态代码块只能访问在静态代码块之前的变量,在它之后的变量,在前面的静态代码块中可以复制,但是不可以使用。
通过一个类的全限定名来获取定义此类的二进制字节流,实现这个动作的代码就是“类加载器”。比较两个类是否相同,只有这两个类是由同一个类加载器加载的前提下才有意义,否则即使这两个类来源于同一个 class 文件,被同一个虚拟机加载,只要加载他们的加载器不同,他们就是不同的类。
类加载器
从 JAVA 开发人员角度,类加载器分为:
1)启动类加载器,这个加载器负责把\lib 目录中或者 Xbootclasspath
下的类库加载到虚拟机内存中,启动类加载器无法被 Java 程序直接引用。
2)扩展类加载器:负责加载\lib\ext 下或者 java.ext.dirs 系统变量指定路径下 all 类库,开发者可以直接使用扩展类加载器。
3)应用程序类加载器,负责加载用户路径 classpath 上指定的类库,开发者可以直接使用这个类加载器,若应用程序中没有定义过自己的类加载器,一般情况下,这个就是程序中默认的类加载器。
双亲委派模型:若一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把所这个请求委派给父类加载器去完成,每一层的加载器都是如此,因此 all 加载请求最终都应该传送到顶级的启动类加载器。只有当父类加载器反馈自己无法加载时(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。双亲委派模型好处:eg,object 类。它存放在 rt.jar 中,无论哪个类加载器要加载这
个类,最终都是委派给处于模型顶端的启动类加载器加载,因此object 类在程序的各种加载
环境中都是同一个类。
jvm分区
虚拟机栈 堆 方法区 程序计数器 本地方法栈
jvm堆分代
分为年轻代,老年代,持久代
年轻代
大多数都是朝生夕死的对象
分为两个survival和一个eden 大小为1:8,
垃圾回收(minor jc)为复制算法, 将survival和eden复制到另一个survival中。
老年代
默认情况下年轻代中的对象经历15次垃圾收集后会晋升到老年代。
动态对象年龄判定 如果在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,大于或等于该年龄的对象就可以直接进入老年代
一些大的对象直接会进入老年代
垃圾回收(full jc)为标记算法 cms是标记-清除 g1是标记-整理
持久代
用于存放静态文件,如Java类、方法等。
垃圾回收机制
方法区垃圾回收 废弃常量和废弃的类
判断对象是否回收
引用计数法,会有互相引用的问题
可达性分析法,即与gc roots直接或间接相连的对象不会被回收,gc会对不能到达的对象进行标记。
相连的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(即一般说的Native方法)引用的对象;
总结就是,方法运行时,方法中引用的对象;类的静态变量引用的对象;类中常量引用的对象;Native方法中引用的对象。
gc线程
gc线程优先级最低,
System.gc()强制执行的是Full Gc. 并且不一定执行
垃圾收集器
cms:
定义:与part new一起使用,cms是负责老年代的垃圾收集器,它是一款真正的并发收集器, 采用的是复制-清除方式清理垃圾。使用线程数(CPU+3)/4
步骤:初始标记 ->并发标记->并发预清理->重新标记->并发清理
初始标记:标记和gc-roots相连的节点 会stop the world
并发标记:是并发操作,标记不能到达gc-roots的节点(GC ROOT TRACING)
重新标记:修正并发标记期间因用户操作而产生变动的标记记录, 会stop the world
并发清理:清除垃圾
优点 并发清理速度快,停顿少
缺点 产生空间碎片,占cpu ,会出现浮动垃圾
g1:
新生代老年代都可以回收的收集器,老年代使用标记-整理方式,java8中出现的,不是很稳定
步骤:初始标记 ->并发标记->最终标记->筛选回收
空间分配担保
在发生 minor gc 前,虚拟机会检测老年代最大可用的连续空间是否>新生代 all 对象总空间,若这个条件成立,那么 minor gc 可以确保是安全的。若不成立,则虚拟机会查看HandlePromotionFailure 设置值是否允许担保失败。若允许,那么会继续检测老年代最大可用的连续空间是否>历次晋升到老年代对象的平均大小。若大于,则将尝试进行一次 minor gc,尽管这次 minor gc 是有风险的。若小于或 HandlePromotionFailure 设置不允许冒险,则这时要改为进行一次 full gc。
多线程
锁的概念
常见名词
· 公平锁/非公平锁
· 可重入锁
· 独享锁/共享锁
· 互斥锁/读写锁
· 乐观锁/悲观锁
· 分段锁
· 偏向锁/轻量级锁/重量级锁
· 自旋锁
volatile
基本原理
读取变量时直接从主存读取,写入变量时直接写入主存,跳过缓存。
三个概念
原子性
一组操作要么全都完成要么全不完成
volatile不保证原子性。
可见性
一个线程修改变量后,其他线程可以立即看到
具有可见性
有序性
jvm会对代码进行排序。
禁止指令重排序。
- hapens-before 中的两个原则
- 对volatile的写发生在读之前
- volatile前的代码一定在volatile之后的代码后执行
使用场景
对变量的写操作不依赖于当前值
该变量没有包含在具有其他变量的不变式中
happens-before
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
synchronized
底层实现
进入monitor和退出monitor对象代表获取释放锁,指令为monitorenter和monitorexit。
锁的升级
锁的状态:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态
锁的升级:随着竞争情况逐渐升级,可以升级但不能降级
锁升级的目的:提高获得锁和释放锁的效率
偏向锁(自旋)
第一次当其他线程访问时,判断原先线程是否活着,活着等其执行完则获得锁,否则则获得锁
轻量级锁(自旋)
重量级锁(阻塞)
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块场景。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 | 追求响应时间。同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。同步块执行速度较长。 |
CAS
基本概念
CAS即compareAndSwap,具有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
CAS原理
通过调用unsafe类的compareAndSwapInt()方法来进行CAS操作,
public final native boolean compareAndSwapInt
(Object o, long offset,int expected, int x);
底层嵌入的汇编实现的, 关键CPU指令是 cmpxchgan
CAS缺点
ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
解决思路:就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
线程自旋,对CPU开销大。
AQS
基本概念
AQS是基础组件,只负责核心并发操作,如加入或维护同步队列,控制同步状态,等,而具体的加锁和解锁操作交由子类完成
两个队列
Sync queue 同步队列,用于存放Node节点
codition queue 条件队列,用于放置ConditionObject 节点
一个AQS只有一个同步队列,但是可以有多个条件队列
node节点
小于0:4个状态 signal cancel condition propagate
等于0:表示当前节点在sync queue中,等待着获取锁
state变量
在不同的实现类中有着不同的含义
核心函数
acquire:
tryAcquire ==> addWaiter==> acquireQueued==> selfInterrupt
说明:
① 首先调用tryAcquire函数,调用此方法的线程会试图在独占模式下获取对象状态。此方法应该查询是否允许它在独占模式下获取对象状态,如果允许,则获取它。
② 若tryAcquire失败,则调用addWaiter函数,addWaiter函数完成的功能是将调用此方法的线程封装成为一个结点并放入Sync queue。
③ 调用acquireQueued函数,此函数完成的功能是Sync queue中的结点不断尝试获取资源,若成功,则返回true,否则,返回false。
addWaiter
addWaiter函数使用快速添加的方式往sync queue尾部添加结点,如果sync queue队列还没有初始化或者插入失败,则会使用enq插入
- enq
循环插入,保证插入到节点
acquireQueue(自旋一定时间获取锁)
如果前驱节点是头结点并且能够获取(资源),代表该当前节点能够占有锁,设置头结点为当前节点,返回。否则,调用shouldParkAfterFailedAcquire和parkAndCheckInterrupt函数
- shouldParkAfterFailedAcquire
判断是否可以阻塞:只有当该节点的前驱结点的状态为SIGNAL时,才可以对该结点所封装的线程进行park操作。
- parkAndCheckInterrupt
阻塞:LockSupport.park(this);
release
释放锁:调用unparkSuccessor释放下一个节点
ReentrantLock
原理与AQS一样 区别在于公平锁与非公平锁
公平锁
1.判断AQS的state是否等于0
2.hasQueuedPredecessors判断队列是否有前面的线程在等待锁
3.setExclusiveOwnerThread设置为独占锁的线程
非公平锁
1.尝试将state修改为1
2.不调用hasQueuedPredecessors 其他与公平锁一样
锁的中断
普通的lock方法并不能被其他线程中断,ReentrantLock是可以支持中断,需要使用lockInterruptibly。
currentHashMap
1.7
1.8
重要属性
sizeCtl: 控制标识符,
负数代表正在进行初始化或扩容操作
- -1代表正在初始化
- N 表示有-N-1个线程正在进行扩容操作
正数或0代表hash表还没有被初始化,
- 这个数值表示初始化或下一次进行扩容的大小,这一点类似于扩容阈值的概念。还后面可以看到,它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。
重要类
Node
Node是最核心的内部类,它包装了key-value键值对,所有插入ConcurrentHashMap的数据都包装在这里面。它与HashMap中的定义很相似,但是但是有一些差别它对value和next属性设置了volatile同步锁(与JDK7的Segment相同),它不允许调用setValue方法直接改变Node的value域,它增加了find方法辅助map.get()方法。
TreeNode
树节点类,另外一个核心的数据结构。当链表长度过长的时候,会转换为TreeNode。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。而且TreeNode在ConcurrentHashMap集成自Node类,而并非HashMap中的集成自LinkedHashMap.Entry
TreeBin
这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。
这里仅贴出它的构造方法。可以看到在构造TreeBin节点时,仅仅指定了它的hash值为TREEBIN常量,这也就是个标识为。同时也看到我们熟悉的红黑树构造方法
ForwardingNode
一个用于连接两个table的节点类。它包含一个nextTable指针,用于指向下一张表。而且这个节点的key value next指针全部为null,它的hash值为-1. 这里面定义的find的方法是从nextTable里进行查询节点,而不是以自身为头节点进行查找。
重要方法
initTable(初始化)
对于ConcurrentHashMap来说,调用它的构造方法仅仅是设置了一些参数而已。而整个table的初始化是在向ConcurrentHashMap中插入元素的时候发生的。如调用put、computeIfAbsent、compute、merge等方法的时候,调用时机是检查table==null。
初始化方法主要应用了关键属性sizeCtl 如果这个值〈0,表示其他线程正在进行初始化,就放弃这个操作。在这也可以看出ConcurrentHashMap的初始化只能由一个线程完成。如果获得了初始化权限,就用CAS方法将sizeCtl置为-1,防止其他线程进入。初始化数组后,将sizeCtl的值改为0.75*n。
transfer(扩容)
当ConcurrentHashMap容量不足的时候,需要对table进行扩容。这个方法的基本思想跟HashMap是很像的,但是由于它是支持并发扩容的,所以要复杂的多。原因是它支持多线程进行扩容操作,而并没有加锁。
整个扩容操作分为两个部分
- 第一部分是构建一个nextTable,它的容量是原来的两倍,这个操作是单线程完成的。这个单线程的保证是通过RESIZE_STAMP_SHIFT这个常量经过一次运算来保证的,这个地方在后面会有提到;
- 第二个部分就是将原来table中的元素复制到nextTable中,这里允许多线程进行操作。
Put方法
- 如果一个或多个线程正在对ConcurrentHashMap进行扩容操作,当前线程也要进入扩容的操作中。这个扩容的操作之所以能被检测到,是因为transfer方法中在空结点上插入forward节点,如果检测到需要插入的位置被forward节点占有,就帮助进行扩容;
- 如果检测到要插入的节点是非空且不是forward节点,就对这个节点加锁,这样就保证了线程安全。尽管这个有一些影响效率,但是还是会比hashTable的synchronized要好得多
Semaphore
线程池
基本组成
常用状态
RUNNING : 接受新任务并且处理已经进入阻塞队列的任务
SHUTDOWN :不接受新任务,但是处理已经进入阻塞队列的任务
- STOP : 不接受新任务,不处理已经进入阻塞队列的任务并且中断正在运行的任务
阻塞队列
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
饱和策略
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
- 抛出异常
重要参数
corePoolSize :核心线程数
- 核心线程会一直存活,及时没有任务需要执行
- 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
- 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
maxPoolSize:最大线程数
- 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
- 当线程数=maxPoolSize,且任务队列已满时,按饱和策略处理
keepAliveTime:线程空闲时间
- 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
- 如果allowCoreThreadTimeout=true,则会直到线程数量=0
核心方法
execute与sbumit
execute无返回值 sbumit有返回值
调用submit时,间接调到execute函数,在将来某个时间执行给定任务,
shutdown与shutdownNow
shutdown会设置线程池的运行状态为SHUTDOWN,并且中断所有空闲的worker,由于worker运行时会进行相应的检查,所以之后会退出线程池,并且其会继续运行之前提交到阻塞队列中的任务,不再接受新任务。shutdownNow则会设置线程池的运行状态为STOP,并且中断所有的线程(包括空闲和正在运行的线程),在阻塞队列中的任务将不会被运行,并且会将其转化为List返回给调用者,也不再接受新任务,其不会停止用户任务(只是发出了中断信号),若需要停止,需要用户自定义停止逻辑。
底层实现
Worker类
addWorker函数
① 原子性的增加workerCount。
② 将用户给定的任务封装成为一个worker,并将此worker添加进workers集合中。
③ 启动worker对应的线程,并启动该线程,运行worker的run方法。
④ 回滚worker的创建动作,即将worker从workers集合中删除,并原子性的减少workerCount。
runWorker函数
实际执行给定任务,并且当给定任务完成后,会调用到getTask函数,继续从阻塞队列中取任务,直到阻塞队列为空(即任务全部完成)