Java中各类锁的原理解析

2021年02月24日 阅读数:11
这篇文章主要向大家介绍Java中各类锁的原理解析,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

下图汇总了各类和其适用条

1. 乐观锁 VS 悲观锁

        对于同一个数据的并发操做,悲观锁认为本身在使用数据的时候必定有别的线程来修改数据,所以在获取数据的时候会先加锁,确保数据不会被别的线程修改。html

       Java中,synchronized关键字和Lock的实现类都是悲观锁。java

       而乐观锁认为本身在使用数据时不会有别的线程修改数据,因此不会添加锁!数据结构

乐观锁和悲观锁的调用方式示例:并发

/ ------------------------- 悲观锁的调用方式 -------------------------
// 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 适应性自旋锁

       在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。若是物理机器有多个处理器,可以让两个或以上的线程同时并行执行,咱们就可让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。工具

       而为了让当前线程“稍等一下”,咱们需让当前线程进行自旋,若是在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就能够没必要阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。性能

      自旋锁自己是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。若是锁被占用的时间很短,自旋等待的效果就会很是好。反之,若是锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。因此,自旋等待的时间必需要有必定的限度,若是自旋超过了限定次数(默认是10次,可使用-XX:PreBlockSpin来更改)没有成功得到锁,就应当挂起线程。atom

      自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,而且引入了自适应的自旋锁(适应性自旋锁)spa

      自适应意味着自旋的时间(次数)再也不固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。若是在同一个锁对象上,自旋等待刚刚成功得到过锁,而且持有锁的线程正在运行中,那么虚拟机就会认为此次自旋也是颇有可能再次成功,进而它将容许自旋等待持续相对更长的时间。若是对于某个锁,自旋不多成功得到过,那在之后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。操作系统

在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock线程

3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

这四种锁是指锁的状态,专门针对synchronized的。在介绍这四种锁状态以前还须要介绍一些额外的知识。

首先为何Synchronized能实现线程同步?

在回答这个问题以前咱们须要了解两个重要的概念:“Java对象头”、“Monitor”。

Java对象头

synchronized是悲观锁,在操做同步资源以前须要给同步资源先加锁,这把锁就是存在Java对象头里的,而Java对象头又是什么呢?

咱们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,因此Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽可能多的数据。它会根据对象的状态复用本身的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例。

Monitor

Monitor能够理解为一个同步工具或一种同步机制,一般被描述为一个对象。每个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁

Monitor是线程私有的数据结构,每个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的惟一标识,表示该锁被这个线程占用。

如今话题回到synchronized,synchronized经过Monitor来实现线程同步,Monitor是依赖于底层的操做系统的Mutex Lock(互斥锁)来实现的线程同步。

如同咱们在自旋锁中提到的“阻塞或唤醒一个Java线程须要操做系统切换CPU状态来完成,这种状态转换须要耗费处理器时间。若是同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6以前synchronized效率低的缘由。这种依赖于操做系统Mutex Lock所实现的锁咱们称之为“重量级锁”,JDK 6中为了减小得到锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

因此目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。

经过上面的介绍,咱们对synchronized的加锁机制以及相关知识有了一个了解,那么下面咱们给出四种锁状态对应的的Mark Word内容,而后再分别讲解四种锁状态的思路以及特色:

 

锁状态 存储内容 存储内容
无锁 对象的hashCode、对象分代年龄、是不是偏向锁(0) 01
偏向锁 偏向线程ID、偏向时间戳、对象分代年龄、是不是偏向锁(1) 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10

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()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且没法释放。因此此时会出现死锁。

6. 独享锁(排它锁) VS 共享锁

       独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。若是线程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的并发性相比通常的互斥锁有了很大提高。

 

参考文章:https://tech.meituan.com/2018/11/15/java-lock.html