前言Java提供了种类丰富的锁每种锁因其特性的不同在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码本文中的源码来自JDK 8和Netty 3.10.6、使用场景进行举例为读者介绍主流锁的知识点以及不同的锁的适用场景。Java中往往是按照是否含有某一特性来定义锁我们通过特性将锁进行分组归类再使用对比的方式进行介绍帮助大家更快捷的理解相关知识。下面给出本文内容的总体分类目录1. 乐观锁 VS 悲观锁乐观锁与悲观锁是一种广义上的概念体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。先说概念。对于同一个数据的并发操作悲观锁认为自己在使用数据的时候一定有别的线程来修改数据因此在获取数据的时候会先加锁确保数据不会被别的线程修改。Java中synchronized关键字和Lock的实现类都是悲观锁。而乐观锁认为自己在使用数据时不会有别的线程修改数据所以不会添加锁只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新则根据不同的实现方式执行不同的操作例如报错或者自动重试。乐观锁在Java中是通过使用无锁编程来实现最常采用的是CAS算法Java原子类中的递增操作就通过CAS自旋实现的。根据从上面的概念描述我们可以发现悲观锁适合写操作多的场景先加锁可以保证写操作时数据正确。乐观锁适合读操作多的场景不加锁的特点能够使其读操作的性能大幅提升。光说概念有些抽象我们来看下乐观锁和悲观锁的调用方式示例// ------------------------- 悲观锁的调用方式 ------------------------- // synchronized public synchronized void testMethod() { // 操作同步资源 } // ReentrantLock private ReentrantLock lock new ReentrantLock(); // 需要保证多个线程使用的是同一个锁 public void modifyPublicResources() { lock.lock(); // 操作同步资源 lock.unlock(); } // ------------------------- 乐观锁的调用方式 ------------------------- private AtomicInteger atomicInteger new AtomicInteger(); // 需要保证多个线程使用的是同一个AtomicInteger atomicInteger.incrementAndGet(); //执行自增1通过调用方式示例我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源而乐观锁则直接去操作同步资源。2. 自旋锁 VS 适应性自旋锁在介绍自旋锁前我们需要介绍一些前提知识来帮助大家明白自旋锁的概念。阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单状态转换消耗的时间有可能比用户代码执行的时间还要长。在许多场景中同步资源的锁定时间很短为了这一小段时间去切换线程线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器能够让两个或以上的线程同时并行执行我们就可以让后面那个请求锁的线程不放弃CPU的执行时间看看持有锁的线程是否很快就会释放锁。而为了让当前线程稍等一下我们需让当前线程进行自旋如果在自旋完成后前面锁定同步资源的线程已经释放了锁那么当前线程就可以不必阻塞而是直接获取同步资源从而避免切换线程的开销。这就是自旋锁。自旋锁本身是有缺点的它不能代替阻塞。自旋等待虽然避免了线程切换的开销但它要占用处理器时间。如果锁被占用的时间很短自旋等待的效果就会非常好。反之如果锁被占用的时间很长那么自旋的线程只会白浪费处理器资源。所以自旋等待的时间必须要有一定的限度如果自旋超过了限定次数默认是10次可以使用-XX:PreBlockSpin来更改没有成功获得锁就应当挂起线程。自旋锁的实现原理同样也是CASAtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作如果修改数值失败则通过循环来执行自旋直至修改成功。3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁这四种锁是指锁的状态专门针对synchronized的。在介绍这四种锁状态之前还需要介绍一些额外的知识。总结而言 偏向锁通过对比Mark Word解决加锁问题避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。4. 公平锁 VS 非公平锁公平锁是指多个线程按照申请锁的顺序来获取锁线程直接进入队列中排队队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低等待队列中除第一个线程以外的所有线程都会阻塞CPU唤醒阻塞线程的开销比非公平锁大。非公平锁是多个线程加锁时直接尝试获取锁获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用那么这个线程可以无需阻塞直接获取到锁所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销整体的吞吐效率高因为线程有几率不阻塞直接获得锁CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死或者等很久才会获得锁。直接用语言描述可能有点抽象这里作者用从别处看到的一个例子来讲述一下公平锁和非公平锁。如上图所示假设有一口水井有管理员看守管理员有一把锁只有拿到锁的人才能够打水打完水要把锁还给管理员。每个过来打水的人都要管理员的允许并拿到锁之后才能去打水如果前面有人正在打水那么这个想要打水的人就必须排队。管理员会查看下一个要去打水的人是不是队伍里排最前面的人如果是的话才会给你锁让你去打水如果你不是排第一的人就必须去队尾排队这就是公平锁。但是对于非公平锁管理员对打水的人没有要求。即使等待队伍里有排队等待的人但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时刚好来了一个插队的人这个插队的人是可以直接从管理员那里拿到锁去打水不需要排队原本排队等待的人只能继续等待。如下图所示5. 可重入锁 VS 非可重入锁可重入锁又名递归锁是指在同一个线程在外层方法获取锁的时候再进入该线程的内层方法会自动获取锁前提锁对象得是同一个对象或者class不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析public class Widget { public synchronized void doSomething() { System.out.println(方法1执行...); doOthers(); } public synchronized void doOthers() { System.out.println(方法2执行...); } }在上面的代码中类中的两个方法都是被内置锁synchronized修饰的doSomething()方法中调用doOthers()方法。因为内置锁是可重入的所以同一个线程在调用doOthers()时可以直接获得当前对象的锁进入doOthers()进行操作。如果是一个不可重入锁那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉实际上该对象锁已被当前线程所持有且无法释放。所以此时会出现死锁。而为什么可重入锁就可以在嵌套调用时可以自动获得锁呢我们通过图示和源码来分别解析一下。还是打水的例子有多个人在排队打水此时管理员允许锁和同一个人的多个水桶绑定。这个人用多个水桶打水时第一个水桶和锁绑定并打完水之后第二个水桶也可以直接和锁绑定并开始打水所有的水桶都打完水之后打水人才会将锁还给管理员。这个人的所有打水流程都能够成功执行后续等待的人也能够打到水。这就是可重入锁。但如果是非可重入锁的话此时管理员只允许锁和同一个人的一个水桶绑定。第一个水桶和锁绑定打完水之后并不会释放锁导致第二个水桶不能和锁绑定也无法打水。当前线程出现死锁整个等待队列中的所有线程都无法被唤醒。之前我们说过ReentrantLock和synchronized都是重入锁那么我们通过重入锁ReentrantLock以及非可重入锁NonReentrantLock的源码来对比分析一下为什么非可重入锁在重复调用同步资源时会出现死锁。首先ReentrantLock和NonReentrantLock都继承父类AQS其父类AQS中维护了一个同步状态status来计数重入次数status初始值为0。当线程尝试获取锁时可重入锁先尝试获取并更新status值如果status 0表示没有其他线程在执行同步代码则把status置为1当前线程开始执行。如果status ! 0则判断当前线程是否是获取到这个锁的线程如果是的话执行status1且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值如果status ! 0的话会导致其获取锁失败当前线程阻塞。释放锁时可重入锁同样先获取当前status的值在当前线程是持有锁的线程的前提下。如果status-1 0则表示当前线程所有重复获取锁的操作都已经执行完毕然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后直接将status置为0将锁释放。6. 独享锁(排他锁) VS 共享锁独享锁和共享锁同样是一种概念。我们先介绍一下具体的概念然后通过ReentrantLock和ReentrantReadWriteLock的源码来介绍独享锁和共享锁。独享锁也叫排他锁是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后则其他线程只能对A再加共享锁不能加排它锁。获得共享锁的线程只能读数据不能修改数据。独享锁与共享锁也是通过AQS来实现的通过实现不同的方法来实现独享或者共享。下图为ReentrantReadWriteLock的部分源码我们看到ReentrantReadWriteLock有两把锁ReadLock和WriteLock由词知意一个读锁一个写锁合称读写锁。再进一步观察可以发现ReadLock和WriteLock是靠内部类Sync实现的锁。Sync是AQS的一个子类这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。在ReentrantReadWriteLock里面读锁和写锁的锁主体都是Sync但读锁和写锁的加锁方式不一样。读锁是共享锁写锁是独享锁。读锁的共享锁可保证并发读非常高效而读写、写读、写写的过程互斥因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。结语本文Java中常用的锁以及常见的锁的概念进行了基本介绍并从源码以及实际应用的角度进行了对比分析。限于篇幅以及个人水平没有在本篇文章中对所有内容进行深层次的讲解。其实Java本身已经对锁本身进行了良好的封装降低了研发同学在平时工作中的使用难度。但是研发同学也需要熟悉锁的底层原理不同场景下选择最适合的锁。而且源码中的思路都是非常好的思路也是值得大家去学习和借鉴的。 福利时间如果你正在备战面试或者想要学习其他知识给大家推荐一个宝藏知识库作者整理了一些列 Java 程序员需要掌握的核心知识有需要的自取不谢。知识库地址https://farerboy.com/