详解JUC并发编程之锁

目录
  • 1、自旋锁和自适应锁
  • 2、轻量级锁和重量级锁
    • 轻量级锁加锁过程
    • 轻量级锁解锁过程
  • 3、偏向锁
  • 4、可重入锁和不可重入锁
  • 5、悲观锁和乐观锁
  • 6、公平锁和非公平锁
  • 7、共享锁和独占锁
  • 8、可中断锁和不可中断锁
  • 总结:

当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。但是现实并不是这样子的,所以JVM实现了锁机制,今天就叭叭叭JAVA中各种各样的锁。

1、自旋锁和自适应锁

自旋锁:在多线程竞争的状态下共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复阻塞线程并不值得,而是让没有获取到锁的线程自旋(自旋并不会放弃CPU的分片时间)等待当前线程释放锁,如果自旋超过了限定的次数仍然没有成功获取到锁,就应该使用传统的方式去挂起线程了,在JDK定义中,自旋锁默认的自旋次数为10次,用户可以使用参数-XX:PreBlockSpin来更改(jdk1.6之后默认开启自旋锁)。

自适应锁:为了解决某些特殊情况,如果自旋刚结束,线程就释放了锁,那么是不是有点不划算。自适应自旋锁是jdk1.6引入,规定自旋的时间不再固定了,而是由前一次在同一个锁上的自旋 时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么JVM会认为该线程自旋获取到锁的可能性很大,会自动增加等待时间。反之就认为不容易获取到锁,而放弃自旋这种方式。

锁消除:锁消除时指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。意思就是:在一段代码中,堆上的所有数据都不会逃逸出去而被其他线程访问到那就可以把他们当作栈上的数据对待,认为他们是线程私有的,不用再加锁。

锁粗化:

  public static void main(String[] args) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("a");
        buffer.append("b");
        buffer.append("c");
        System.out.println("拼接之后的结果是:>>>>>>>>>>>"+buffer);
    }
  @Override
    @IntrinsicCandidate
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

StringBuffer 在拼接字符串时是同步的。但是在一系列的操作中都对同一个对象(StringBuffer )反复加锁和解锁,频繁的进行加锁解锁操作会导致不必要的性能损耗,JVM会将加锁同步的范围扩展到整个操作的外部,只加一次锁。

2、轻量级锁和重量级锁

这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁, 取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。轻量级锁是相对于重量级锁而言的。

轻量级锁加锁过程

在HotSpot虚拟机的对象头分为两部分,一部分用于存储对象自身的运行时数据,如Hashcode、GC分代年龄、标志位等,这部分长度在32位和64位的虚拟机中分别是32bit和64bit,称为Mark Word。另一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。

对象头信息是与对象自身定义的数据无关的额外存储成本,Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息。mark word中有两个bit存储锁标记位。

HotSpot虚拟机对象头Mark Word

存储内容 标志位 状态
对象哈希码,分代年龄 01 无锁
指向锁记录的指针 00 轻量级锁
指向重量级锁的指针 10 膨胀重量级锁
空,不需要记录信息 11 GC标记
偏向线程id,偏向时间戳,对象分代年龄 01 可偏向

在代码进入同步代码块时,如果此对象没有被锁定(标记位为01状态),虚拟机首先在当前线程的栈帧建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前Mark Word的拷贝,然后虚拟机使用CAS操作尝试将对象的Mark Word 更新为指向Lock Record的指针,如果操作成功了,那么这个线程就有了这个对象的锁,并且将Mark Word 的标记位更改为00,表示这个对象处于轻量级锁定状态。如果更新失败了虚拟机会首先检查是否是当前线程拥有了这个对象的锁,如果是就进入同步代码,如果不是,那就说明锁被其他线程占用了。如果有两个以上的线程争夺同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标记位变为10,后面等待的线程就要进入阻塞状态。

轻量级锁解锁过程

解锁过程同样使用CAS操作来进行,使用CAS操作将Mark Word 指向Lock Record 指针释放,如果操作成功,那么整个同步过程就完成了,如果释放失败,说明有其他线程尝试获取该锁,那就在释放锁的同时,唤醒被挂起的线程。

3、偏向锁

JVM 参数 -XX:-UseBiasedLocking 禁用偏向锁;-XX:+UseBiasedLocking 启用偏向锁。

启用了偏向锁才会执行偏向锁的操作。当锁对象第一次被线程获取时,虚拟机会把对象头中的标记位设置为01,偏向模式。同时使用CAS操作获取到当前线程的线程ID存储到Mark Word 中,如果操作成功,那么持有偏向锁的线程以后每次进入这个锁相关的同步块时,都不需要任何操作,直接进入。如果有多个线程去尝试获取这个锁时,偏向锁就宣告无效,然后会撤销偏向或者恢复到未锁定。然后再膨胀为重量级锁,标记位状态变为10。

4、可重入锁和不可重入锁

可重入锁就是一个线程获取到锁之后,在另一个代码块还需要该锁,那么不需要重新获取而可以直接使用该锁。大多数的锁都是可重入锁。但是CAS自旋锁不可重入。

package com.xiaojie.juc.thread.lock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * @author xiaojie
 * @version 1.0
 * @description: 测试锁的重入性
 * @date 2021/12/30 22:09
 */
public class Test01 {
    public synchronized void a() {
        System.out.println(Thread.currentThread().getName() + "运行a方法");
        b();
    }
    private synchronized void b() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行b方法");
    }
    public static void main(String[] args) {
        Test01 test01 = new Test01();
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i=0;i<10;i++){
            executorService.execute(() -> test01.a());
        }
    }
}

5、悲观锁和乐观锁

悲观锁总是悲观的,总是认为会发生安全问题,所以每次操作都会加锁。比如独占锁、传统数据库中的行锁、表锁、读锁、写锁等。悲观锁存在以下几个缺点:

  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延迟,引起性能问题。
  • 一个线程占有锁后,其他线程就得阻塞等待。
  • 如果优先级高的线程等待一个优先级低的线程,会导致线程优先级导致,可能引发性能风险。

乐观锁总是乐观的,总是认为不会发生安全问题。在数据库中可以使用版本号实现乐观锁,JAVA中的CAS和一些原子类都是乐观锁的思想。

6、公平锁和非公平锁

公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。

非公平锁:非公平锁不需要按照申请锁的时间顺序来获取锁,而是谁能获取到CPU的时间片谁就先执行。非公平锁的优点是吞吐量比公平锁大,缺点是有可能导致线程优先级反转或者造成过线程饥饿现象(就是有的线程玩命的一直在执行任务,有的线程至死没有执行一个任务)。

synchronized中的锁是非公平锁,ReentrantLock默认也是非公平锁,但是可以通过构造函数设置为公平锁。

7、共享锁和独占锁

共享锁就是同一时刻允许多个线程持有的锁。例如Semaphore(信号量)、ReentrantReadWriteLock的读锁、CountDownLatch倒数闩等。

独占锁也叫排它锁、互斥锁、独占锁是指锁在同一时刻只能被一个线程所持有。例如synchronized内置锁和ReentrantLock显示锁,ReentrantReadWriteLock的写锁都是独占锁。

package com.xiaojie.juc.thread.lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
 * @description: 读写锁验证共享锁和独占锁
 * @author xiaojie
 * @date 2021/12/30 23:28
 * @version 1.0
 */
public class ReadAndWrite {
    static class ReadThred extends Thread {
        private ReentrantReadWriteLock lock;
        private String name;
        public ReadThred(String name, ReentrantReadWriteLock lock) {
            super(name);
            this.lock = lock;
        }
        @Override
        public void run() {
            try {
                lock.readLock().lock();
                System.out.println(Thread.currentThread().getName() + "这是共享锁。。。。。。");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.readLock().unlock();
                System.out.println(Thread.currentThread().getName() + "释放锁成功。。。。。。");
            }
        }
    }
    static class WriteThred extends Thread {
        private ReentrantReadWriteLock lock;
        private String name;
        public WriteThred(String name, ReentrantReadWriteLock lock) {
            super(name);
            this.lock = lock;
        }
        @Override
        public void run() {
            try {
                lock.writeLock().lock();
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName() + "这是独占锁。。。。。。。。");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.writeLock().unlock();
                System.out.println(Thread.currentThread().getName() + "释放锁。。。。。。。");
            }
        }
    }
    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        ReadThred readThred1 = new ReadThred("read-thread-1", reentrantReadWriteLock);
        ReadThred readThred2 = new ReadThred("read-thread-1", reentrantReadWriteLock);
        WriteThred writeThred1 = new WriteThred("write-thread-1", reentrantReadWriteLock);
        WriteThred writeThred2 = new WriteThred("write-thread-2", reentrantReadWriteLock);
        readThred1.start();
        readThred2.start();
        writeThred1.start();
        writeThred2.start();
    }
}

8、可中断锁和不可中断锁

可中断锁只在抢占锁的过程中可以被中断的锁如ReentrantLock。

不可中断锁是不可中断的锁如java内置锁synchronized。

总结:


名称


优点


缺点


使用场景


偏向锁


加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距


如果线程间存在锁竞争,会带来额外的锁撤销的消耗


适用于只有一个线程访问同步快的场景


轻量级锁


竞争的线程不会阻塞,提高了响应速度


如线程成始终得不到锁竞争的线程,使用自旋会消耗CPU性能


追求响应时间,同步快执行速度非常快


重量级锁


线程竞争不适用自旋,不会消耗CPU


线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗


追求吞吐量,同步快执行速度较长

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • 浅谈Java并发编程之Lock锁和条件变量

    简单使用Lock锁 Java 5中引入了新的锁机制--java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接口有3个实现它的类:ReentrantLock.ReetrantReadWriteLock.ReadLock和ReetrantReadWriteLock.WriteLock,即重入锁.读锁和写锁.lock必须被显式地创建.锁定和释放,为了可以使用更多的功能,一般用ReentrantLock为其实例

  • 详解Java并发编程之内置锁(synchronized)

    简介 synchronized在JDK5.0的早期版本中是重量级锁,效率很低,但从JDK6.0开始,JDK在关键字synchronized上做了大量的优化,如偏向锁.轻量级锁等,使它的效率有了很大的提升. synchronized的作用是实现线程间的同步,当多个线程都需要访问共享代码区域时,对共享代码区域进行加锁,使得每一次只能有一个线程访问共享代码区域,从而保证线程间的安全性. 因为没有显式的加锁和解锁过程,所以称之为隐式锁,也叫作内置锁.监视器锁. 如下实例,在没有使用synchronize

  • Java并发编程之死锁相关知识整理

    一.什么是死锁 所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进 二.死锁产生的条件 以下将介绍死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁 互斥条件 进程要求对所分配的资源(如打印机〉进行排他性控制,即在一段时间内某资源仅为一个进程所占有.此时若有其他进程请求该资源,则请求进程只能等待 不可剥夺条件 进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能

  • java并发编程专题(四)----浅谈(JUC)Lock锁

    首先我们来回忆一下上一节讲过的synchronized关键字,该关键字用于给代码段或方法加锁,使得某一时刻它修饰的方法或代码段只能被一个线程访问.那么试想,当我们遇到这样的情况:当synchronized修饰的方法或代码段因为某种原因(IO异常或是sleep方法)被阻塞了,但是锁有没有被释放,那么其他线程除了等待以外什么事都做不了.当我们遇到这种情况该怎么办呢?我们今天讲到的Lock锁将有机会为此行使他的职责. 1.为什么需要Lock synchronized 是Java 语言层面的,是内置的关

  • 并发编程之Java内存模型锁的内存语义

    目录 1.锁的释放-获取建立的happens-before关系 2.锁释放和获取的内存语义 3.锁内存的语义实现 4.concurrent包的实现 简介: 锁的作用是让临界区互斥执行.本文阐述所得另一个重要知识点--锁的内存语义. 1.锁的释放-获取建立的happens-before关系 锁是Java并发编程中最重要的同步机制.锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息. 锁释放-获取的示例代码: package com.lizba.p1; /** * <p>

  • 详解JUC并发编程之锁

    目录 1.自旋锁和自适应锁 2.轻量级锁和重量级锁 轻量级锁加锁过程 轻量级锁解锁过程 3.偏向锁 4.可重入锁和不可重入锁 5.悲观锁和乐观锁 6.公平锁和非公平锁 7.共享锁和独占锁 8.可中断锁和不可中断锁 总结: 当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的.但是现实并不是这样子的,所以JVM实现了锁机制,今天就叭叭叭JAVA中各种

  • 详解Java并发编程之volatile关键字

    目录 1.volatile是什么? 2.并发编程的三大特性 3.什么是指令重排序? 4.volatile有什么作用? 5.volatile可以保证原子性? 6.volatile 和 synchronized对比 总结 1.volatile是什么? 首先简单说一下,volatile是什么?volatile是Java中的一个关键字,也是一种同步机制.volatile为了保证变量的可见性,通过volatile修饰的变量具有共享性.修改了volatile修饰的变量,其它线程是可以读取到最新的值的 2.并

  • 详解Python GUI编程之PyQt5入门到实战

    1. PyQt5基础 1.1 GUI编程学什么 大致了解你所选择的GUI库 基本的程序的结构:使用这个GUI库来运行你的GUI程序 各种控件的特性和如何使用 控件的样式 资源的加载 控件的布局 事件和信号 动画特效 界面跳转 设计工具的使用 1.2 PyQT是什么 QT是跨平台C++库的集合,它实现高级API来访问现代桌面和移动系统的许多方面.这些服务包括定位和定位服务.多媒体.NFC和蓝牙连接.基于Chromium的web浏览器以及传统的UI开发.PyQt5是Qt v5的一组完整的Python

  • 详解C++元编程之Parser Combinator

    引子 前不久在CppCon上看到一个Talk:[constexpr All the things](https://www.youtube.com/watch?v=PJwd4JLYJJY),这个演讲技术令我非常震惊,在编译期解析json字符串,进而提出了编译期构造正则表达式(编译期构建FSM),现场掌声一片,而背后依靠的是C++强大的constexpr特性,从而大大提高了编译期计算威力. 早在C++11的时候就有constexpr特性,那时候约束比较多,只能有一条return语句,能做的事情只有

  • 详解python异步编程之asyncio(百万并发)

    前言:python由于GIL(全局锁)的存在,不能发挥多核的优势,其性能一直饱受诟病.然而在IO密集型的网络编程里,异步处理比同步处理能提升成百上千倍的效率,弥补了python性能方面的短板,如最新的微服务框架japronto,resquests per second可达百万级. python还有一个优势是库(第三方库)极为丰富,运用十分方便.asyncio是python3.4版本引入到标准库,python2x没有加这个库,毕竟python3x才是未来啊,哈哈!python3.5又加入了asyn

  • 详解JUC并发编程中的进程与线程学习

    目录 进程与线程 进程 线程 同步异步 串行并行执行时间 创建和运行线程 Thread 与 Runnable 的关系原理分析 查看进程 线程运行原理 线程上下文切换 start与run方法 sleep方法 sleep打断 join方法 interrupt 方法 守护进程 线程的状态 Java API 层面 总结 进程与线程 进程 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存.在指令运行过程中还需要用到磁盘.网络等设备.进程就是用来加载指令.管理内

  • 详解C语言编程之thread多线程

    目录 线程创建与结束 线程的创建方式: 线程的结束方式: join() detach() 互斥锁 <mutex> 头文件介绍 std::mutex 介绍 std::lock_guard std::unique_lock 示例: 原子变量 线程同步通信 线程死锁 死锁概述 死锁产生的条件 示例: 总结 线程创建与结束 C++11 新标准中引入了四个头文件来支持多线程编程,他们分别是<atomic> ,<thread>,<mutex>,<condition

  • Java并发编程之Semaphore(信号量)详解及实例

    Java并发编程之Semaphore(信号量)详解及实例 概述 通常情况下,可能有多个线程同时访问数目很少的资源,如客户端建立了若干个线程同时访问同一数据库,这势必会造成服务端资源被耗尽的地步,那么怎样能够有效的来控制不可预知的接入量呢?及在同一时刻只能获得指定数目的数据库连接,在JDK1.5 java.util.concurrent 包中引入了Semaphore(信号量),信号量是在简单上锁的基础上实现的,相当于能令线程安全执行,并初始化为可用资源个数的计数器,通常用于限制可以访问某些资源(物

  • java并发编程之cas详解

    CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值.这听起来可能有一点复杂但是实际上你理解之后发现很简单,接下来,让我们跟深入的了解一下这项技术. CAS的使用场景 在程序和算法中一个经常出现的模式就是"check and act"模式.先检查后操作模式发生在代码中首先检查一个变量的值,然后再基于这个值做一些操作.下面是一个

  • Java并发编程之LockSupport类详解

    一.LockSupport类的属性 private static final sun.misc.Unsafe UNSAFE; // 表示内存偏移地址 private static final long parkBlockerOffset; // 表示内存偏移地址 private static final long SEED; // 表示内存偏移地址 private static final long PROBE; // 表示内存偏移地址 private static final long SEC

随机推荐