前言

年轻人,醒醒吧!此时不搏何时搏!本文主要讲一下常见的CAS理论。再者就是说一下锁的分类,什么乐观锁啊,悲观锁、重入锁等等。这篇文章要一网打尽,都介绍一下。

java多线程抢锁会有顺序吗_并发编程

把CAS按在地上摩擦

中文名:比较并交换

英文名:Compare And Swap

英文缩写:CAS

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

java多线程抢锁会有顺序吗_面试_02

解释一下:当A、B两个线程都操作value值时,线程A如果一切顺利,会在进行预期值与内存值做比较且相等,这个动作是原子化操作,这时候执行原子的修改value值的操作。修改完成后,B线程也来修改,发现有敌情,只好原地循环等待,直到条件符合时才进行内存值的操作。还有一点要注意的是,比对和修改两个动作都是原子的,但是原子操作 + 原子操作 != 原子操作。多线程高并发,搞的额头没头发。

java多线程抢锁会有顺序吗_并发编程_03

java多线程抢锁会有顺序吗_java多线程抢锁会有顺序吗_04

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

锁的分类

乐观锁与悲观锁

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

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

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

java多线程抢锁会有顺序吗_java_05

自旋锁与非自旋锁

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

java多线程抢锁会有顺序吗_java_06

画图是真的费眼睛,大家有什么比较好的画图方式吗?可以分享一下。这里再解释一下,自旋锁并不完美,有很多缺点,比如自旋时如果此时控制不当,会造成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更用不到。

java多线程抢锁会有顺序吗_面试_07

公平锁与非公平锁
公平锁

是指多个线程按照申请锁的顺序来依次获取锁,线程直接进入队列中排队,当共享资源可用时,只有队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。但是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,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对共享数据加上共享锁后,则其他线程也只能对共享数据加共享锁,不能加排它锁。而获得共享锁的线程只能读数据,不能修改数据。

最后贴一张锁分类图:

java多线程抢锁会有顺序吗_面试_08