java中什么是锁?什么是cas?什么是AQS?

java中什么是锁?什么是cas?什么是AQS?


本人写下这篇博客是为了强化记忆和给也想明白的人指条道路,但可能本人还不是很理解透彻会造成理解偏差,请各位指正
这里感谢https://mp.weixin.qq.com/s/KFsqsCVgyiiTDXMR-Hu1-Q给的详细介绍,大部分引用其中
首先我们要对锁的机制有一定的了解,像同步锁synchronized、重入锁ReentrantLock等和一些属性volatile等:
其实lock的概念很简单,主要是机制,你明白机制和用法后,基本就会了。

synchronized

大部分入门的人,都知道这个,都知道把它加到对象,方法上就能锁住代码,它的原理也很简单,同步锁,顾名思义就是把访问到这个方法的都变成串行。
意思就是说一个方法很多线程来访问,本来大家谁调用用就行了,但你加了这个synchronized后就不一样了,那就不能一起上得一个一个来。这样来保证一次只会有一个调用。
但对于一些小的机制就不需要使用synchronized,因为它太笨重,像下面的情况:

public static int count = 0;
public static synchronized void inc() {
        count++;
}
public static void main(String[] args) {
        //同时启动1000个线程,去进行i++计算,看看实际结果
        for (int i = 0; i < 1000; i++) {
        	new Thread(new Runnable() {
                @Override
                public void run() {
                    Counter.incNum();
                }
            });
        	
        }
        //为了防止主线程结束了,子线程还没结束,用子线程join也可以
        try {
            Thread.sleep(2);
        } catch (InterruptedException e) {

        }
        //这里每次运行的值都有可能不同,可能为1000
        System.out.println("运行结果:Counter.count=" + Counter.count);
    }

在这里我只是想递增个变量,就用到了synchronized,但杀鸡焉用牛刀,java提供了像volatile、AtomicInteger这两个东西。
首先讲下volatile,上述变化中,最主要就是count++这个过程,这个过程在一个线程使用的时候,另外的过程不能使用,因为假如我进来使用的时候你也要用,但我把值加1的时候是在工作内存,并不会第一时间刷到主内存,你确在主内存中调用还没刷新的值,那就不行了,如图:
在这里插入图片描述
然后就可以见识下volatile的好处,它可以在每次完成后强制先刷入主内存,并且让其他线程已经取到这个值的线程,强制过期,如下图:
在这里插入图片描述
这样就简单了吧,可能很多人要问,为什么大家不直接读主内存,而都要自己有个工作内存,我只能告诉你是为了效率。然后代码就可以改成这样

public volatile static int count = 0;
public static void incNum() {
        count++;
    }
     public static void main(String[] args) {
        //同时启动1000个线程,去进行i++计算,看看实际结果
        for (int i = 0; i < 1000; i++) {
        	new Thread(new Runnable() {
                @Override
                public void run() {
                    Counter.incNum();
                }
            }).start();
        }
        try {
            Thread.sleep(2);
        } catch (InterruptedException e) {

        }
        //这里每次运行的值都有可能不同,可能为1000
        System.out.println("运行结果:Counter.count=" + Counter.count);
    }

你执行之后不一定成功都是1000,然后就会觉得我在骗你,但不是这样的,volatile主要解决的是java并发中可见性的问题,而不是锁的问题,当然我放在这里不怎么合理主要我是为了和AtomicInteger有一定的对比,那如果你觉得这两个对比个毛啊,完全没得比。但我就觉得这两个一对比我记得更深了,怎么了?

终极锁核心Atomic 什么是cas?

介绍这个得时候就不得不说AtomicInteger,就如上面得代码,我为了多线程下保持一个数据得累加就动用synchronized这种东西,诚然随着Java版本更新,也对synchronized做了很多优化,但是处理这种简单的累加操作,仍然显得“太重了”。人家synchronized是可以解决更加复杂的并发编程场景和问题的,而且,在这个场景下,你要是用synchronized,不就相当于让各个线程串行化了么?一个接一个的排队,加锁,处理数据,释放锁,下一个再进来。你不嫌麻烦,程序都嫌你麻烦。

那什么可以替代呢?AtomicInteger就可以,上面说得volatile并不可以,再次提醒你volatile解决的是java并发中可见性的问题,像count++这种多线程调用其实它并没有办法解决,还是会发生调用出错,所以用的时候你一定要明白你到底在干啥?

现在介绍的AtomicInteger就解决了这个问题,不得不说前人的高智商,不愧为大牛,原子操作的出现,锁的核心也就诞生了。有了AtomicInteger上面的代码前半部分就可以改为

public static AtomicInteger count = new AtomicInteger(0);
public static void incNum() {
        count.incrementAndGet();
}

这样就完美解决了多线程递增问题,那么问题来了AtomicInteger是怎么做到的呢?难道它锁住了?

实际上,Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

那什么是CAS呢?他的全称是:Compare and Set,也就是先比较再设置的意思。原理如图:
在这里插入图片描述
首先,每个线程都会先获取当前的值,接着走一个原子的CAS操作,原子的意思就是这个CAS操作一定是自己完整执行完的,不会被别人打断。
然后CAS操作里,会比较一下说,唉!大兄弟!现在你的值是不是刚才我获取到的那个值啊?
如果是的话,说明没人改过这个值,那你给我设置成累加1之后的一个值好了!
同理,如果有人在执行CAS的时候,发现自己之前获取的值跟当前的值不一样,会导致CAS失败,失败之后,进入一个无限循环,再次获取值,接着执行CAS操作!
这样就能保证数据会正常显示而不会因为什么多线程而发生错误,这个原子操作也成为了一种锁机制的核心(cas)。

很神奇是不是前人的想法,但这种方式也存在缺点,当线程过多的时候,多个线程要改值发现被人改了,那它们都要自旋,原地打转,这样就会发生问题,不断的再次进入下一个循环,获取值,发起CAS操作又失败了,再次进入下一个循环,java8提出了新类LongAdder,基本原理就是里面实现了分段CAS的机制,有兴趣可以去认识一下。

那cas都明白了,AQS还难?

AQS全称AbstractQueuedSynchronizer,抽象队列同步器
先来段大家熟悉的加锁:

ReentrantLock lock = new ReentrantLock();
lock.lock();
//业务逻辑代码
lock.unlock();

你这时可能会问,这个跟AQS有啥关系?关系大了去了!因为java并发包下很多API都是基于AQS来实现的加锁和释放锁等功能的,AQS是java并发包的基础类。

举个例子,比如说ReentrantLock、ReentrantReadWriteLock底层都是基于AQS来实现的。

给大家画一个图先,看一下ReentrantLock和AQS之间的关系。
在这里插入图片描述
也就是说AQS是基石,那么我前面扯什么cas,原子操作什么的又和AQS有半毛钱关系?当然有关系不然觉得我闲的蛋疼。
好吧,我现在的确闲。

首先我给大家上个原理图,你就明白cas在AQS的作用,
在这里插入图片描述
看到没cas不是用到了?而且很重要。
现在我给大家解读下这个图,
1.线程1进入锁,发起cas,取得当前state值,询问值,是否是0,假如是就设置state为1,线程1进入到加锁线程。

2.此时线程2,也同时加锁,取得state,cas尝试把state从0变成1,发现不行,因为已经被人动过了,这个时候还会看看自己之前是否加过锁,不是,则把自己放入等待队列。

3.线程1用完后,unlock会释放锁,重置state等于0,退出加锁线程,去通知线程2,小老弟我好了换你来,线程2再次尝试加锁,cas尝试把state值从0变1成功后把自己加入,加锁线程。

步骤就这样,这里面也用到了cas,所以AQS大致就这个原理,当然你可能也听过公平锁和非公平锁。

公平和非公平,给大家个比喻,就不细说了,其实这个就好比上厕所,你要进厕所才能上,有人先进去了,你排队,后面又有人来排队,假如是不公平的,后来是个壮汉你干不过么,只能让人家先,但你也很急啊,但没办法人家运气好生的这么壮。那你能怎么办,只能忍一时越想越气。
然后就有了公平机制的出现,我先排队肯定我先的原理,然等待队列有了秩序。

怎么样简单明了吧,那代码怎么实现的呢?很简单只要加个true就好,如下

ReentrantLock lock = new ReentrantLock(true);

那基本上cas,AQS,锁这些就介绍完了,我个人感觉大体上我是讲明白了,要是以上内容有错,欢迎指正,我也不想因为自己理解有误害了别人。

其实还有读写锁ReentrantReadWriteLock,但今天突然有事出去,下次再说吧。

再次感谢:石杉的架构笔记,我是从这里明白这些的。当然我也盗图了,我的确有点懒,假如侵权的话请通知我,我再自己画几个上去。
https://mp.weixin.qq.com/s/9SKmHeMu_1lNVEJPlEcpsA

猜你喜欢

转载自blog.csdn.net/qq_36963177/article/details/86635978