锁专题(1)java 常见锁介绍,高级程序员必知必会

序言Java提供了种类丰富的锁 , 每种锁因其特性的不同 , 在适当的场景下能够展现出非常高的效率 。
本文旨在对锁相关源码(本文中的源码来自JDK 8)、使用场景进行举例 , 为读者介绍主流锁的知识点 , 以及不同的锁的适用场景 。
Java中往往是按照是否含有某一特性来定义锁 , 我们通过特性将锁进行分组归类 , 再使用对比的方式进行介绍 , 帮助大家更快捷的理解相关知识 。
下面给出本文内容的总体分类目录:
锁专题(1)java 常见锁介绍,高级程序员必知必会文章插图
输入图片说明
悲观锁、乐观锁乐观锁与悲观锁是一种广义上的概念 , 体现了看待线程同步的不同角度 。 在Java和数据库中都有此概念对应的实际应用 。
先说概念 。 对于同一个数据的并发操作 , 悲观锁认为自己在使用数据的时候一定有别的线程来修改数据 , 因此在获取数据的时候会先加锁 , 确保数据不会被别的线程修改 。 Java中 , synchronized关键字和Lock的实现类都是悲观锁 。
而乐观锁认为自己在使用数据时不会有别的线程修改数据 , 所以不会添加锁 , 只是在更新数据的时候去判断之前有没有别的线程更新了这个数据 。 如果这个数据没有被更新 , 当前线程将自己修改的数据成功写入 。 如果数据已经被其他线程更新 , 则根据不同的实现方式执行不同的操作(例如报错或者自动重试) 。
悲观锁(Pessimistic Lock)顾名思义 , 就是很悲观 , 每次去拿数据的时候都认为别人会修改 , 所以每次在拿数据的时候都会上锁 , 这样别人想拿这个数据就会 block 直到它拿到锁 。传统的关系型数据库里边就用到了很多这种锁机制 , 比如行锁 , 表锁等 , 读锁 , 写锁等 , 都是在做操作之前先上锁 。
乐观锁(Optimistic Lock)乐观锁在Java中是通过使用无锁编程来实现 , 最常采用的是CAS算法 , Java原子类中的递增操作就通过CAS自旋实现的 。
顾名思义 , 就是很乐观 , 每次去拿数据的时候都认为别人不会修改 , 所以不会上锁 , 但是在更新的时候会判断一下在此期间别人有没有去更新这个数据 , 可以使用版本号等机制 , 即对数据做版本控制 。乐观锁适用于多读的应用类型 , 这样可以提高吞吐量 , 像数据库如果提供类似于 write_condition 机制的其实都是提供的乐观锁 。
锁专题(1)java 常见锁介绍,高级程序员必知必会文章插图
输入图片说明
适合场景根据从上面的概念描述我们可以发现:
悲观锁适合写操作多的场景 , 先加锁可以保证写操作时数据正确 。
乐观锁适合读操作多的场景 , 不加锁的特点能够使其读操作的性能大幅提升 。
实际例子光说概念有些抽象 , 我们来看下乐观锁和悲观锁的调用方式示例:
//悲观锁的调用方式public synchronized void testMethod() (//操作同步资源)// ReentrantLock private ReentrantLock lock = new ReentrantLock();public void modifyPublicResources() (lock. lock();//操作同步资源lock.unlock();)// 乐观锁的调用方式private AtomicInteger atomicInteger = new AtomicInteger();//需要保证多个线程使用同一个AtomicIntegeratomicInteger.incrementAndGet();通过调用方式示例 , 我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源 , 而乐观锁则直接去操作同步资源 。
那么 , 为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?
我们通过介绍乐观锁的主要实现方式 “CAS” 的技术原理来为大家解惑 。
CAS 技术详解
自旋锁 VS 适应性自旋锁在介绍自旋锁前 , 我们需要介绍一些前提知识来帮助大家明白自旋锁的概念 。
阻塞阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成 , 这种状态转换需要耗费处理器时间 。 如果同步代码块中的内容过于简单 , 状态转换消耗的时间有可能比用户代码执行的时间还要长 。