我的编程空间,编程开发者的网络收藏夹
学习永远不晚

详解JUC并发编程之锁

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

详解JUC并发编程之锁

当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。但是现实并不是这样子的,所以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膨胀重量级锁
空,不需要记录信息11GC标记
偏向线程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;

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;

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

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

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

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

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

详解JUC并发编程之锁

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

JUC并发编程中的锁有哪些

这篇文章主要讲解了“JUC并发编程中的锁有哪些”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“JUC并发编程中的锁有哪些”吧!当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和
2023-06-22

Java并发编程之显式锁机制详解

我们之前介绍过synchronized关键字实现程序的原子性操作,它的内部也是一种加锁和解锁机制,是一种声明式的编程方式,我们只需要对方法或者代码块进行声明,Java内部帮我们在调用方法之前和结束时加锁和解锁。而我们本篇将要
2023-05-30

Golang并发编程之Channel详解

传统的并发编程模型是基于线程和共享内存的同步访问控制的,共享数据受锁的保护,使用线程安全的数据结构会使得这更加容易。本文将详细介绍Golang并发编程中的Channel,,需要的朋友可以参考下
2023-05-19

JUC 并发编程学习笔记(总)

文章目录 1. 什么是JUC2. 进程和线程2.1 进程2.2 线程2.3 并发2.4 并行2.5 线程的状态2.6 wait 和 sleep 的区别 3. Lock锁(重点)3.1 传统Synchronized3.2 Lock
2023-08-18

Java并发编程面试题——JUC专题

文章目录 一、AQS高频问题1.1 AQS是什么?1.2 唤醒线程时,AQS为什么从后往前遍历?1.3 AQS为什么用双向链表,(为啥不用单向链表)?1.4 AQS为什么要有一个虚拟的head节点1.5 ReentrantLock的
2023-08-18

Java并发之嵌套管程锁死详解

·嵌套管程死锁是如何发生的·具体的嵌套管程死锁的例子·嵌套管程死锁 vs 死锁嵌套管程锁死类似于死锁, 下面是一个嵌套管程锁死的场景:Thread 1 synchronizes on AThread 1 synchronizes on B
2023-05-30

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录