Java常用锁及源码实现分析

常用锁

  • 乐观锁(无锁)
  • Lock(显示锁)
  • synchronized(内置锁)

一、乐观锁

实现方式:CAS(compare and swap:比较并交换),就是在更新一个变量之前,先获取这个变量是否和预期相等,如果相等则更新,否则什么也不做。

例如:

  • 数据库更新:update user set name=xxx where xxx=xxx,只有当where符合条件才更新,否则什么也不做。
  • Atomic操作

AtomicInteger底层源码

我们在多线程并发i++时,通常会使用atomicInteger.incrementAndGet();来保证线程安全。

来看incrementAndGet方法源码【以i增1为例,i当前是6】:

  • 首先获取当前i的值(6)
  • 然后计算我增1之后应该是多少(7)
  • 然后比较当前的值+1和我计算的7是否相等
  • 如果相等则直接修改值,如果不相等则while循环一直等,直到相等。
public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);// 读取当前的值
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));// 比较

        return var5;
    }

public native int getIntVolatile(Object var1, long var2);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

CAS存在的问题

  • 每一次比较和交换时,都没有得到修改的机会,达到一定次数时如果还没有停下来,则会一直浪费CPU,导致CPU占用非常高
  • 无法实现复杂场景下的线程安全,比如:i*5+6
  • ABA问题

ABA问题及解决

A->B->A,最终结果仍然为A,CAS发现不了中间B的修改过程。

  • 八大基本数类型不存在ABA问题,因为变了就是变了,不代表什么。
  • 只有引用类型才存在ABA问题,比如银行搞活动,存款满10万赠送一袋大米,当月仅赠送一次,但不解决ABA问题,每次你取出去2万,再存2万,又到了十万,银行就又要给你一袋大米,因此这个转入转出的过程必须记录,才能解决ABA问题。

解决方式:AtomicStampedReference类,其提供了compareAndSet方法,可以:以版本号或者时间戳的方式,对比要更新的版本号是否等于预期的版本号,如果等于,则以原子方式更新,否则什么也不做。


创建银行账户实体,包含账户名及转账金额两个字段,初始张三的原始金额为8万

public static class BankAccount {
        private String name;// 账户名
        private Integer money;// 转账金额(转入或转出)
    }
BankAccount zhangSan = new BankAccount("张三", 8);// 初始张三的原始金额为8万

ABA问题代码演示

首先使用未解决ABA问题的原子类:AtomicReference

AtomicReference<BankAccount> bankAccountAtomicReference = new AtomicReference<>(zhangSan);

AtomicInteger atomicInteger = new AtomicInteger(0);// 计数
        for (int i = 0; i < 100; i++) {
            // 入账,每次2万
            new Thread(() -> {
                BankAccount oldAccount = bankAccountAtomicReference.get();
                BankAccount newAccount = new BankAccount("张三", oldAccount.getMoney() + 2);

                boolean result = bankAccountAtomicReference.compareAndSet(oldAccount, newAccount);
                if (result && bankAccountAtomicReference.get().money >= 10) {
                    // 计数,计算兑换了多少袋大米
                    atomicInteger.incrementAndGet();
                    // 只要余额>=10万,就送大米,无法判断之前有没有送过
                    System.out.println("领一袋大米");
                }
            }).start();
            // 出账,每次2万
            new Thread(() -> {
                BankAccount oldAccount = bankAccountAtomicReference.get();
                BankAccount newAccount = new BankAccount("张三", oldAccount.getMoney() - 2);
                bankAccountAtomicReference.compareAndSet(oldAccount, newAccount);
            }).start();
        }
        Thread.sleep(1000);
        System.out.println("成功兑换了:" + atomicInteger.get() + "袋大米,可真鸡贼");

运行结果【多拿了人家好多袋大米】:

可以看出,100次的来回入账出账,这个鸡贼的客户一不小心领了银行89袋大米,银行亏大了!但细心的朋友会发现,为什么是89袋而不是100袋?这是因为在并发过程中,CAS操作时可能会有多个线程获取到同一个值,导致只能有一个线程更新成功,解决办法就是加上死循环一直等,类似AtomicInteger.incrementAndGet()源码,上文分析过,直接上case:

AtomicReference<BankAccount> bankAccountAtomicReference = new AtomicReference<>(zhangSan);

AtomicInteger atomicInteger = new AtomicInteger(0);
        for (int i = 0; i < 100; i++) {
            // 入账,每次2万
            new Thread(() -> {
                // 增加死循环,防止线程冲突CAS失败,失败后一直等直到CAS成功,释放线程
                while (true) {
                    BankAccount oldAccount = bankAccountAtomicReference.get();
                    BankAccount newAccount = new BankAccount("张三", oldAccount.getMoney() + 2);

                    boolean result = bankAccountAtomicReference.compareAndSet(oldAccount, newAccount);
                    if (result && bankAccountAtomicReference.get().money >= 10) {
                        // 计数,计算兑换了多少袋大米
                        atomicInteger.incrementAndGet();
                        // 只要余额>=10万,就送大米,无法判断之前有没有送过
                        System.out.println("领一袋大米");
                        break;// 释放线程
                    }
                }
            }).start();
            // 出账,每次2万
            new Thread(() -> {
                BankAccount oldAccount = bankAccountAtomicReference.get();
                BankAccount newAccount = new BankAccount("张三", oldAccount.getMoney() - 2);
                bankAccountAtomicReference.compareAndSet(oldAccount, newAccount);
            }).start();
        }
        Thread.sleep(1000);
        System.out.println("成功兑换了:" + atomicInteger.get() + "袋大米,可真鸡贼");

运行结果【这次数量正确啦~~】:

ABA解决问题代码演示

然后使用解决了ABA问题的原子类:AtomicStampedReference

AtomicStampedReference<BankAccount> bankAccountAtomicStampedReference = new AtomicStampedReference<>(zhangSan, 8);

        int stamp = bankAccountAtomicStampedReference.getStamp();
        for (int i = 0; i < 100; i++) {
            // 入账,每次2万
            new Thread(() -> {
                BankAccount oldAccount = bankAccountAtomicStampedReference.getReference();
                BankAccount newAccount = new BankAccount("张三", oldAccount.getMoney() + 2);

                boolean result = bankAccountAtomicStampedReference.compareAndSet(oldAccount, newAccount, stamp, stamp + 1);
                if (result && bankAccountAtomicStampedReference.getReference().getMoney() >= 10) {
                    // 只有首次变成10万时,才送大米,之后因为有stamp的匹配限制,后面的10万均不在活动范围内
                    System.out.println("领一袋大米");
                }
            }).start();
            // 出账,每次2万
            new Thread(() -> {
                BankAccount oldAccount = bankAccountAtomicStampedReference.getReference();
                BankAccount newAccount = new BankAccount("张三", oldAccount.getMoney() - 2);
                bankAccountAtomicStampedReference.compareAndSet(oldAccount, newAccount, stamp, stamp + 1);
            }).start();
        }

运行结果【就第一次到10万的时候拿了一袋,没毛病!】:

二、Lock

常用:ReentrantLock(底层原理:CAS)

可能实现方法:

  1.  当CAS没成功时,采用while死循环进行自旋,直到获取到锁
  2.  方法1太占CPU,可否采用wait方法?不可以,wait必须配合synchronized使用,实现一把锁不可能采用另一把锁
  3. 死循环中采用yield线程让步+自旋?当竞争只有两个线程时可以采用此方法,但线程比较多,下一次轮到谁具有太多的不确定性,无法控制
  4. 死循环中采用sleep+自旋?sleep多久不确定
  5. 死循环中采用park+自旋?可!LockSupport.park,底层是unsafe类提供的native方法park,意思是让某个线程睡眠,需要使用unpark手动叫醒。

源码分析及AQS详解,请看我的另一篇文章:理解AQS(AbstractQueuedSynchronizer)源码分析_γìńɡ雄尐年ぐ的博客-CSDN博客java.util.concurrent.locks下public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {/** * The synchronization state. */ private volatile int state;}其继承的https://blog.csdn.net/qq_26012495/article/details/117936194

三、synchronized

具体可参考我的其余两篇文章 

【synchronized锁的膨胀过程】
synchronized锁的膨胀过程_γìńɡ雄尐年ぐ的博客-CSDN博客_synchronized膨胀过程在jdk1.6以前,随着并发数提高,synchronized吞吐量下降严重,而ReentrantLock则比较稳定,如果说ReentrantLock性能较强,那么synchronizedzeyuohttps://blog.csdn.net/qq_26012495/article/details/118071692?spm=1001.2014.3001.5501

【原子性、可见性、有序性】Java内存模型与线程_γìńɡ雄尐年ぐ的博客-CSDN博客由于内存和CPU的读写速度有断崖式差距,于是在内存与CPU之间增加了一层高速缓存,以缓解CPU想要数据时内存供不应求的尴尬场面。在CPU需要时将使用到的数据复制到缓存中,提高运算速度,运算结束后将结果同步...https://blog.csdn.net/qq_26012495/article/details/118035417?spm=1001.2014.3001.5501

四、常见问题

synchronized是重量级锁吗?

原来是,现在不全是。只有发生了OS调用的才是重量级锁,因为需要用户态到内核态的切换。用户态无法操作线程的阻塞及唤醒。

当年为什么会出现ReentrantLock?

Doug Lea,这哥们当年觉得synchronized太慢了,于是自己写了个锁,后来Sun公司觉得它很牛逼,就把它收录到了JDK里面(JDK1.5)。然后Sun觉得我自带的被你干下去了,我一定要超过你,然后在JDK1.6把synchronized进行了升级

ReentrantLock的park操作是否需要用户态切换到内核态?

需要

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值