1.锁策略
1.1 乐观锁与悲观锁
其实前三个锁是同一种锁,只是站在不同的角度上去进行描述,此处的乐观与悲观其实是指在预测的角度上看会发生锁竞争的概率大小,概率大的则是悲观锁,概率小的则是乐观锁
乐观锁在加锁的时候就会做较少的事情,加锁的速度较快,但是消耗的cpu资源等也会增加,悲观锁在加锁的时候就会做很多事情来避免锁的冲突,从而加锁的时候做的事情就比较多,加锁的开销相对较小
1.2 轻量级锁与重量级锁
这里是从加锁的量级出发,本质上是和上面的悲观锁和乐观锁的意思差不多,意思是从结果的角度上看,轻量级的锁加锁开销小,重量级的锁加锁开销比较大
1.3 自旋锁与挂起等待锁
自旋锁是轻量级锁和乐观锁的一种典型实现方式,挂起等待锁是重量级锁的一种典型实现方式,自旋锁就是有一个while循环来一直判断是否需要加锁,加上锁了就跳出循环,加锁不成功就继续判断而不是阻塞,这就导致了一直消耗了系统的资源而没有做实事,所以说消耗了相对多的cpu资源
挂起等待锁则与他截然相反,挂起等待锁就需要内核调度器去参与操作了,所以要做的事情也就多了,所以需要获取到锁的时间也就多了
1.4 普通互斥锁与读写锁
普通互斥锁类似于Synchronized,有加锁和解锁两个动作
读写锁则是两种锁
读锁和读锁之间不会有锁冲突
写锁和读锁之间会有锁冲突
写锁之间也会有锁冲突
总结:一个线程加读锁的时候,另一个线程只能读不能写
一个线程加写锁的时候,另一个线程不能写也不能读
这个要和MySQL中事务的隔离级别要分得开
MySQL中的脏读,不可重复读,幻读
我们以一个有一条数据的表为例,age=10
脏读:事务A修改age =20,没有提交事务,事务B读取到这条数据之后,事务A回滚了,此时B就读到了一条脏的数据
不可重复读:事务B读取到了age=10,此时事务A修改数据为20并提交事务,事务B又一次查询数据,查到的两条数据是不一样的,这就是不可重复读
幻读:事务B查询到一条数据,此时事务A又增加一条数据,此时事务B再次查询就是两条数据,这就称为幻读
不可重复读和脏读之间的区别是:不可重复读读取的事务是已经提交的
1.5 公平锁与非公平锁
与之前介绍的线程饿死有一定的联系
这里的公平指的是先到先得,只要线程释放锁,第一个等待的线程可以率先拿到锁,就不会出现持有锁,释放锁,这样的循环持有的状态,导致其他线程没办法做事情
这就需要引入一个数据结构来实现先到先得这种特点
java原生的锁其实就是非公平锁,靠抢占式执行.
1.6 可重入锁与不可重入锁
我们之前说过,Synchronized就是一个可重入锁,就是针对一个线程,不断用这个锁加锁很多次,可重入锁不会出现问题,因为可重入锁只是增加了计数器,实际上仍然是只加了一层锁结构,而可重入锁就可能出现死锁的情况
2.Synchronized的锁优化策略
我们都说Synchronized有自适应的效果,能够根据锁冲突状态确定自己是什么类型的锁,那么到底是怎么回事呢??咱们慢慢说
Synchronized锁其实有三种状态,根据情况来逐级递增,注意这里的锁级别是不可降级的
1.偏向锁状态(假设没线程来竞争锁)
这里的核心思想就是懒汉模式的思想,用的时候再创建,能不加锁就不加锁,所谓的偏向锁,就是给线程加上一个非常轻量级的标记,如果没有人来竞争锁,就直接省略这样的加锁的操作,有的话则升级为轻量锁
2.轻量级锁状态(假设竞争较小)
此处的实现就是自旋锁,优点是可以第一时间拿到锁,缺点是比较消耗cpu资源
与此同时Synchronized也会计算锁竞争的激烈程度,从而来判断是否需要升级到重量级锁
对于轻量级锁来说,假设竞争这个锁的线程很多,那么大多数线程此刻就处自旋的状态,此刻就比较消耗cpu资源
3.重量级锁状态(假设竞争很大)
此时拿不到锁的线程就不会选择去自旋了,而是直接阻塞等待,直接让出cpu,当线程释放之后就会随机分配一个线程来持有这个锁
4.锁消除策略
Synchronized会自动判断线程加上的锁是否有效,在编译期间,如果判断无效就会自动把这个锁给干掉,比如说这里没有涉及到多个线程对成员变量的修改等等
5.锁粗化
会将多个细粒度的锁,合并成一个粗粒度的锁,避免了重复加锁解锁的过程
3.CAS策略 (避免使用锁的另一种解决线程安全问题的策略)
全称叫做compare and swap 就是比较和交换,其实是一个cpu指令
关于CAS的api都放在java的unsafe包内,也就是暂时不推荐去使用的
我们简单介绍一下它的观点与使用技巧
这里给出一段伪代码
address:内存中的地址
expectValue:寄存器1中的值
swapvalue:寄存器2中的值
比较内存中的值和寄存器1的值是否相同,相同则直接交换(赋值),返回一个true不相同则无事发生,返回一个false
java标准库也提供了很多原子类,保证了多线程操作一个数据是原子的,本质上就是基于cas的
我们这个时候用两个线程给她进行自增50000次就会获取到正确的结果,而不会出现线程安全问题了
原始标准库里的代码略显复杂,这里我们使用简化版本的进行说明
这里的一次自增操作是先拿到旧数据,然后使用cas进行操作,如果判断相等则直接写入内存,不相等则更新一下目前的旧值为内存中的最新值,从而进行自增,这里就不会出现线程安全问题了
如有问题,希望大家多多指正