JAVA锁详解——(CAS、PCC、AQS、CLH、Synchronized、Lock、公平/非公平锁、锁粗化/消除、分段锁)

锁的常缩写用名词简介

CAS(Compare And Swap):

乐观锁 并发策略先进行对数据的操作,如果没有发现其它线程也操作了数据,那么就认为这个操作是成功的。如果发生了其它线程也操作了数据,那么一般采取不断重试的手段,直到成功或最大重试次数为止,这种乐观锁的策略,不需要把线程阻塞,属于 非阻塞同步 的一种手段。

PCC(Pessimistic Concurrency Control):

悲观锁 在整个数据处理过程中,将数据处于锁定状态。禁止其他线程、事务进行修改。典型如数据库中的行锁,Java中的synchronized。

AQS(AbstractQueuedSynchronizer):

抽象队列同步器。AQS就是基于 CLH 队列,用一个state标记位,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。支持 独占共享 两种方式。
AQS里面的CLH队列是CLH同步锁的一种变形。其主要从两方面进行了改造:节点的结构节点等待机制

  • 节点结构上引入了头结点和尾节点,他们分别指向队列的头和尾,尝试获取锁、入队列、释放锁等实现都与头尾节点相关,并且每个节点都引入前驱节点和后后续节点的引用(双向链表)
  • 等待机制由原来的自旋改成 自旋 + 阻塞 + 唤醒

ReentrantLock就是基于 AQS 实现的

CLH(Craig,Landin and Hagersten)发明人名字:
  1. 当线程获取锁失败后入队尾,指针指向自己的前一个节点
  2. 线程自旋判断前驱节点为头节点则尝试获取锁,成功则将自己设置为头结点。失败则判断前驱节点waitStatus是否为-1。如果是就进入阻塞状态等待被唤醒,否则修改waitStatus为-1等待下一次自旋。(如前驱节点不是头结点则直接进入判断waitStatus是否为-1的流程)
  3. 释放锁的时候判断自身的waitStatus如果 != 0 则说明后继节点正在阻塞,等待被唤醒,所以唤醒后继线程(有效的避免了 惊群效应 )。

Synchronized详解

synchronized是什么

synchronized是Java提供的一个并发控制的关键字JVM层面),作用于 对象 上。主要有三种用法:

  • 非静态方法:锁作用于访问的对象上
  • 静态方法:锁作用于方法所在的类对象class
  • 代码块:锁作用于括号中指定的对象
synchronized的历史

JDK1.6以前:synchronized 那时还属于重量级锁。
JDK1.6及以后:synchronized 引入锁升级策略(只能升级不能降级),依次为 无锁偏向锁轻量级锁重量级锁 。(因为大多数时间都不会发生多个线程同时竞争锁的情况,每次线程都加锁解锁,每次这么搞都要操作系统在用户态和内核态之间来回切,太耗性能了。)

偏向锁、轻量级锁、重量级锁优缺点对比
      锁      优点缺点使用场景
偏向锁加锁解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗使用于基本只有一个线程访问同步块的场景
轻量级锁多个线程竞争不会阻塞,提高程序响应速度如果始终得不到锁,线程自旋会消耗CPU追求响应时间,同步代码块执行耗时短
重量级锁多个线程竞争不自旋,节省CPU消耗线程阻塞,需要唤醒,相对耗时较长追求吞吐量,同步代码块执行耗时较长
  • 偏向锁
    • 对象头 信息 Mark Word 里存储锁偏向的 线程ID ,同一个线程会自动获取锁。
    • 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。
    • 如果确定同步方法会被高并发的访问,建议通过-XX:-UseBiasedLocking 参数关闭偏向锁
  • 轻量级锁
    • 当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过 自旋 的形式尝试获取锁,不会阻塞,从而提高性能。
    • 若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
  • 重量级锁
    • 此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态等待被唤醒。通过对象内部的监视器(Monitor)实现。
    • Monitor对象重要属性说明(并非所有的属性):
      • _owner:指向持有ObjectMonitor对象的线程,当线程释放monitor时,_owner又恢复为NULL。
      • _WaitSet:存放处于wait状态的线程队列,因为调用wait方法而被阻塞的线程会被放在该队列中
      • _EntryList:存放处于等待锁block状态的线程队列
      • _recursions:锁的重入次数
      • _count:用来记录该线程获取锁的次数(当_count = 0、_recursions = 0 则释放锁,唤醒EntryList中的线程)

既然聊到了这里咱在了解下对象头。
对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)
Mark Word:默认存储对象的HashCode、分代年龄、GC次数和锁标志位信息。
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
对象头信息

锁升级流程

Synchronized锁升级流程

Lock对比Synchronized

对比项SynchronizedLock
存在层次JAVA关键字,JVM层面是一个接口
锁释放1. 获取锁的线程执行完同步代码,释放锁
2. 程执行发生异常,jvm会让线程释放锁
调用unlock()方法释放锁
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待Lock有多种获取锁的方式,如lock、tryLock
锁状态无法判断可判断
可避免死锁:tryLock(long time, TimeUnit unit)
锁类型可重入
非公平
不可中断
可重入
可公平/非公平
可中断:lockInterruptibly()

公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

锁粗化

原则上,我们在编写同步块的时候,同步块的范围应当尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能小,如果存在锁竞争,那等待锁的线程也能尽快的拿到锁。
大部分情况下,这种原则是正确的,但是如果一系列的连续操作都需要对同一个对象进行加锁和解锁,甚至加锁操作时出现在循环体中,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不需要的性能损耗。这时JIT编译器就会把锁同步的范围扩展(粗化)。
例如我们有如下代码 ↓

for(int i=0;i<size;i++){
    synchronized(lock){
    ...
    }
}

锁粗化后的代码如下 ↓

synchronized(lock){
    for(int i=0;i<size;i++){
    ...
    }
}

锁消除

锁消除是发生在编译器级别的一种锁优化方式。将检测到不可能存在共享数据竞争的锁进行削除。
例如我们有如下代码 ↓

public void method() {
        Object object = new Object();
        synchronized (object) {
            System.out.println("Hello world");
        }
    }

object 本身就是局部变量,方法的的局部变量是线程独立的,并发的场景每个线程都有各自的object对象,这个时候的锁就无意义的。

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁。
典型如 ConcurrentHashMap ,其并发的实现就是通过分段锁的形式来实现高效的并发操作。ConcurrentHashMap中的分段锁称为 Segment ,它即类似于HashMap的结构,即 内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)
当需要put元素的时候,并不是对整个Hashmap进行加锁,而是先通过 HashCode 来知道他要放在那一个分段中,然后 对这个分段进行加锁 ,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在 统计size 的时候,可就是获取Hashmap全局信息的时候,就需要 获取所有的分段锁 才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhibo_lv

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值