多线程(二)- Java内置锁的核心原理

前言

Java内置锁是一个互斥锁,这就意味着最多只有一个线程能够获得该锁,当线程B尝试去获得线程A持有的内置锁时,线程B必须等待或者阻塞,直到线程A释放这个锁,如果线程A不释放这个锁,那么线程B将永远等待下去。

Java中每个对象都可以用作锁,这些锁称为内置锁。线程进入同步代码块或方法时会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁保护的同步代码块或方法。

一、线程安全问题

当多个线程并发访问某个Java对象(Object)时,无论系统如何调度这些线程,也无论这些线程将如何交替操作,这个对象都能表现出一致的、正确的行为,那么对这个对象的操作是线程安全的。如果这个对象表现出不一致的、错误的行为,那么对这个对象的操作不是线程安全的,发生了线程的安全问题。

1.自增运算不是线程安全的

下面写一个例子

package test.juc.safe;import java.util.concurrent.CountDownLatch;/*** 用10个线程自增,最后结果汇总*/
public class SelfPlusTest {private static final int THREAD_SIZE = 10;private static final int ADD_TIMES = 1000;public static void main(String[] args) throws InterruptedException {SelfPlus selfPlus = new SelfPlus();//用于让线程全部跑完(countDownLatch归0),才获取计算结果CountDownLatch countDownLatch = new CountDownLatch(THREAD_SIZE);for (int i = 0; i < THREAD_SIZE; i++) {new Thread(() -> {for (int j = 0; j < ADD_TIMES; j++) {selfPlus.selfAdd();}countDownLatch.countDown();}).start();}//等待所有线程跑完countDownLatch.await();long amount = selfPlus.getAmount();System.out.println("预期计算结果:" + THREAD_SIZE * ADD_TIMES);System.out.println("实际计算结果:" + amount);}
}
package test.juc.safe;public class SelfPlus {private Integer amount = 0;public void selfAdd() {amount++;}public Integer getAmount() {return amount;}}

按照常规理解,预期结果应该是10万,但是结果不是。

原因分析

为什么自增运算符不是线程安全的呢?实际上,一个自增运算符是一个复合操作,至少包括三个JVM指令:​“内存取值”​“寄存器增加1”和“存值到内存”​。这三个指令在JVM内部是独立进行的,中间完全可能会出现多个线程并发进行。

比如在amount=100时,假设有三个线程同一时间读取amount值,读到的都是100,增加1后结果为101,三个线程都将结果存入amount的内存,amount的结果是101,而不是103。

“内存取值”​“寄存器增加1”和“存值到内存”这三个JVM指令本身是不可再分的,它们都具备原子性,是线程安全的,也叫原子操作。但是,两个或者两个以上的原子操作合在一起进行操作就不再具备原子性了。比如先读后写,就有可能在读之后,其实这个变量被修改了,出现读和写数据不一致的情况。

2.synchronized关键字

每个Java对象都隐含有一把锁,这里称为Java内置锁(或者对象锁、隐式锁)​。使用synchronized(syncObject)调用相当于获取syncObject的内置锁,所以可以使用内置锁对临界区代码段进行排他性保护。

在前面1的例子中,对selfAdd加上synchronized关键字,就相当于给这个方法的存取操作做了排他处理。方法只有在一个线程运行完后,另一个线程才能运行这个方法,其他线程只能等待,就可以解决线程安全问题。

    public synchronized void selfAdd() {amount++;}


 

3.生产者消费者问题

生产者-消费者问题(Producer-Consumer Problem)也称有限缓冲问题(Bounded-Buffer Problem)​,是一个多线程同步问题的经典案例。

生产者-消费者问题描述了两类访问共享缓冲区的线程(所谓的“生产者”和“消费者”​)在实际运行时会发生的问题。生产者线程的主要功能是生成一定量的数据放到缓冲区中,然后重复此过程。消费者线程的主要功能是从缓冲区提取(或消耗)数据。

生产者-消费者问题的关键是:

(1)保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区空时消耗数据。

(2)保证在生产者加入过程、消费者消耗过程中,不会产生错误的数据和行为。

生产者-消费者问题不仅仅是一个多线程同步问题的经典案例,而且业内已经将解决该问题的方案抽象成了一种设计模式——“生产者-消费者”模式。​“生产者-消费者”模式是一个经典的多线程设计模式,它为多线程间的协作提供了良好的解决方案。

3.1 生产者-消费者模式

在生产者-消费者模式中,通常有两类线程,即生产者线程(若干个)和消费者线程(若干个)​。生产者线程向数据缓冲区(DataBuffer)加入数据,消费者线程则从数据缓冲区消耗数据。

在生产者-消费者模式中,至少有以下关键点:

(1)生产者与生产者之间、消费者与消费者之间,对数据缓冲区的操作是并发进行的。

(2)数据缓冲区是有容量上限的。数据缓冲区满后,生产者不能再加入数据;数据缓冲区空时,消费者不能再取出数据。

(3)数据缓冲区是线程安全的。在并发操作数据缓冲区的过程中,不能出现数据不一致的情况;或者在多个线程并发更改共享数据后,不会造成出现脏数据的情况。

(4)生产者或者消费者线程在空闲时需要尽可能阻塞而不是执行无效的空操作,尽量节约CPU资源。

为了解决生产者消费者存在的线程安全问题,可以用synchronized关键字,这样一来,所有的生产、消费动作在执行过程中都需要抢占同一个同步锁,最终的结果是所有的生产、消费动作都被串行化了。生产、消费动作肯定不能串行执行,而是需要并行执行,而且并行化程度越高越好。如何既保障没有线程安全问题,又能提高生产、消费动作的并行化程度呢?就是使用下面要介绍的Java内置锁。

3.2 Java对象结构和内置锁

在介绍Java内置锁之前,需要介绍一下Java对象结构

3.2.1 Java对象结构 

Java对象结构包括三部分:对象头、对象体和对齐字节

1)对象头
对象头包括三个字段,第一个字段叫作Mark Word(标记字),用于存储自身运行时的数据,例如GC标志位、哈希码、锁状态等信息。
第二个字段叫作Class Pointer(类对象指针),用于存放方法区Class对象的地址,虚拟机通过这个指针来确定这个对象是哪个类的实例。
第三个字段叫作Array Length(数组长度)。如果对象是一个Java数组,那么此字段必须有,用于记录数组长度的数据;如果对象不是一个Java数组,那么此字段不存在,所以这是一个可选字段。

(2)对象体
对象体包含对象的实例变量(成员变量),用于成员属性值,包括父类的成员属性值。这部分内存按4字节对齐。

(3)对齐字节
对齐字节也叫作填充对齐,其作用是用来保证Java对象所占内存字节数为8的倍数HotSpot VM的内存管理要求对象起始地址必须是8字节的整数倍。对象头本身是8的倍数,当对象的实例变量数据不是8的倍数时,便需要填充数据来保证8字节的对齐。

3.2.2 Mark Word的结构信息

Mark Word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark Word为32位,64位JVM为64位。Mark Word的位长度不会受到Oop对象指针压缩选项的影响。

不同状态下的Mark Word字段结构

Java内置锁的状态总共有4种,级别由低到高依次为:无锁、偏向锁、轻量级锁和重量级锁。其实在JDK 1.6之前,Java内置锁还是一个重量级锁,是一个效率比较低下的锁,在JDK 1.6之后,JVM为了提高锁的获取与释放效率,对synchronized的实现进行了优化,引入了偏向锁和轻量级锁,从此以后Java内置锁的状态就有了4种(无锁、偏向锁、轻量级锁和重量级锁),并且4种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别)

32位Mark Word的结构信息

64位Mark Work的结构信息

3.2.3 64位Mark Word的构成

(1)lock:锁状态标记位,占两个二进制位,由于希望用尽可能少的二进制位表示尽可能多的信息,因此设置了lock标记。该标记的值不同,整个Mark Word表示的含义就不同。
(2)biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

lock和biased_lock两个标记位组合在一起共同表示Object实例处于什么样的锁状态,二者组合的含义具体如下

(3)age:4位的Java对象分代年龄。在GC中,对象在Survivor区复制一次,年龄就增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,因此最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

(4)identity_hashcode:31位的对象标识HashCode(哈希码)采用延迟加载技术,当调用Object.hashCode()方法或者System.identityHashCode()方法计算对象的HashCode后,其结果将被写到该对象头中。当对象被锁定时,该值会移动到Monitor(监视器)中。

(5)thread:54位的线程ID值为持有偏向锁的线程ID。

(6)epoch:偏向时间戳。

(7)ptr_to_lock_record:占62位,在轻量级锁的状态下指向栈帧中锁记录的指针。

(8)ptr_to_heavyweight_monitor:占62位,在重量级锁的状态下指向对象监视器的指针。

3.3 无锁、偏向锁、轻量级锁、重量级锁

3.3.1 无锁状态

Java对象刚创建时,还有任何线程来竞争,对象处于无锁状态。这时偏向锁的标识位是0,锁状态是01

3.3.2 偏向锁状态

偏向锁是指对象一直被同一个线程访问,那么该线程会自动获取锁,以降低获取锁的代价。如果内置锁处于偏向状态,当有一个线程来竞争锁时,先用偏向锁,表示内置锁偏爱这个线程,这个线程要执行关联的同步代码时,不需要再做任何检查和切换。偏向锁在竞争不激烈的情况下,效率非常高。 偏向锁状态的Mark Word会记录内置锁自己偏爱的线程ID。

3.3.3 轻量级锁状态

当有两个线程开始竞争这个锁对象时,情况发生变化了,不再是偏向锁(独占锁)了,锁会升级为轻量级锁,两个锁公平竞争,哪个线程先占有锁对象,锁对象的Mark Word就指向哪个线程的栈帧中的锁记录。

当锁处于偏向锁又被另一个线程企图抢占时,偏向锁会升级为轻量级锁。企图抢占的线程会通过自旋的方式尝试获取锁,不会阻塞抢锁线程,以便提高性能。

自旋的原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,他们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核切换的消耗。

但是线程自旋是需要消耗CPU的,如果一直获取不到锁,那线程也不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。JVM对于自旋周期的选择,JDK1.6之后引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不是固定的,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定的。线程如果自旋成功了,下次自旋的次数就会更多,如果自旋失败了,自旋的次数就会减少。

如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,自旋不会一直持续下去,这时争用线程会停止自旋进入阻塞状态,该锁膨胀为重量级锁。

3.3.4 重量级锁状态

重量级锁会让其他申请锁的线程进入阻塞,性能降低。重量级锁也叫同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,该监视器对象用集合的形式来登记和管理排队的线程。

3.3.5  偏向锁、轻量级锁与重量级锁的对比

3.4  线程间通信

3.4.1 定义

多个线程间按照指定的规则公共完成一件任务,这些线程之前就需要互相协调,这个过程被称为线程的通信。线程间的通信有很多种:等待-通知、共享内存、管道流。

3.4.2 wait、notify方法的原理

3.4.2.1 对象的wait()方法

对象wait()方法的主要作用是让当前线程阻塞并等待被唤醒。wait()方法与对象监视器紧密相关,使用wait()方法时也一定需要放在同步代码块中

Object类中的wait方法有三个版本

(1) void wait()
当调用了同步对象的wait实例方法后,当前线程将进入等待,当前线程进入looko的监视器的WaitSet,等待被娶其他线程唤醒

(2) void wait(long timeout)

阻塞等待的超时版本,等待指定的时间

(3) wait(long timeout , int nanos)

阻塞等待的超时版本,可以设置等待时长,更精确的控制等待时间,实现更高精度的等待

3.4.3 wait方法的核心原理

1)当线程调用了lock实例的wait方法后,JVM会当前线程加入locko监视器的ViewSet,等待被其他线程唤起

2)当线程会释放lokco对象监视器的Owner权利,让其他线程可以抢夺locko对象的监视

3)让当前线程等待,使其状态变成WAITTING

3.4.4 对象的notify()方法

对象的notify()方法的主要作用是唤醒在等待的线程。notify()方法与对象监视器紧密相关,使用notify()方法时也需要放在同步代码块中。

(1)void notify()

调用后,唤醒locko监视器等待集中的第一个等待线程;

被唤醒的线程进入EntryList,其状态从WAITTING等待状态变成BLOCKED

(2)void notifyAll()

locko.notigyAll()被调用后,唤醒locko监视器等待集中的全部等待线程;

所有被唤醒的线程进入EntryList,线程状态从WAITING等待状态变成BLOCKED

3.4.5 notify()方法的核心原理

1)当线程调用了某个对象的notify()方法后,JVM会唤醒对象实例监视器WaitSet中的第一个等待线程

2)当线程调用了某个对象的notigyAll()方法后,JVM会唤醒对象实例监视器WaitSet中的所有等待线程

3)等待线程被唤醒后,会从监视器的WaitSet移动到EntryList,线程具备了排队抢夺监视器Owner的权利,其状态从WAITING变成BLOCKED

4)EntryList中的线程抢夺到资源器Owner权利之后,线程的状态从BLOCKED变成Runnable,具备重新执行的资格

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/486284.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024年12月7日Github流行趋势

项目名称&#xff1a;lobe-chat 项目维护者&#xff1a;arvinxx, semantic-release-bot, canisminor1990, lobehubbot, renovate项目介绍&#xff1a;Lobe Chat 是一个开源的现代化设计的人工智能聊天框架。支持多AI提供商&#xff08;OpenAI / Claude 3 / Gemini / Ollama / Q…

群控系统服务端开发模式-应用开发-邮件工厂QQ发送开发

一、邮件发送类实例修改 在Mail目录下修改邮件发送类实例&#xff0c;具体代码如下&#xff1a; <?php /*** 创建邮件发送类实例工厂* User: 龙哥三年风水* Date: 2024/12/5* Time: 14:32*/ namespace Mail; use app\model\param\Emailsms; use Error\BaseError; use Mail…

Golang内存模型总结1(mspan、mcache、mcentral、mheap)

1.内存模型 1.1 操作系统存储模型 从上到下分别是寄存器、高速缓存、内存、磁盘&#xff0c;其中越往上速度越快&#xff0c;空间越小&#xff0c;价格越高。 关键词是多级模型和动态切换 1.2 虚拟内存与物理内存 虚拟内存是一种内存管理技术&#xff0c;允许计算机使用比…

ASP.NET Core8.0学习笔记(二十五)——EF Core Include导航数据加载之预加载与过滤

一、导航属性数据加载 1.在EF Core中可以使用导航属性来加载相关实体。 2.加载实体的三种方式&#xff1a; (1)预先加载&#xff1a;直接在查询主体时就把对应的依赖实体查出来&#xff08;作为初始查询的一部分&#xff09; (2)显式加载&#xff1a;使用代码指示稍后显式的从…

MATLAB Simulink® - 智能分拣系统

系列文章目录 前言 本示例展示了如何在虚幻引擎 环境中对四种不同形状的标准 PVC 管件实施半结构化智能分拣。本示例使用 Universal Robots UR5e cobot 执行垃圾箱拣选任务&#xff0c;从而成功检测并分类物体。cobot 的末端执行器是一个吸力抓手&#xff0c;它使 cobot 能够拾…

环形链表 (简单易懂)

给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&#xff08;…

【C++】奇偶数判断题的高级分析与优化

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;1. 题目描述题目背景 &#x1f4af;2. 基本解决思路示例分析 &#x1f4af;3. 原始代码分析代码分析代码优点代码缺点 &#x1f4af;4. 教师代码及其优化分析代码分析代码优…

1.1 Beginner Level学习之“创建 ROS msg 和 srv”(第十节)

学习大纲&#xff1a; 1. msg 和 srv msg 文件是描述 ROS 消息字段的简单文本文件。它们用于为不同语言生成消息的源代码。srv 文件则描述了一个服务&#xff0c;包括两部分&#xff1a;请求和响应。Srv 文件用于生成服务的源代码。msg 文件存储在包的 msg 目录中。srv 文件存…

Linux-笔记---系统文件I/O

1. open函数和close函数 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);#include <unistd.h> int close(int fd); open函数…

红日靶场vulnstark 4靶机的测试报告[细节](一)

目录 一、测试环境 1、系统环境 2、注意事项 3、使用工具/软件 二、测试目的 三、操作过程 1、信息搜集 2、漏洞利用Getshell ①Struts 2 s2-045漏洞 手工利用s2-45漏洞 Msf综合利用 ②Tomcat框架(CVE-2017-12615) ③phpMyAdmin(CVE-2018-12613) 构造语句写入冰蝎木…

利用 360 安全卫士极速版关闭电脑开机自启动软件教程

在使用电脑的过程中&#xff0c;过多的开机自启动软件会严重拖慢电脑的开机速度&#xff0c;影响我们的使用体验。本教程中简鹿办公将详细介绍如何使用 360 安全卫士极速版关闭电脑开机自启动软件&#xff0c;让您的电脑开机更加迅速流畅。 一、打开 360 安全卫士极速版 在电…

计算机毕业设计Spark股票推荐系统 股票预测系统 股票可视化 股票数据分析 量化交易系统 股票爬虫 股票K线图 大数据毕业设计 AI

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

本文介绍麒麟信安服务器系统(kylinsec)的安装。

本文介绍麒麟信安服务器系统&#xff08;kylinsec&#xff09;的安装。 下载 在开源欧拉官方找到商业版本的介绍找到相关产品&#xff1a; https://www.openeuler.org/zh/download/commercial-release/ 麒麟信安kylinsec下载地址&#xff1a; https://mirrors.kylinsec.com…

并发专题(10)之FutureTask源码剖析

一、FutureTask介绍 Java创建线程的方式&#xff0c;一般常用的是Thread&#xff0c;Runnable&#xff0c;如果需要处理当前的任务有返回结果的话&#xff0c;需要使用Callable。Callable运行需要配合Future来使用。 Future是一个接口&#xff0c;一般会使用FutureTask实现类去…

ssh远程升级Ubuntu20.04到Ubuntu 22.04

ssh远程升级Ubuntu20.04到Ubuntu 22.04 陈拓 2024/10/16-2024/10/26 1. 简介 本文介绍了如何通过ssh将Ubuntu系统从20.04升级到22.04。 在进行系统升级之前&#xff0c;建议备份重要数据&#xff0c;以防升级过程中出现问题。 2. 更新当前系统 硬件系统架构 当前操作系统版…

新手SEO指南:如何从零开始优化网站实现流量增长

内容概要 在这一部分&#xff0c;我们将简要概述新手在进行SEO优化时需要掌握的一些关键内容。SEO&#xff08;搜索引擎优化&#xff09;是一个复杂而多层次的过程&#xff0c;对网站流量的提升至关重要。无论您是刚刚踏入这一领域的新手&#xff0c;还是希望进一步提升网站性…

FPGA实战篇(呼吸灯实验)

1.呼吸灯简介 呼吸灯采用 PWM 的方式&#xff0c;在固定的频率下&#xff0c;通过调整占空比的方式来控制 LED 灯亮度的变化。 PWM&#xff08;Pulse Width Modulation &#xff09;&#xff0c;即脉冲宽度调制&#xff0c;它利用微处理器输出的 PWM 信号&#xff0c;实现对…

使用 OpenCV 进行 Android 开发

在本节中&#xff0c;我们将创建一个简单的应用程序&#xff0c;它除了加载 OpenCV 之外什么都不做。在下一节中&#xff0c;我们将扩展它以支持相机。 除了这个说明&#xff0c;你还可以使用一些视频指南&#xff0c;例如这个 打开 Android Studio 并选择Empty Views Activi…

项目实例_FashionMNIST_CNN

前言 提醒&#xff1a; 文章内容为方便作者自己后日复习与查阅而进行的书写与发布&#xff0c;其中引用内容都会使用链接表明出处&#xff08;如有侵权问题&#xff0c;请及时联系&#xff09;。 其中内容多为一次书写&#xff0c;缺少检查与订正&#xff0c;如有问题或其他拓展…

Autosar FO时间分析和设计规范导读

一、规范功能概述 “Timing Analysis and Design AUTOSAR FO R24 - 11” 文档主要聚焦于汽车电子系统开发中的定时分析与设计&#xff0c;详细阐述了相关概念、方法、用例及涉及的各项要素&#xff0c;旨在为汽车电子系统的开发提供全面且系统的定时分析指导&#xff0c;以确保…