Java虚拟机——线程安全与锁优化

news2024/9/24 7:14:38

1 线程安全

  • 当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作。调用这个对象的行为都可以获得安全的结果,就称这个对象是线程安全的。
  • 代码本身封装了所有必要的正确保障性操作(如互斥同步等),令调用者无须关心多线程下的调用问题,更无须自己实现任何措施来保证多线程环境下的正确调用。

1.1 Java语言中的线程安全

  • 这里讨论的线程安全,都是以多个线程之间存在共享数据访问为前提。 按照线程安全的"安全程度"由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下几大类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

1.1.1 不可变

  • 不可变的对象一定是线程安全的,无论是谁都不需要进行任何线程安全保障措施。
  • 如果是一个基本数据类型,那么只需要使用final关键字来修饰,他就是不可变的。如果共享数据是一个对象,那么就需要对象保证其行为不会对其状态产生任何的影响才行。例如java.lang.String类,它是不可变的,就算用户调用它的substring(),replace()等方法,都只会返回一个新构建的字符串对象。
  • 类似的不可变的还有 枚举类型(及java.lang.Number的部分子类),如Long和Double等数值包装类型,BigInteger和BigDecimal等大数据类型。

1.1.2 绝对线程安全

  • 绝对线程安全能够完全满足我们前面提到的那个复杂的定义。 一个类要达到"不管运行环境如何,调用者都不需要任何额外的同步措施"可能需要付出非常高昂的代价。
  • JavaAPI中标注自己是线程安全的类,绝大多数不是绝对的线程安全。
  • 例如java.util.Vector是一个线程安全的容器,它的add(),get()和size()等方法都是被synchronized修饰的。虽然这样子效率不高,但是保证了它具备了原子性、可见性和有序性。
package specialTest;

import java.util.Vector;

public class VectorThreadSafeDemo {
    private static Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {

        while (true) {
            for (int i = 0; i < 10; i++) {
                vector.add(i);
            }

            Thread removeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.remove(i);
                    }
                }
            });

            Thread printThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i = 0 ; i < vector.size() ; i++){
                        System.out.println(vector.get(i));
                    }
                }
            });

            removeThread.start();
            printThread.start();

            while(Thread.activeCount() > 20);

        }

    }
}
  • 如果一个线程恰好在某一个时间点删除了一个元素,导致序号i不可用了,这个时候再访问i可能就会抛出异常。
  • 假设Vector一定要做到绝对的线程安全,就必须在内部维护一组一致性的快照访问才行,每次对其中的元素进行改动都要产生新的快照,这样付出的时间成本和空间成本是巨大的。

1.1.3 相对线程安全

  • 相对线程安全就是我们通常意义上讲的线程安全,他需要保证这个对象的单次操作是线程安全的。
  • 我们调用的时候不需要进行额外的保障操作,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
  • 在Java中,大部分声称线程安全的类都属于这个类型。

1.1.4 线程兼容

  • 线程兼容是指对象本身并不是线程安全的,但是可以在调用端正确地使用同步手段来保证对象在并发环境下可以安全使用。 Java中的大部分类都是线程兼容的,例如ArrayList和HashMap等。

1.1.5 线程对立

  • 线程对立是指不管调用端是否采用了同步措施,都无法在多线程环境下并发使用代码。这种情况在Java中是比较少见的。
  • 典型案例就是,一个Thread类的suspend()和resume()方法,如果有两个线程同时持有一个线程对象,一个尝试去中断线程,一个尝试去恢复线程,在并发的情况下,无论调用时是否执行了同步,目标线程都存在死锁的风险。

1.2 线程安全的实现方式

1.2.1 互斥同步

  • 是最常见也是最主要的并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一条线程使用

  • 互斥是实现同步的一种手段。临界区、互斥量和信号量都是常见的互斥实现方式。

  • Java中,最基本的互斥手段就是synchronized关键字,这是一种块状结构的同步语法。被synchronized修饰的同步块对于同一条线程来说是可重入的,所以同一线程反复进入同步块也不会出现把自己锁死的情况。 被synchronized修饰的同步块在持有锁的线程执行完毕释放锁之前,将会阻塞后面其他线程的进入。

  • 持有锁是一个重量级的操作。

  • 重入锁是Lock接口(全新的互斥同步手段)最常见的一种实现形式,它与synchronized一样是可以重入的。ReentrantLock与synchronized相比增加了一些高级功能。

  1. 等待可中断:如果持有锁的线程长期不释放锁,其他等待线程可以选择放弃等待,改为处理其他事情。
  2. 公平锁:多个线程在等待同一个锁的时候,必须按照申请锁的时间顺序来依次获得锁。(非公平锁在锁释放的时候,任何线程都可能获得锁)
  3. 锁绑定多个条件:一个ReentrantLock对象可以绑定多个Condition对象。在synchronized中,锁对象的wait方法跟他的notify()或者notifyAll()方法配合可以实现一个隐含条件,但是要和多个条件关联,就得添加一个额外的锁。

1.2.2 非阻塞同步

  • 互斥同步面临的问题是进行线程阻塞和唤醒所带来的性能开销,因此也叫作阻塞同步。
  • 从解决问题角度来看,互斥同步属于一种悲观的并发策略,认为只要不去做同步措施,那肯定会出现问题。无论共享的数据是否会发生竞争,都会进行加锁。
  • 乐观并发策略:不管风险,先进行操作,如果没有其他线程争用共享数据,那么操作就直接成功。如果共享的数据被争用,那么就进行补偿措施。(不断地重试,直到出现没有竞争的共享数据为止)
  • 这种策略的实现不需要把线程阻塞挂起,因此这种同步操作称为非阻塞同步。
  • 使用CAS(乐观锁)进行操作

1.2.3 无同步方案

  • 不一定一定要进行阻塞或非阻塞同步,同步与线程安全没有必要的联系,同步只是保障在共性数据争用时的正确手段。因此会有一些代码天生就是安全的。
  1. 可重入代码:指可以在代码执行的任何时刻中断它、转而去执行另外一段代码,而在控制返回后,原本的程序不会出现任何错误,也不会对结果有所影响。
    • 共同特征:不依赖全局变量、不依赖存储在堆上的数据和公用的资源、用到的状态量都由参数中传入、不调用非可重入的方法
    • 如果一个方法的返回结果是可以预测的,即输入了相同的数据,返回的结果都相同。他就满足可重入性的要求,当然也是线程安全的。
  2. 线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否保证在同一个线程中执行。
  • 例如生产者-消费者模式中的消费过程都限制在一个线程中消费完。
  1. ThreadLocal:使用ThreadLocal可以实现线程本地存储的功能。

2 锁优化

  • 为了在线程之间高效地共享数据及解决竞争问题,HotSpot花费了大量的资源去实现各种锁优化技术。如适应性自旋、锁消除、锁粗化、轻量级锁、偏向锁等。

2.1 自旋锁与自适应自旋

  • 互斥同步对性能最大的影响就是阻塞的实现,挂起线程和恢复线程的操作都要转入内核态中实现。共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。
  • 如果一个物理机器上 有一个以上的处理器或者处理器核心 , 能让两个线程同时并行执行,我们就可以然后面请求锁的线程"稍等一会",但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放。
  • 为了让线程等待,我们让线程执行一个忙循环,也就是自旋。这项技术叫做自旋锁

不过自旋会消耗处理器资源,如果锁被占用的时间长,这些资源的消耗是浪费的

  • JDK6对自旋锁进行了优化,引入了自适应的自旋
  • 自适应意味着自旋的时间不是固定的,而是由前一次在同一个锁上的自旋时间和锁的拥有者的状态来决定。
  • 如果虚拟机认为这次自旋很可能会成功,那么就允许自旋等待持续相对更长的时间。如果自旋很少成功获得过锁,甚至可能直接省略自旋的过程。

2.2 锁消除

  • 锁消除指 虚拟机即时编译器在运行时检测 到某段需要同步的代码根本不可能存在共享数据竞争, 那么就对锁进行消除的优化策略。
  • 锁消除的主要判断来自于逃逸分析的数据支持,如果判断一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,就可以把它们当作栈上的数据对待,认为是线程私有的。

可是变量是否逃逸,程序员自己是清楚的,为什么会在这种情况下还要求同步呢
那是因为Java中同步措施不是程序员加的,同步代码出现的次数很频繁。

public String concatString(String s1 , String s2 , String s3){
    return s1 + s2 + s3;
}
  • 这里的String是一个不可变的类,对字符串的连接操作是通过生成新的String对象来进行的。
  • 但是Javac编译器会对String连接进行自动优化,会将字符串加法转化为StringBuffer对象的连续append操作。而StringBuffer.append()方法中就有一个同步块,锁的就是sb对象。
  • 而逃逸分析后发现,它的动态作用域被限制在concatString()内部,sb的所有引用不会逃逸到外部,其他线程无法访问,所以可以安全地消除掉。

2.3 锁粗化

  • 在编写代码时,我们希望同步块的作用范围限制的小一点,在共享数据的实际作用域中进行同步。
  • 一系列的连续操作都对同一个对象进行加锁和解锁,会带来不必要的损耗。
public String concatString(String s1 , String s2 , String s3){
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}
  • 如果虚拟机检测到有一串零碎的操作都对同一个对象加锁,会将加锁同步的范围扩展(粗化)到整个操作序列的外部。

2.4 轻量级锁

  • 轻量级锁是相对于操作系统互斥量来实现的传统锁而言,因此传统的锁机制被称为"重量级锁"。轻量级锁不是用来代替重量级锁的,而是在没有多线程竞争的情况下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
  • 对于HotSpot虚拟机的内存布局来说(尤其是对象头部分)
    • HotSpot虚拟机的的对象头分为两部分,第一部分用于存储对象的自身运行时数据。(如哈希码、GC分代年龄)
    • 另外一部分存储的是指向方法区对象类型数据的指针。
  • 对象头的第一部分,官方称为"Mark Word",是实现轻量级锁的关键。
    在这里插入图片描述
    在这里插入图片描述

轻量级锁的工作过程

  1. 在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为"01")。那么虚拟机首先将在当前的线程的栈帧中创建一个名为锁记录(Lock Record)的空间。
  2. 此空间用于存储锁对象目前的Mark Word的拷贝。
  3. 虚拟机使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。获得锁
  • 成功
  1. 代表该线程拥有了这个对象的锁,并且对象MarkWord的锁标志位变为"00",表示此对象属于轻量级锁状态。
  • 失败
  1. 意味着至少存在一条线程与当前线程竞争获取该对象的锁
  2. 虚拟机会检查当前对象是否指向当前线程的栈帧
  3. 如果是,说明当前线程已经拥有这个对象的锁,那么直接进入同步块继续执行就可以
  4. 不是的话,说明已经被其他线程抢占,那轻量级锁(两条线程竞争)就不再有效,需要改为重量级锁,标志位变为"10"
    在这里插入图片描述
    再有竞争的情况下,轻量级锁反而比重量级锁更慢。

2.5 偏向锁

  • 所谓偏向锁,就是从这个锁偏心,永远只给一个线程加锁。

  • 也就是说,有一个线程访问了这个锁资源,那么就认为只有一个线程访问,完全没有其他线程访问。所以完全忽略了同步问题,让这个线程一直持有这个锁,哪怕退出了锁住的代码块也不解锁,减少了每次加锁、解锁带来的开销。

  • 但是如果一旦有一个别的线程与它竞争这个锁,那么就会升级成轻量级锁。

  • 工作过程

  1. 当一个线程访问同步代码块并获取锁时,虚拟机会把对象头中的标志位设置为"01",把偏向模式设置为"1",表示进入偏行模式。
  2. 同时通过CAS操作把当前线程的ID存储在Mark Word中。
  3. 在线程进入和退出同步代码块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里面是否存储着指向当前线程的偏向锁
  4. 一旦出现第二个线程来尝试获取这个锁的情况,偏行模式马上宣告结束。
  5. 它首先会暂停拥有偏向锁的线程,然后根据锁对象目前是否处于被锁定的状态来决定是否撤销偏向。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从零开始理解Linux中断架构(23)中断运行临界区和占先调度

Linux在内核中定义了6种运行临界区。 in_interrupt in_interrupt在驱动中使用频率最高的函数了,in_interrupt()就是指示Core是否正在中断处理中,包含了硬中断,软中断运行临界区。如果在中断处理中,则不能调用__do_softirq执行软中断处理。硬中断中不可调度不可中断,所有…

智慧园区安保人员巡更巡检解决方案,蓝牙信标主动式蓝牙定位导航系统

一、需求分析 目前&#xff0c;大部分写字楼&#xff0c;工厂&#xff0c;学校&#xff0c;银行&#xff0c;车站等场景对安保人员的管理依然靠手填单子记录作业情况&#xff0c;在缺乏信息化手段的情况下&#xff0c;靠人员自觉性或者RFID巡更棒&#xff0c;在这些传统方式下…

2023 年牛客多校第四场题解

A Bobo String Construction 题意&#xff1a;给定一个 01 01 01 字符串 t t t&#xff0c;构造一个长度为 n n n 的 01 01 01 串 s s s&#xff0c;使得 t t t 在 c o n c a t ( t , s , t ) {\rm concat}(t, s, t) concat(t,s,t) 中仅出现两次。多测&#xff0c; 1 ≤…

Android杂录 语音转文字功能 python混合开发环境搭建 priv-app开机赋予麦克风权限 HDMI与麦克风同时工作配置

专栏目录: 高质量文章导航-持续更新中_GZVIMMY的博客-CSDN博客 一.语音转文字功能 软件架构 硬件架构: 耳机接口 报错类型: AudioFlinger could not create record track, status: -1 Androi

【雕爷学编程】Arduino动手做(175)---机智云ESP8266开发板模块7

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

FreeRTOS之互斥量

什么是互斥量&#xff1f; 在多数情况下&#xff0c;互斥型信号量和二值型信号量非常相似&#xff0c;但是从功能上二值型信号量用于同步&#xff0c; 而互斥型信号量用于资源保护。 互斥型信号量和二值型信号量还有一个最大的区别&#xff0c;互斥型信号量可以有效解决优先级…

Python(四十八)列表的特点

❤️ 专栏简介&#xff1a;本专栏记录了我个人从零开始学习Python编程的过程。在这个专栏中&#xff0c;我将分享我在学习Python的过程中的学习笔记、学习路线以及各个知识点。 ☀️ 专栏适用人群 &#xff1a;本专栏适用于希望学习Python编程的初学者和有一定编程基础的人。无…

基于SpringBoot+Vue的藏区特产销售平台设计与实现(源码+LW+部署文档等)

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

python调用百度ai将图片/pdf识别为表格excel

python调用百度ai将图片识别为表格excel 表格文字识别(异步接口)图片转excel 表格文字识别V2图片/pdf转excel通用 表格文字识别(异步接口) 图片转excel 百度ai官方文档&#xff1a;https://ai.baidu.com/ai-doc/OCR/Ik3h7y238 使用的是表格文字识别(异步接口)&#xff0c;同步…

安装taghighlight遇到的需要python问题

起因&#xff1a; 在vim里面使用taghighlight时&#xff0c;输入命令:UpdateTypesFile后出现下面的提示&#xff1a; 看了一下&#xff0c;系统里面有安装python3.10&#xff0c;为什么还提示要安装python2.6呢&#xff1f;开始以为是python3.10的版本与taghighlight不匹配&am…

Linux常用命令——dpkg-split命令

在线Linux命令查询工具 dpkg-split Debian Linux中将大软件包分割成小包 补充说明 dpkg-split命令用来将Debian Linux中的大软件包分割成小软件包&#xff0c;它还能够将已分割的文件进行合并。 语法 dpkg-split(选项)(参数)选项 -S&#xff1a;设置分割后的每个小文件最…

LeetCode[1302]层数最深叶子节点的和

难度&#xff1a;Medium 题目&#xff1a; 给你一棵二叉树的根节点 root &#xff0c;请你返回 层数最深的叶子节点的和 。 示例 1&#xff1a; 输入&#xff1a;root [1,2,3,4,5,null,6,7,null,null,null,null,8] 输出&#xff1a;15示例 2&#xff1a; 输入&#xff1a;r…

HTTP——二、简单的HTTP协议

本章将针对 HTTP 协议结构进行讲解&#xff0c;主要使用HTTP/1.1版本。学完这章&#xff0c;想必大家就能理解 HTTP 协议的基础了。 HTTP 一、HTTP协议用于客户端和服务器之间的通信二、通过请求和响应的交换达成通信三、HTTP是不保存状态的协议四、请求URI定位资源五、告知服…

GAMES101 笔记 Lecture12 Geometry3

目录 Mesh Operations: Geometry ProcessingMesh Subdivision (曲面细分)Mesh Simplification(曲面简化)Mesh Regularization(曲面正则化) Subdivision(细分)Loop Subdivision(Loop细分)如何来调整顶点位置呢&#xff1f;Loop Subdivision Result (Loop细分的结果) Catmull-Cla…

chatglm-6b量化推理指标记录

chatglm量化推理指标对比&#xff0c;单卡显存32G, 保持batchsize为64不变。通过不同的量化可以节省显存进而提升提升batch size&#xff0c;加快全量数据的推理速度。当然通过量化可以降低大模型的显存使用门槛。

Ae 效果:CC Toner

颜色校正/CC Toner Color Correction/CC Toner CC Toner&#xff08;CC 色调&#xff09;效果常用于对图像进行色调处理&#xff0c;可以实现双色调、三色调、五色调以及纯色的效果。 在某种程度上&#xff0c;与 Ae 自带的填充 Fill、色调 Tint以及三色调 Tritone等效果有相似…

[Linux]进程控制详解!!(创建、终止、等待、替换)

hello&#xff0c;大家好&#xff0c;这里是bang___bang_&#xff0c;在上两篇中我们讲解了进程的概念、状态和进程地址空间&#xff0c;本篇讲解进程的控制&#xff01;&#xff01;包含内容有进程创建、进程等待、进程替换、进程终止&#xff01;&#xff01; 附上前2篇文章…

37 coredump 的生成和使用

前言 呵呵 coredump 之前对于我而言也是一个挺陌生的概念 但是 只从开始了 linux 的相关学习之后, 对于这个 概念也有了一些 理解 呵呵 这里 以一些 简单的例子 来看一下 coredump 的生成和使用 首先执行 "ulimit -c unlimited" 测试用例1 - 除数为 0 root…

使用Kmeans算法完成聚类任务

聚类任务 聚类任务是一种无监督学习任务&#xff0c;其目的是将一组数据点划分成若干个类别或簇&#xff0c;使得同一个簇内的数据点之间的相似度尽可能高&#xff0c;而不同簇之间的相似度尽可能低。聚类算法可以帮助我们发现数据中的内在结构和模式&#xff0c;发现异常点和离…

FreeRTOS之二值信号量

什么是信号量&#xff1f; 信号量&#xff08;Semaphore&#xff09;&#xff0c;是在多任务环境下使用的一种机制&#xff0c;是可以用来保证两个或多个关键代 码段不被并发调用。 信号量这个名字&#xff0c;我们可以把它拆分来看&#xff0c;信号可以起到通知信号的作用&am…