Java 锁策略、CAS、synchronized原理

锁策略

乐观锁和悲观锁

        乐观锁:假设接下来发生冲突的可能性很小,每一次数据在更新的时候,才会正式对数据是不是发生冲突检测。如果发生冲突了,返回错误信息让用户决定。

简单认为,加锁没有那么容易出现锁冲突。

        悲观锁:假设接下来发生冲突的可能性很大,每一次的数据操作都要加锁,其他线程要拿到这个数据只能阻塞。

简单认为,只要加锁,就会出现锁冲突。

        比如说,疫情时期,出现阳性的时候。我认为接下来封城的可能性很小,没必要做什么,要发生的事概率很小,这就是一个乐观锁;而有的人认为封城的可能性很大,赶紧去屯点吃的在家里。要发生的事的概率很大,就是一个悲观锁。

synchronized刚开始使用乐观锁,到后面发现锁的竞争比较频繁的时候,就会自动切换成悲观锁。synchronized锁是一个自适应锁。

读写锁和普通互斥锁

        多线程之间,读数据的时候不会产生线程安全,但是写数据的时候会产生线程安全。如果这个时候只使用普通的锁(有互斥的功能),会产生大量的性能损耗。

读写锁在读的时候和写的时候执行不同的锁策略。对于同一个数据,

(1)线程a尝试加写锁,线程b尝试加写锁,ab产生了锁竞争。这个时候和普通的锁没有区别;

(2)线程a尝试加读锁,线程b尝试加读锁,ab不竞争,相当于每一次加锁,不涉及数据的修改,是线程安全的。这是多线程中的普遍情况;

(3)线程a尝试加读锁,线程b尝试加写锁,ab竞争,和普通锁没有区别。

读写锁把读操作和写操作区分开来对待,其中,读和写、写和写之间发生互斥。比较适合频繁读,不频繁写的场景。

synchronized不是读写锁。

重量级锁和轻量级锁

        锁是基于内核的功能实现的。CPU提供了原子操作指令后,操作系统根据CPU原子指令,实现mutex互斥锁。JVM基于操作系统提供的互斥锁,实现了synchronized 和 ReentrantLock 等关键字和类。

        重量级锁:要做更多的事,开销很大。加锁机制严重依赖操作系统提供的mutex,涉及到大量的内核态用户态切换,很容易引发线程的调度。涉及到用户态和内核态的切换,意味着开销比较大,很容易产生阻塞。

        轻量级锁:要做的事很少,开销很小。加锁机制尽量不使用mutex,而是尽量在用户态代码完成。实在不行了,再使用mutex。涉及到少量的内核态用户切换,尽量避免了阻塞。

synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁。

与乐观锁悲观锁的含义有一定的重叠。悲观锁一般都是重量级锁,而乐观锁一般都是轻量级锁。

挂起等待锁和自旋锁

        挂起等待锁往往是通过内核来实现,而自旋锁往往是通过用户态的代码实现。线程在抢锁失败后进入阻塞状态,过不了多长时间,锁就会被释放。这个时候,可以使用自旋锁来处理这样的问题。自旋锁是典型的轻量级锁的实现方式。

自旋锁的特点:

(1)没有放弃CPU,不涉及线程的阻塞和调度。能够第一时间获取锁;

(2)如果锁被其他线程的持有时间比较长,就会持续消耗CPU资源。挂起等待锁不消耗资源。

synchronized中的轻量级锁策略大概率通过自旋锁的方式实现。

公平锁和非公平锁

公平锁:线程遵守先来后到的原则;

非公平锁:线程不遵守先来后到的原则;

假设有三个线程A、B、C。顺序是ABC。A先获取锁,其他的线程阻塞。当A释放锁的时候,如果是公平锁,那么接下来就是B获取,B释放后就是C获取。如果是非公平锁,那么B和C抢占,都有可能先获取锁。

操作系统内部的线程调度是随机的,如果不做任何额外的限制,锁就是非公平的锁。

synchronized 是非公平锁。

可重入锁和不可重入锁

        可重入锁:可以重新进入的锁。允许同一个线程多次获取同一把锁。可重入锁会在内部记录这个锁是哪一个线程获取到的,如果当前的锁和加的锁是同一个锁,就不用挂起等待。

        不可重入锁:对于同一个线程,第一次加锁,加锁成功。第二次加锁,就会重新阻塞,等待第一次的锁被释放,才能获取到第二个锁。但是释放第一个锁由该线程完成。

synchronized 是可重入锁。

CAS

        CAS是操作系统/硬件,给JVM提供的另一种更加轻量级的原子操作的机制。字面意思是“比较并交换”。是CPU提供的特殊指令:compare and swap,虽然是涉及到多个操作,但是编译器认为是一条指令(原子性)。一个CAS涉及到一下的操作:假设内存中源数据是V,旧的预期值是A,需要修改的值是B。

(1)比较A和V是不是相等(比较);

(2)如果比较相等,把B写入V(交换);

(3)返回操作是否成功。

        当多个线程对某一个资源进行CAS操作,只有一个线程能够成功,但是不会阻塞其他线程,其他线程会收到操作失败的信号。CAS可以看成一种乐观锁。

实现

(1)java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;

(2)unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;

(3)Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子 性。

简而言之,是因为硬件予以了支持,软件层面才能做到。

应用场景

使用AtomicInteger实现i++操作。

//直接++,涉及原子性
public class demo {

    private static int SUM = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                ++SUM;
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                ++SUM;
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println(SUM);
    }
}

//保证原子性
public class demo {

    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                atomicInteger.getAndIncrement();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                atomicInteger.getAndIncrement();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println(atomicInteger);
    }
}

 还可以实现自旋锁。基于CAS实现更加灵活的锁,获取更多的控制权。

//伪代码
public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

ABA问题

        有这样的情况,假设有人去银行转账。A银行卡有1000元,取款机创建了两个线程并发执行。A给别人转账500元。

正常的逻辑是:

(1)有存款1000,线程1获取到当前存款1000,目的要减少500;线程2获取到当前存款,目的要减少500元;

(2)线程1先扣了500元成功,这个时候线程2阻塞中;

(3)到线程2执行的时候,发现少了500,和之前的1000元不相同,就不扣款。

如果是CAS的过程:

(1)有存款1000,线程1获取到当前存款1000,目的要减少500;线程2获取到当前存款,目的要减少500元;

(2)线程1先扣了500元成功,这个时候线程2阻塞中;

(3)线程2执行前,A的卡中正好有人转账进来500,这个时候余额编程1000;

(4)到线程2执行的时候,发现是1000,和之前的1000元相同,扣款500元。

整个过程,无法区分数据始终是A,还是从A到B再到A。

        解决方案是给要修改的值,引入版本号,CAS比较当前数据的值和以前的是不是相同的同时也要比较版本号。规定,如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1。 如果当前版本号高于读到的版本号,就操作失败(认为数据已经被修改过了)。

synchronized 原理

特点

是一个自适应锁。

(1)既是乐观锁,也是悲观锁。开始的时候是乐观锁,如果锁冲突频繁,转变为悲观锁;

(2)既是重量级锁,也是轻量级锁。轻量级锁的内部实现一般是是自旋锁。重量级锁的内部实现一般是挂起等待锁;开始是轻量级锁实现,如果锁被持有的时间较长,就转换成重量级锁。

(3)是非公平锁;

(4)是可重入锁;

(5)不是读写锁。

加锁的过程

没有加锁之前是无锁;

(1)刚开始的时候,还没有产生竞争,这个时候是偏向锁。偏向锁不是真正的加锁,只是一个标识,表示这个锁属于这个线程。遇到其他的线程竞争之前,始终保持这个状态,直到产生竞争,才是真正的加锁。类似于单例模式中的懒汉模式,只在必要的时候创建实例。

(2)产生竞争了,随着其他线程进入竞争,偏向锁的状态被消除,使用轻量级锁,通过CAS来实现。

(3)竞争加剧,轻量级锁就会变成重量级锁。涉及到内核态和用户态之间的转换。

其他优化

        锁消除:有些锁虽然在多线程上对每一步的操作加了锁,但是单线程的时候,就是没有必要的,浪费资源。JVM在运行时,虽然代码进行了同步,但是如果虚拟机检测到不存在数据竞争时,虚拟机就会自动把锁进行消除。锁消除大部分情况下不触发,由编译器自动判断,认为代码不需要加锁的时候,就不会加锁。

        锁粗化:把锁的同步范围扩大。我们在写代码的时候,哦那个是尽可能减小锁的作用范围,但是连续的对同一个对象反复加锁解锁,也会浪费资源。所以需要锁粗化,把锁的同步范围扩大。锁越细,越能提高并发的效率,但是增加加锁解锁的次数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值