9

干饭人!多线程锁分类你不看看吗?肆

 3 years ago
source link: http://www.cnblogs.com/RollerRunning/p/14089109.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

开篇闲扯

打工人,你是不是也不喜欢吃挂面?吃多了面试容易挂!咔~~好冷的段子。分享一个小故事,中午我对象聊天,她说中午食堂吃的海鲜拌面。我立马就羡慕的问有啥海鲜,你猜怎么着?人家说就放了两块鱼豆腐...食堂的文案工作者为了业绩也是辛苦了!致敬这不讲武德的宣传

N3YfQjA.png!mobile

回归正题,年轻人,醒醒吧!此时不搏何时搏!本文主要讲一下常见的CAS理论,因为上文也提到了,但是没来得及解释。再者就是说一下锁的分类,什么乐观锁啊,悲观锁、重入锁等等。这篇文章要一网打尽,都介绍一下。最后想说一下,其实这篇文章应该在讲Synchronized之前写的,结果写的时候搞错了,可我就是不想改!

yyMzeem.png!mobile

把CAS按在地上摩擦

中文名:比较并交换

英文名:Compare And Swap

英文缩写:CAS

他是一种无锁化基于乐观锁思想实现的算法,目的是在不使用锁的情况下实现多线程之间的共享数据同步。在Java的java.util.concurrent包中的原子类(不是原子弹)就是基于CAS的实现的。在CAS的算法世界中,存在三大护法:value(要更新的变量)、expected(预期值)和new(要新写入的值)。下面画图说明CAS是如何实现不加锁的情况下协调多线程同步共享数据的:

fQBBVrB.png!mobile

解释一下:当A、B两个线程都操作value值时,线程A如果一切顺利,会在进行预期值与内存值做比较且相等,这个动作是原子化操作,这时候执行原子的修改value值的操作。修改完成后,B线程也来修改,发现有敌情,只好原地循环等待,直到条件符合时才进行内存值的操作。还有一点要注意的是,比对和修改两个动作都是原子的,但是原子操作 + 原子操作 != 原子操作。因此在高并发下存在ABA问题,这个在前面《打工人!肝了这套多线程吧!壹》中有说明,感兴趣可以翻看一下。多线程高并发,搞的额头没头发。

bqemeyY.png!mobileuyIzaa.png!mobile

理想与现实的差距就是这么大....

锁的分类

乐观锁与悲观锁

这二位其实并不是实际存在的锁,仅仅是对锁的抽象定义。乐观锁的目的就是不加锁,从而提升效率。这一思想在Java以及基于数据库实现的乐观锁中都有实践。

在乐观锁的概念里,认为所有的数据都是为我当前线程服务的,在我使用的过程中不会有别的线程修改我的数据(哼,想多了),但是为了保险起见,在更新目标数据的时候还是要做一次对比,即前面说的CAS过程。不过乐观锁是思想,CAS是算法。搞清楚这个就行了。

在悲观锁的概念里,跟乐观锁恰好相反,它的核心是“总有刁民想害朕”即所有线程都可能修改自己持有的数据。因此在读取数据的时候就赶紧上锁,其他人都别想动我的宝贝!大家都立正,一个一个按顺序来。比如前面写到的Synchronized和后面将要写的Lock接口,还有就是基于数据库的悲观锁:select xx from xx where xxx for update

eAzUFjn.png!mobile

自旋锁与非自旋锁

自旋锁其实就是前一篇中说的轻量级锁,还有兄弟是自适应自旋锁,目前自旋锁是被废了的太子,自适应自旋锁顶替了太子之位了,因为它可以自动的动态调整自旋次数,以达到最高效的运行状态,具体根据那些参数自动调整,可以看《是兄弟!就来看这篇多线程!叁》。而非自旋锁则是当目标资源被占用时,直接进入休眠状态了(遇到困难,睡大觉),等资源就绪后会被再次唤醒并尝试获取锁,这样就造成了反复的内核态与用户态的切换,浪费系统资源。一张图,展示一下自旋与非自旋的差异性:

Q3uYzuE.png!mobile

画图是真的费眼睛,大家有什么比较好的画图方式吗?可以分享一下。这里再解释一下,自旋锁并不完美,有很多缺点,比如自旋时如果此时控制不当,会造成CPU资源的浪费,JDK也在不断的优化这些锁的性能。

再往深了说,其实在自旋锁中还分为三种:TicketLock、CLHlock和MCSlock。

TicketLock

看名字就知道:票据锁,即想要获取锁,你要出示对应的凭证,对上号了,才能把锁给你。跟你去银行取钱似的,拿对卡,输对密码才能给你取钱。

/**
 * FileName: TicketLock
 * Author:   RollerRunning
 * Date:     2020/12/3 9:34 PM
 * Description:
 */
public class TicketLock {
    //保证可见性
    volatile int flag = 0;
    AtomicInteger ticket = new AtomicInteger(0);
    void lock() {
        int getTicket = ticket.getAndIncrement();
        while (getTicket != flag) {
            
        }
    }

    void unlock() {
        flag++;
    }
}

还记得前面讲的Volatile是基于总线监听实现的可见性吗?这里如果线程特别多,大家都在监听flag,这对于带宽容量有限的主存来说,线程的不断增加,压力会越来越大,这也桑畅TicketLock的缺点。

CLHlock

CLHLock其实是三个人发明的:Craig, Landin和Hagersten所以叫CLH了,它的底层是基于链表的公平自旋锁。赫赫有名的AQS(AbstractQueuedSynchronizer)就是基于这种锁变种而来的。在CLH中,所有相互竞争的线程都被放到一个链表中排队,每一个线程被抽象成一个链表的节点,每一个节点在前趋结点的locked域上自旋等待。当前驱释放锁状态,则后续节点就可以进行获取锁的操作。在以后的文章里会手撕AQS的,今天主要介绍一下有啥,埋个种子。

MCSlock

CLHLock这么牛13了,还整个MCSLock干啥呢?原因竟然是为了兼容硬件系统,从架构上来看,分为三大怪物:SMP, MPP和NUMA,问题就处在了这个NUMA上了。它的中文名是:非一致存储访问结构。正是因为这种结构,导致了在使用CLHLock时,后节点在获取前节点中的locked域状态时内存过远。行了,当做八股文背住就行了,面试估计也没人问这个,写BUG更用不到。

auqaAn3.png!mobile

公平锁与非公平锁

公平锁

是指多个线程按照申请锁的顺序来依次获取锁,线程直接进入队列中排队,当共享资源可用时,只有队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。但是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁

是指多个线程加锁时直接尝试获取锁,加锁失败时,才会被放入队列中去。但如果此时锁刚好可用,那么这个线程可以直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。优点是可以减少唤起线程的开销,吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

重入锁和不可重入锁

可重入锁

以Java为例ReentrantLock和Synchronized都是可重入锁,是指在同一个线程在外层方法获取锁的时候,如果其内部调用的方法也有锁,则可以直接获取锁,不会因为之前的锁还没释放而阻塞,一定程度上避免了死锁。在下面的代码中,testRoller()和testRunning() 都是加了锁的两个方法,因为Synchronized是可重入锁,所以在testRoller()中调用testRunning()时,可以直接获取锁。

/**
 * FileName: TestLock
 * Author:   RollerRunning
 * Date:     2020/12/3 9:34 PM
 * Description:
 */
public class TestLock {
    public synchronized void testRoller() {
        System.out.println("testRoller....");
        testRunning();
    }

    public synchronized void testRunning() {
        System.out.println("testRunning....");
    }
}

共享锁和独占锁

独享锁

是一种吃独食的锁,一次只能被一个线程持有。如果线程A对共享数据独占锁以后,那么其他线程就都没有机会再加锁了,获得排它锁的线程就拥有了对该数据的读写权限。JDK中的Synchronized以及Lock的实现类就是互斥锁。

共享锁

是指这类锁可被多个线程持有。如果线程A对共享数据加上共享锁后,则其他线程也只能对共享数据加共享锁,不能加排它锁。而获得共享锁的线程只能读数据,不能修改数据。

这两类锁会在下一篇讲Lock时结合实例详细讲解,今天就讲到这里,太晚了,睡觉,晚安!

最后贴一张锁分类图:

vaIVfia.png!mobile

感谢各位观众老爷,点赞关注加转发!谢谢

更多文章请扫码关注或微信搜索 Java栈点 公众号!

RFRr2yY.jpg!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK