如何根据不同的业务场景,来选择合适的锁?

简介: 锁是有效的解决并发情况下保证临界资源操作原子性的有效手段之一。下面我就从我们几个开发使用的角度来说我们常用的锁。

锁可以解决什么问题?


锁可以解决并行执行任务执行过程中对,共享数据顺序访问、修改的场景。比如对同一个账户进行并行扣款或者转账。下面我们展开讨论下 synchronized 、ReetranLock 以及他们的使用。


synchronized


synchronized 是 JDK 提供的内置锁, 由 JVM 虚拟机内部实现,是基于 monitor 机制, 在 JDK 1.6 之后被优化,会有一个锁升级的过程,将锁的状态存储到对象头中。


锁升级过程,默认是无锁状态,首先会进行判断,如果是没有字段竞争的情况下会使用偏向锁,偏向锁的本质就是将当前获得锁的线程 id 设置到共享数据的对象头中。


然后升级为轻量级锁,轻量级锁的本质是通过 CAS 来修改 MarkWord 来实现的。


最后再升级为重量级锁,我们可以通过操作系统的 monitor 依赖操作系统的 MutexLock(互斥锁)来实现的 。


四种使用方式


  1. 在静态方法上使用


  1. 在普通方法上使用


  1. 锁定 this 状态


  1. 锁定静态类


加锁状态记录位置


对象加锁,记录在对象头中,对象头入下图所示。


image.png


在运行期间,Mark Word里面存储的数据会随着锁标志位的变化而变化。Mark Word可能变为存储以下4种数据,如下图所示


image.png


锁的膨胀和升级


锁的升级和膨胀时候不可逆转的。


image.png


使用场景


JDK 在并发包中, 使用 synchroinzed 的地方有:


  1. ConcurrentHashMap (jdk 1.8)


  1. HashTable


ReetrantLock


ReetrantLock 开发作者是 Doug Lea ,从 JDK1.5 开始过后加入 JDK 的锁,主要是通过 QAS 的方式来实现的, 通过 Unsafe 包提供的 CAS 操作来进行锁状态(state)的竞争。然后通过 LockSupport.park(this). 进行 park 住线程,如果在 AQS 队列头的对象进行唤醒执行 unpack 方法,然后让他去竞争锁。


ReetrantLock 还分为公平锁和非公平锁,默认是非公平锁。 因为公平锁,是需要保证竞争者按照获取锁的顺序进行获得,性能略低于非公平锁。


AQS 队列结构如下所示,它的本质是一个 FIFO 的线程安全的同步队列,如下图所示:


image.png


ReetrantLock 加锁和解锁的过程入下图所示:


image.png


使用方式


ReetrantLock 的使用方式如下,主要是有三个步骤:创建、加锁、解锁。


class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...
   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }


使用场景


JDK 在并发包中, 使用 ReetrantLock 的地方有:


  1. CyclicBarrier


  1. DelayQueue


  1. LinkedBlockingDeque


  1. ThreadPoolExecutor


  1. ReentrantReadWriteLock


  1. StampedLock


上面我只是列举了一步分,对于 ReetrantLock 来看可以说是并发包中非常基础的类,也是我们学习并发的基础,在后续的文章中我会给展开做更加深入的分析。


如何选择锁?


  1. 对于单机环境我们在 JDK 内进行并发控制我们可以使用 synchronized (内置锁) 和 RentrantLock 。


  1. 对于自增或者原子数据累计我们可以使用 Unsafe 提供的原子类,比如 AtomicInteger , AtomicLong


  1. 对于数据库的话,对于用户金额扣除的场景我们可以使用乐观锁的方式来进行控制,SQL 如下


update table_name set amount = 100, 
                      version = version + 1 where id = 1 and version = 1;


  1. 对于分布式场景下我们需要保证一致性,可以使用 Redis 或者 Zk 实现分布式锁。来进行分布式场景下的并发控制。


参考信息


  • 《深入理解 Java 虚拟机》周志明



相关文章
|
3月前
|
算法 Java 编译器
多线程线程安全问题之系统层面的锁优化有哪些常见的策略
多线程线程安全问题之系统层面的锁优化有哪些常见的策略
|
3月前
|
Java
通用快照方案问题之调整Hystrix的信号量隔离模式的并发限制如何解决
通用快照方案问题之调整Hystrix的信号量隔离模式的并发限制如何解决
24 0
|
3月前
|
存储 缓存 NoSQL
架构设计篇问题之在数据割接过程中,多线程处理会导致数据错乱和重复问题如何解决
架构设计篇问题之在数据割接过程中,多线程处理会导致数据错乱和重复问题如何解决
|
3月前
|
存储 设计模式 监控
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
40 0
|
5月前
|
分布式计算 Java 数据库连接
回答粉丝疑问:Spark为什么调优需要降低过多小任务,降低单条记录的资源开销?
回答粉丝疑问:Spark为什么调优需要降低过多小任务,降低单条记录的资源开销?
55 1
|
5月前
|
SQL 安全 算法
在高并发情况下,如何做到安全的修改同一行数据?
在高并发情况下,如何做到安全的修改同一行数据?
127 0
|
Java 编译器 调度
锁的优化过程
锁的优化过程
|
缓存 监控 算法
内容服务锁优化实践
内容服务锁优化实践
86 0
|
并行计算 安全 算法
Oh!老伙计,提高自己的并发技能,先从锁优化开始吧
锁是最常用的同步方法之一。在高并发的环境下,激烈的锁竞争会导致程序的性能下降。 对于单任务或者单线程的应用而言,其主要资源消耗都花在任务本身,它既不需要维护并行数据结构间的一致性状态,也不需要为线程的切换和调度花费时间。对于多线程应用来说,系统除了处理功能需求外,还需要额外维护多线程环境的特有信息,如线程本身的元数据、线程的调度、线程上下文的切换等。并行计算之所以能提高系统的性能,并不是因为它"少干活"了,而是因为并行计算可以更合理地进行任务调度,充分利用各个CPU资源。
|
Kubernetes 负载均衡 算法
异步任务处理系统,如何解决业务长耗时、高并发难题?
阿里云函数计算 FC 为用户提供了开箱即用的,接近于Level ß3能力的异步任务处理服务。用户只需要创建任务处理函数,通过控制台,命令行工具,API/SDK,事件触发等多种方式提交任务,就可以弹性、可靠、可观测完备的方式处理任务。
异步任务处理系统,如何解决业务长耗时、高并发难题?