Java 中的锁机制

synchronized介绍

synchronized关键字是Java加锁的一种方式,用于修饰代码块和方法(静态方法和普通方法),根据修饰范围的不同,可以分为类锁和对象锁。同时,synchronized也是一种可重入锁

synchronized实现原理

synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 JVM 处理字节码会出现相关指令。

jvm 基于进入和退出 Monitor 对象来实现方法同步和代码块同步。

1.代码块的同步

利用 monitorenter 和 monitorexit 这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当 jvm 执行到 monitorenter 指令时,当前线程试图获取 monitor 对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器 + 1;当执行 monitorexit 指令时,锁计数器 - 1;当锁计数器为 0 时,该锁就被释放了。如果获取 monitor 对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

2. 方法级的同步

是隐式的,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM 可以从方法常量池中的方法表结构 (method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有 monitor(虚拟机规范中用的是管程一词), 然后再执行方法最后再方法完成 (无论是正常完成还是非正常完成) 时释放 monitor

synchronized优化

synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高。

Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了 “偏向锁” 和 “轻量级锁”:锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。这个过程又叫做锁膨胀

LOCK

Lock 有三个实现类,一个是 ReentrantLock, 另两个是 ReentrantReadWriteLock 类中的两个静态内部类 ReadLock 和 WriteLock。

public interface Lock {//lock 相关代码
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

LOCK 的实现类其实都是构建在 AbstractQueued-Synchronizer 上,为何图中没有用 UML 线表示呢,这是每个 Lock 实现类都持有自己内部类 Sync 的实例,而这个 Sync 就是继承 AbstractQueuedSynchronizer (AQS)。为何要实现不同的 Sync 呢?这和每种 Lock 用途相关。

FairSync 与 NonfairSync 的区别在于,是不是保证获取锁的公平性,因为默认是 NonfairSync(非公平性)

可以看到Lock锁的**底层实现是AQS,**那么说明是AQS呢?

AQS

1.定义

AQS(AbstractQuenedSynchronizer ),抽象的队列式同步器,除了 java 自带的 synchronized 关键字之外的锁机制。

2.核心思想

被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁(CLH 锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性)实现的,即将暂时获取不到锁的线程加入到队列中。

AQS 是将每一条请求共享资源的线程封装成一个 CLH 锁队列(该队列是一个双向链表,没有实现)的一个结点(Node),来实现锁的分配。

3.实现

AQS 基于 CLH 队列,用 volatile 修饰共享变量 state,线程通过 CAS 去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

4.根据AQS,得出Lock的实现过程

  • lock 的存储结构:一个 int 类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)

  • lock 获取锁的过程:本质上是通过 CAS 来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。

  • lock 释放锁的过程:修改状态值,调整等待链表。

    可以看到在整个实现过程中,lock 大量使用 CAS + 自旋。因此根据 CAS 特性,lock 建议使用在低锁冲突的情况下。目前 java1.6 以后,官方对 synchronized 做了大量的锁优化(偏向锁、自旋、轻量级锁)。因此在非必要的情况下,建议使用 synchronized 做同步操作。

atomic 包底层实现原理

Atomic 包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

Atomic 系列的类中的核心方法都会调用 unsafe 类中的几个本地方法。因此atomic证原子性就是通过**:自旋 + CAS(乐观锁)**

仔细分析 concurrent 包的源代码(lock和atomic均在这个包下) 实现,会发现一个通用化的实现模式:

  • 首先,声明共享变量为 volatile

  • 然后,使用 CAS 的原子条件更新来实现线程之间的同步;

  • 同时,配合以 volatile 的读 / 写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信

    可以看出,Lock类和Atomic包底层实现都是通过 CAS+自旋的方式解决多线程同步问题。那这二者有什么区别呢?

    Atomic在竞争激烈时能维持常态,比 lock 性能好,但是只能同步一个变量。

volatile关键字

Java 因为指令重排序,优化我们的代码,让程序运行更快,也随之带来了多线程下,指令执行顺序的不可控。

volatile关键字的作用:

  • 内存可见性,修饰的变量发生改变之后对所有线程立即可见

  • 禁止指令重排序

    volatile的底层是通过内存屏障实现的,第一个作用是禁止指令重排。内存屏障另一个作用是强制更新一次不同 CPU 的缓存

    synchronized 看作重量级的锁,而 volatile 看作轻量级的锁synchronized 使用的锁的层面是在JVM层面,虚拟机处理字节码文件实现相关指令。volatile 底层使用多核处理器实现的 lock 指令,更底层,消耗代价更小。

CAS

CAS 的全称是 Compare-And-Swap , 它是一条 CPU 并发原语

CAS 并不是一种实际的锁,它仅仅是实现乐观锁的一种思想,java 中的乐观锁(如自旋锁)基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

乐观锁一般会使用版本号机制CAS 算法实现

1.版本号机制

一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

2.CAS 算法

compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步所以也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操作数

  • 需要读写的内存值 V

  • 进行比较的值 A

  • 写入的新值 B

    当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

    3.CAS在Java中的实现

CAS 并发原语现在 Java 语言中就是 Unsafe 类的各个方法,调用 Unsafe 类中的 CAS 方法,JVM 会帮我们实现 CAS 汇编指令,这是一种完全依赖硬件的功能,通过它实现了原子操作,由于 CAS 是一种系统原语,原语属于操作系统用语范畴,是由于诺干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS 是一条 CPU 原子指令,不会造成所谓的数据不一致问题。

4.synchronized与CAS的比较

synchronized涉及线程之间的切换,存在用户状态和内核状态的切换,耗费巨大。CAS只是CPU的一条原语,是一个原子操作,消耗较少。

CAS 相对于其他锁,不会进行内核态操作,有着一些性能的提升。但同时引入自旋,当锁竞争较大的时候,自旋次数会增多。cpu 资源会消耗很高。CAS + 自旋适合使用在低并发有同步数据的应用场景。

synchronized 与Lock的区别

1.实现层面不一样。synchronized 是 Java 关键字JVM 层面 实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁,(但是Lock的底层CAS乐观锁比synchronized更底层,是CPU原语,属于操作系统层面的)

2.是否自动释放锁synchronized 在线程代码执行完或出现异常时自动释放锁Lock 不会自动释放锁,需要再 finally {} 代码块显式地中释放锁

3.是否一直等待synchronized 会导致线程拿不到锁一直等待Lock 可以设置尝试获取锁或者获取锁失败一定时间超时。

4.获取锁成功是否可知synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功

5.功能复杂性。synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可判断、可公平和不公平、细分读写锁提高效率。

©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页