一.前言

  最近项目遇到多线程并发的情景(并发抢单&恢复库存并行),代码在正常情况下运行没有什么问题,在高并发压测下会出现:库存超发/总库存与sku库存对不上等各种问题。

  在运用了 限流/加锁等方案后,问题得到解决。

  限流方案见本人另一篇博客:Guava-RateLimiter实现令牌桶限流

  加锁方案见下文。

二.乐观锁 & 悲观锁

  1.乐观锁

    顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号(version)等机制。

    例如:mybatis-plus 自带插件OptimisticLockerInterceptor,在数据库表加上一个version字段,每次更新完数据库mp会自动在version字段上加1,如果在更新提交的时候发现version字段的值与数据库中最新的值不一致,则提交失败。

  2.悲观锁

   悲观锁总是假设会出现最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

    传统的MySQL关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

    Java的同步synchronized关键字的实现就是典型的悲观锁。

  3.总结

    悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
   乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

三.独占锁 & 共享锁

  1.独占锁

    是指该锁一次只能被一个线程所持有。

    例如:Synchronized 和 ReentrantLock  ,它们同时也是悲观锁

   2.共享锁

    是指该锁可被多个线程所持有,允许多个线程同时去获取。

    例如:Semaphore 和 ReadWriteLock 和 countdownlatch,其读锁是共享锁,写锁是独享锁。

     3.总结

    读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
     独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

四.公平锁 & 非公平锁

  1.公平锁

   加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程,先来先得

  2.非公平锁

   线程加锁时直接尝试获取锁,获取不到就自动到队尾等待。

  3.总结

   更多的是直接使用非公平锁:非公平锁比公平锁性能高5-10倍,因为公平锁需要在多核情况下维护一个队列,如果当前线程不是队列的第一个无法获取锁,增加了线程切换次数。

五.java线程锁

  由于多个线程是共同占有所属进程的资源和地址空间的,那么就会存在一个问题:如果多个线程要同时访问某个资源,怎么处理?

  在Java并发编程中,经常遇到多个线程访问同一个共享资源 ,这时候作为开发者必须考虑如何维护数据一致性,这就是Java锁机制(同步问题)的来源。

  Java提供了多种多线程锁机制的实现方式,常见的有:

    •   synchronized
    •   ReentrantLock
    •   Semaphore
    •   AtomicInteger等

      每种机制都有优缺点与各自的适用场景,必须熟练掌握他们的特点才能在Java多线程应用开发时得心应手。

  1.Synchronized

  在Java中synchronized关键字被常用于维护数据一致性。synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是顺序的。

   Java开发人员都认识synchronized,使用它来实现多线程的同步操作是非常简单的,只要在需要同步的对方的方法、类或代码块中加入该关键字,它能够保证在同一个时刻最多只有一个线程执行同一个对象的同步代码,可保证修饰的代码在执行过程中不会被其他线程干扰。使用synchronized修饰的代码具有原子性和可见性,在需要进程同步的程序中使用的频率非常高,可以满足一般的进程同步要求。  

  •  synchronized (obj) {
    //方法
    …….
    }

    synchronized实现的机理依赖于软件层面上的JVM,因此其性能会随着Java版本的不断升级而提高。

  • 到了Java1.6,synchronized进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的Java1.7与1.8中,均对该关键字的实现机理做了优化。

  • 需要说明的是,当线程通过synchronized等待锁时是不能被Thread.interrupt()中断的,因此程序设计时必须检查确保合理,否则可能会造成线程死锁的尴尬境地。

  • 最后,尽管Java实现的锁机制有很多种,并且有些锁机制性能也比synchronized高,但还是强烈推荐在多线程应用程序中使用该关键字,因为实现方便,后续工作由JVM来完成,可靠性高。只有在确定锁机制是当前多线程程序的性能瓶颈时,才考虑使用其他机制,如ReentrantLock等。

  • 总结:在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好。

  • 2.ReentrantLock

  可重入锁,顾名思义,这个锁可以被线程多次重复进入进行获取操作。
  ReentantLock继承接口Lock并实现了接口中定义的方法,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的法。
  Lock实现的机理依赖于特殊的CPU指定,可以认为不受JVM的约束,并可以通过其他语言平台来完成底层的实现。在并发量较小的多线程应用程序中,ReentrantLock与synchronized性能相差无几,但在高并发量的条件下,synchronized性能会迅速下降几十倍,而ReentrantLock的性能却能依然维持一个水准。
  因此我们建议在高并发量情况下使用ReentrantLock。
  ReentrantLock引入两个概念:公平锁与非公平锁。
  公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁。反之,JVM按随机、就近原则分配锁的机制则称为不公平锁。
  ReentrantLock在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。这是因为,非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。
  ReentrantLock通过方法lock()与unlock()来进行加锁与解锁操作,与synchronized会被JVM自动解锁机制不同,ReentrantLock加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在finally控制块中进行解锁操作。通常使用方式如下所示:

  • /**
    * 初始化一个非公平锁(该锁只适用于单实例)
    */
    private static Lock lock = new ReentrantLock(false); void test() {
    try {
     lock.lock();
    //...执行业务逻辑
    }finally {
    lock.unlock();
    }
    }
  • 总结:在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍,而ReentrantLock确还能维持常态。高并发量情况下使用ReentrantLock。
  • 注意:Spring @Transactional注解和ReentrantLock同步锁同时使用不能同步的问题
  • 3.Semaphore (信号量)

  上述两种锁机制类型都是“互斥锁”,互斥是进程同步关系的一种特殊情况,相当于只存在一个临界资源,因此同时最多只能给一个线程提供服务。但是,在实际复杂的多线程应用程序中,可能存在多个临界资源,这时候我们可以借助Semaphore信号量来完成多个临界资源的访问。
  Semaphore基本能完成ReentrantLock的所有工作,使用方法也与之类似,通过acquire()与release()方法来获得和释放临界资源。
  经实测,Semaphone.acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()方法中断。
  此外,Semaphore也实现了可轮询的锁请求与定时锁的功能,除了方法名tryAcquire与tryLock不同,其使用方法与ReentrantLock几乎一致。Semaphore也提供了公平与非公平锁的机制,也可在构造函数中进行设定。
  Semaphore的锁释放操作也由手动进行,因此与ReentrantLock一样,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在finally代码块中完成

/** 定义一个非公平的共享锁 */
public static Semaphore LOCK = new Semaphore(5, false); void test() {
try{
//获取许可
LOCK.acquire();
}
finally {
//释放许可
LOCK.release();
}
}

  总结:Semaphore有着非常强大的功能,并且是共享锁,在特殊情景时非常有效。

Java多线程并发编程/锁的理解的更多相关文章

  1. Java 多线程并发编程一览笔录

    Java 多线程并发编程一览笔录 知识体系图: 1.线程是什么? 线程是进程中独立运行的子任务. 2.创建线程的方式 方式一:将类声明为 Thread 的子类.该子类应重写 Thread 类的 run ...

  2. Java多线程并发08——锁在Java中的应用

    前两篇文章中,为各位带来了,锁的类型及锁在Java中的实现.接下来本文将为各位带来锁在Java中的应用相关知识.关注我的公众号「Java面典」了解更多 Java 相关知识点. 锁在Java中主要应用还 ...

  3. Java多线程并发07——锁在Java中的实现

    上一篇文章中,我们已经介绍过了各种锁,让各位对锁有了一定的了解.接下来将为各位介绍锁在Java中的实现.关注我的公众号「Java面典」了解更多 Java 相关知识点. 在 Java 中主要通过使用sy ...

  4. Java基础系列篇:JAVA多线程 并发编程

    一:为什么要用多线程: 我相信所有的东西都是以实际使用价值而去学习的,没有实际价值的学习,学了没用,没用就不会学的好. 多线程也是一样,以前学习java并没有觉得多线程有多了不起,不用多线程我一样可以 ...

  5. 【收藏】Java多线程/并发编程大合集

    (一).[Java并发编程]并发编程大合集-兰亭风雨    [Java并发编程]实现多线程的两种方法    [Java并发编程]线程的中断    [Java并发编程]正确挂起.恢复.终止线程    [ ...

  6. Java多线程并发编程一览笔录

    线程是什么? 线程是进程中独立运行的子任务. 创建线程的方式 方式一:将类声明为 Thread 的子类.该子类应重写 Thread 类的 run 方法 方式二:声明实现 Runnable 接口的类.该 ...

  7. Java 多线程并发编程面试笔录一览

    知识体系图: 1.线程是什么? 线程是进程中独立运行的子任务. 2.创建线程的方式 方式一:将类声明为 Thread 的子类.该子类应重写 Thread 类的 run 方法 方式二:声明实现 Runn ...

  8. Java 多线程并发编程

    导读 创作不易,禁止转载! 并发编程简介 发展历程 早起计算机,从头到尾执行一个程序,这样就严重造成资源的浪费.然后操作系统就出现了,计算机能运行多个程序,不同的程序在不同的单独的进程中运行,一个进程 ...

  9. java多线程并发编程与CPU时钟分配小议

    我们先来研究下JAVA的多线程的并发编程和CPU时钟振荡的关系吧 老规矩,先科普 我们的操作系统在DOS以前都是单任务的 什么是单任务呢?就是一次只能做一件事 你复制文件的时候,就不能重命名了 那么现 ...

随机推荐

  1. 使用timeout-decorator为python函数任务设置超时时间

    需求背景 在python代码的实现中,假如我们有一个需要执行时间跨度非常大的for循环,如果在中间的某处我们需要定时停止这个函数,而不停止整个程序.那么初步的就可以想到两种方案:第一种方案是我们先预估 ...

  2. 闲聊CAP、BASE与XA

    CAP理论与BASE理论 首先要和大家说的就是大名鼎鼎的CAP理论与BASE理论了,这两个理论与解决分布式事务问题是密切相关的. 其实网上有很多关于CAP与BASE相关的文章,一写就写了一大堆,篇幅很 ...

  3. 从ReentrantLock实现非公平锁的源码理解AQS中的CLH队列

    虽然前面也看过AQS的文章,并且转载过一篇大佬的分析,但是我觉得他们对于AQS和ReentrantLock部分的源码的分析并不详细,自己理解期来还是有问题,于是自己准备花时间重新梳理下,好了,进入正题 ...

  4. CSS奇思妙想 -- 使用 background 创造各种美妙的背景

    本文属于 CSS 绘图技巧其中一篇,系列文章: 在 CSS 中使用三角函数绘制曲线图形及展示动画 CSS奇思妙想 -- 使用 CSS 创造艺术 将介绍一些利用 CSS 中的 background.mi ...

  5. CSS开发过程中的20个快速提升技巧

    摘要:本文涵盖了20个CSS技巧,可以解决许多工作中常见的问题, 让你也成为一个CSS高手. 1.使用CSS重置(reset) css重置库如normalize.css已经被使用很多年了,它们可以为你 ...

  6. linux(9)find命令详解

    find命令格式: find path -option [ -print ] [ -exec -ok command ] {} \; find命令的参数: path:要查找的目录路径. ~ 表示$HO ...

  7. A - 最长回文(马拉车算法//manacher)

    给出一个只由小写英文字符a,b,c...y,z组成的字符串S,求S中最长回文串的长度.回文就是正反读都是一样的字符串,如aba, abba等 Input输入有多组case,不超过120组,每组输入为一 ...

  8. Codeforces Round #670 (Div. 2) B. Maximum Product (暴力)

    题意:有一长度为\(n\)的序列,求其中任意五个元素乘积的最大值. 题解:先排序,然后乘积能是正数就搞正数,模拟一下就好了. 代码: int t; ll n; ll a[N]; int main() ...

  9. Java基础(第二期)

    数据类型扩展以及面试题讲解 整数拓展:进制 int i=10; int i2=010; //八进制0 int i3=0x10; //十六进制0x 0~9 A~F 16 相关进制转换自行学习,用的不多 ...

  10. C# EventWaitHandle类解析

    EventWaitHandle 类用于在异步操作时控制线程间的同步,即控制一个或多个线程继行或者等待其他线程完成. 构造函数 EventWaitHandle(bool initialState, Ev ...