这个部分主要是一些面试常考的八股文,主要是为了应付面试。不必太纠结其细节。
注意:
锁策略和普通的程序猿基本没啥关系和”实现锁“的人才有关系。
这里所提到的“锁策略”,和 Java
本身没有关系,适用于所有和“锁”相关的情况。
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这
样别人想拿这个数据就会阻塞直到它拿到锁。
乐观锁:
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并
发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
简单说一下我理解的:
悲观锁:预期锁冲突的概率很高。例如:悲观的态度认为下一波疫情来了之后,可能买不到菜要抢菜,于是换了一个很大的冰箱并且去超市定期屯一些米面油肉+方便面+生活用品。【要做更多的事情,付出更多的成本和代价,更低效】
乐观锁:预期锁冲突的概率很低。例如乐观的态度认为下一波疫情即使来了,但是不会出现买不到菜和抢菜的情况,便不做过多的预备打算。【做的事情更少,付出的成本和代价更少,更高效】
普通的互斥锁,只有两个操作:加锁 和 解锁。
只要两个线程针对同一个对象进行加锁,就会产生 互斥 / 锁竞争。
对于读写锁来说,分成了三个操作:
1、加读锁:如果代码只是进行读操作,就加读锁
2、加写锁:如果代码中只是进行了修改操作,就加写锁。
3、解锁
这样就把读和写操作给天然的分离开来了
重量级锁就是做了更多的事情开销更大
轻量级锁就是做的事情更少开销更小
例如:
在使用的锁中,如果锁是基于内核的一些功能来实现的(比如调用了操作系统提供的mutex接口),此时一般认为这是重量级锁
如果锁是纯用户态实现的,此时一般认为是轻量级锁。(用户态的代码更可控,也更高效)
也可以认为通常情况下,悲观锁一般都是重量级锁,乐观锁一般都是轻量级锁。(这种观点并不绝对)
挂起等待锁往往就是通过内核的一些机制来实现的,往往较重【重量级锁的一种典型实现】
自旋锁往往就是通过用户态代码来实现的往往较轻【轻量级锁的一种典型实现】
首先评价一件事情是公平还是不公平的标准就是依靠制定的规则,在这儿我们的规则是:遵循先来后到
公平锁:多个线程在等待一把锁的时候,谁先来谁就能够先获取到这个锁
非公平锁:多个线程在等待一把锁的时候,不遵循先来后到的规则
要想实现公平锁反而需要付出更多的代价(要整个队列来把这些参与竞争的线程给排一排先来后到~~)
一个线程,针对一把锁,咔咔连续加锁两次,如果会死锁,就是不可重入锁,如果不会死锁,就是可重入锁~
对于上面的知识,不用深入了解,万一面试官真的说到上述的一些词,不至于太懵逼~就okk。
1.既是一个乐观锁,同时也是一个悲观锁(根据锁竞争的激烈程度,自适应)
2.不是读写锁,只是一个普通互斥锁
3.既是一个轻量级锁,也是一个重量级锁(根据锁竞争的激烈程度,自适应)
4.轻量级锁的部分基于自旋锁来实现,重量级锁的部分基于挂机等待锁来实现
5.非公平锁
6.可重入锁
原理:要做的事情就是拿着寄存器/某个内存中的值和另外一个内存的值进行比较,如果值相同了,就把另一个寄存器/内存的值和当前的这个内存进行交换。
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B
1.比较A与V是否相等(比较)
2.如果比较相等,将B写入V。(交换)
3.放回操作是否成功。
此处所谓的CAS指的是,cpu提供了一个单独的CAS指令,通过这条指令,就能完成上诉伪代码描述的过程。
如果上诉过程都是这“一条指令”就干完了,就相当于这是原子的了(CPU上面执行的指令就是一条一条执行的~指令已经是不可分割的最小单位)此时线程就代表是安全的。
CAS最大的意义就是让我们写这种多线程安全的代码,提供了一个新的思路和方向(就和锁不一样了)
举一个例子把:
1.基于CAS能够实现“原子类”
Java标准库里提供了一组原子类,针对锁常用的一些long,int,intarray。。。。进行了封装,可以基于CAS的方式修改,并且线程安全)
import java.util.concurrent.atomic.AtomicInteger;
class test{
public static void main(String[] args) throws InterruptedException {
AtomicInteger num=new AtomicInteger(0);//使用原子类
Thread t1=new Thread(()->{
for (int i =0 ;i<50000;i++)
{
num.getAndIncrement();
}
});
t1.start();
Thread t2=new Thread(()->{
for(int i =0;i<50000;i++)
{
num.getAndIncrement();
}
});
t2.start();
t1.join();
t2.join();
System.out.println(num.get());
}
}
原子类背后具体是怎么实现的?
class AtomicInteger{
private int value;
public int getAndIncrement(){
int oldValue=value;
while (CAS(value,oldValue,oldValue+1)!=true)
//CAS就是来判定一下,当前内存的值,是不是和刚才取出的值是一样的,
// 如果两个值是一样的,就把value值,设为oldValue+1,同时放回true,循环结束
//如果两个值不一样,就啥也不做,返回false,进行下次循环继续判断
{
oldValue=value;
}
return oldValue;
}
}
2.基于CAS能够实现“自旋锁”
public class SpinLock{ //自旋锁
private Thread owner=null; //记录下当前锁被哪个线程持有了~为null表示当前未加锁
public void lock()
{
//通过CAS看当前锁是否被某个线程持有
//如果这个锁已经被别的线程持有,那么就自旋等待
//如果这个锁没有被别的线程持有,那么久把owner设为当前尝试加锁的线程
while (!CAS(this.owner,null,Thread.currentThread())){
}
}
}
public void unlock()
{
this.owner=null;
}
和刚才的原子类类似,也是通过一个循环来实现的,循环里面调用CAS
,CAS
会比较当前的owner
值是否为null
,如果是null
就改成当前线程,意思是当前线程拿到了锁,如果不是null
就返回false
,进入下一次循环,下次循环仍然是进行CAS
操作,如果当前这个锁一直被别人持有,当前尝试加锁的线程就会在这个while
的地方快速反复的进行计算—自旋~~忙等
自旋锁是一个轻量级锁,也可以视为是一个乐观锁~
当前这把锁虽然没能立即拿到,但是预期很快就能拿到(假设锁冲突不激烈)
短暂的自旋几次,浪费点CPU
问题都不大,好处就是只要这边锁一释放,就能立即的拿到锁~
CAS
中的关键是先进行比较,再进行交换,比较当前值和旧值是不是相同,如果这两个值相同就视为是中间没有发生过改变。
但是这里的结论存在漏洞,当前值和旧值可能是中间确实没改变过,也有可能变了,但是又变回来了,这样的漏洞,在大多数情况下,其实没啥影响~~~,但是,极端情况下也会引起bug~
ABA问题就好比,我今天买了一个手机,我拿到这个手机,以我的技术我无法区分出这个是一个全新的手机还是翻新机,虽然说翻新机大多数情况下,也是能用的,说不定还挺不错,但是少数情况下还是可能翻车~~
举一个典型的例子~因为ABA问题产生bug(有点复杂不过很容易看懂)
那就非常容易想到了,类似于给他贴一个标签,每次给他++
或者--
引入一个“版本号”,这个版本号,只能变大,不能变小。
在修改变量的时候,比较就不是比较变量本身了,而是版本号了。
其实锁优化机制可以理解为就是编译器对锁的优化
首先我们得要回顾一下synchronized是属于哪些锁:
1.既是一个乐观锁,同时也是一个悲观锁(根据锁竞争的激烈程度,自适应)
2.不是读写锁,只是一个普通互斥锁
3.既是一个轻量级锁,也是一个重量级锁(根据锁竞争的激烈程度,自适应)
4.轻量级锁的部分基于自旋锁来实现,重量级锁的部分基于挂机等待锁来实现
5.非公平锁
6.可重入锁
synchronized 几个典型的优化手段:
1、锁膨胀/锁升级
synchronized
会根据自身的情况来对锁进行升级(体现了能够“自适应”这样的能力)