多线程锁.

公平锁与非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的Lock lock= new Reentrantlock(true);/true表示公平锁,先来先得
非公平锁指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)
Lock lock= new Reentrantlock(fase);/false表示非公平锁,后来的也可能先获得锁
Lock lock= new Reentrantlock(); //默认非公平锁

公平锁与非公平锁设计原因

  1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
  2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

公平锁与非公平锁设计的选择

如果为了更高的吞吐量,限显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。

可重入锁(又名递归锁)

可重入锁又名递归锁,是指在同一个线程的外层方法获取锁的时候,再次进入该线程的内层方法会自动获取锁(前提:锁对象是同一个对象),不会因为之前已经获取过还没有释放而阻塞。ReentrantLock和synchronized都是可重入锁。

synchronized可重入实现原理

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。当执行 monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。当执行 monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁己被释放。

public class ReEntryLockDemo
{public synchronized void m1(){//指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。System.out.println(Thread.currentThread().getName()+"\t ----come in");m2();System.out.println(Thread.currentThread().getName()+"\t ----end m1");}public synchronized void m2(){System.out.println(Thread.currentThread().getName()+"\t ----come in");m3();}public synchronized void m3(){System.out.println(Thread.currentThread().getName()+"\t ----come in");}static Lock lock = new ReentrantLock();public static void main(String[] args){/*ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();new Thread(() -> {reEntryLockDemo.m1();},"t1").start();*/new Thread(() -> {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t ----come in内层调用");}finally {lock.unlock();}}finally {// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。//lock.unlock();// 正常情况,加锁几次就要解锁几次}},"t1").start();new Thread(() -> {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");}finally {lock.unlock();}},"t2").start();}private static void reEntryM1(){final Object object = new Object();new Thread(() -> {synchronized (object){System.out.println(Thread.currentThread().getName()+"\t ----外层调用");synchronized (object){System.out.println(Thread.currentThread().getName()+"\t ----中层调用");synchronized (object){System.out.println(Thread.currentThread().getName()+"\t ----内层调用");}}}},"t1").start();}
}

自旋锁与自适应自旋

如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

自旋锁在JDK 1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK 6中就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有价值的工作,这就会带来性能的浪费。因此自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。自旋次数的默认值是十次,用户也可以使用参数-XX:PreBlockSpin来自行更改。不过无论是默认值还是用户指定的自旋次数,对整个Java虚拟机中所有的锁来说都是相同的。在JDK 6中对自旋锁的优化,引入了自适应的自旋。自适应意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋等待持续相对更长的时间,比如持续100次忙循环。另一方面,如果对于某个锁,自旋很少成功获得过锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程,以避免浪费处理器资源。

锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。也许读者会有疑问,变量是否逃逸,对于虚拟机来说是需要使用复杂的过程间分析才能确定的,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下还要求同步呢?这个问题的答案是:有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中出现的频繁程度也许超过了大部分读者的想象。我们来看看如下代码所示的例子,这段非常简单的代码仅仅是输出三个字符串相加的结果,无论是源代码字面上,还是程序语义上都没有进行同步。

public String concatString(String s1, String s2, String s3) {return s1 + s2 + s3;
}

我们也知道,由于String是一个不可变的类,对字符串的连接操作总是通过生成新的String对象来进行的,因此Javac编译器会对String连接做自动优化。在JDK 5之前,字符串加法会转化为StringBuffer对象的连续append()操作,在JDK 5及以后的版本中,会转化为StringBuilder对象的连续append()操作。即代码清所示的代码可能会变成下面代码所示的样子。

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();
}

现在大家还认为这段代码没有涉及同步吗?每个StringBuffer.append()方法中都有一个同步块,锁就是sb对象。虚拟机观察变量sb,经过逃逸分析后会发现它的动态作用域被限制在concatString()方法内部。也就是sb的所有引用都永远不会逃逸到concatString()方法之外,其他线程无法访问到它,所以这里虽然有锁,但是可以被安全地消除掉。在解释执行时这里仍然会加锁,但在经过服务端编译器的即时编译之后,这段代码就会忽略所有的同步措施而直接执行。

锁粗化

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变少,即使存在锁竞争,等待锁的线程也能尽可能快地拿到锁。
大多数情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。上面锁消除所示连续的append()方法就属于这类情况。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,以上面代码例,就是扩展到第一个append()操作之前直至最后一个append()操作之后,这样只需要加锁一次就可以了。

偏向锁

当一个线程访问同步块并获取锁时 ,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进 入和退出同步块时不需要 进 行 CAS操作来加 锁 和解 锁 ,只需 简单地测试 一下 对 象头 的 Mark Word里是否存储着指向当前线程的偏向锁 。如果测试成功,表示线程已经获得了锁 。如果 测试失败则需要再 测试 一下 Mark Word中偏向锁的标识是否 设 置成 1(表示当前是偏向 锁 ):如果没有设置, 则 使用 CAS竞 争 锁 ;如果设置了, 则尝试使用CAS将对 象头的偏向锁 指向当前线 程。

偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

关闭偏向锁

偏向锁在Java 6和 Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用 JVM参数来关闭延迟: :-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过 JVM参数关闭偏向锁:-XX:- UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

轻量级锁

轻量级锁加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的 Mark Word复制到锁记录中,官方称为 Displaced Mark Word。然后线程尝试使用 CAS将对象头中的 Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁 。

轻量级锁解锁

轻量级解锁时,会使用原子的CAS操作将 Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。因为 自旋会消耗 CPU 为 了避免无用的自旋(比如 获 得 锁 的 线 程被阻塞住了),一旦 锁 升 级 成重量 级锁 ,就不会再恢复到 轻 量 级锁 状 态 。当 锁处 于 这 个状 态 下,其他 线 程试图获 取 锁时 ,都会被阻塞住,当持有 锁 的 线 程 释 放 锁 之后会 唤 醒 这 些 线 程,被 唤 醒的线 程就会 进 行新一 轮 的 夺锁 之争。image.png

死锁及排查

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,如果系统资源从中,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

产生死锁的原因

  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当

产生死锁的条件

  • 互斥条件:一个资源每次只能被一个线程使用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

死锁的解决办法

如果发生了死锁,那么只要破坏死锁 4 个必要条件之一中的任何一个,死锁问题就能被解决。下面就是对于上面这些必要条件所作出的推想

  1. 如果想要打破互斥条件,我们需要允许进程同时访问某些资源,这种方法受制于实际场景,不太容易实现条件;
  2. 打破不可抢占条件,这样需要允许进程强行从占有者那里夺取某些资源,或者简单一点理解,占有资源的进程不能再申请占有其他资源,必须释放手上的资源之后才能发起申请,这个其实也很难找到适用场景;
  3. 进程在运行前申请得到所有的资源,否则该进程不能进入准备执行状态。这个方法看似有点用处,但是它的缺点是可能导致资源利用率和进程并发性降低;
  4. 避免出现资源申请环路,即对资源事先分类编号,按号分配。这种方式可以有效提高资源的利用率和系统吞吐量,但是增加了系统开销,增大了进程对资源的占用时间。

如果我们在死锁检查时发现了死锁情况,那么就要努力消除死锁,使系统从死锁状态中恢复过来。消除死锁的几种方式:

  1. 最简单、最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程;
  2. 撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁。这时又分两种情况:一次性撤消参与死锁的全部进程,剥夺全部资源;或者逐步撤消参与死锁的进程,逐步收回死锁进程占有的资源。一般来说,选择逐步撤消的进程时要按照一定的原则进行,目的是撤消那些代价最小的进程,比如按进程的优先级确定进程的代价;考虑进程运行时的代价和与此进程相关的外部作业的代价等因素;
  3. 进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。虽然这是个较理想的办法,但是操作起来系统开销极大,要有堆栈这样的机构记录进程的每一步变化,以便今后的回退,有时这是无法做到的。

如何排查死锁

纯命令jstack

image.png

jconsle

image.png

小总结

锁的优缺点对比

image.png

锁的选择

  1. 偏向锁:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
  2. 轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似), 存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。
  3. 重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁

image.png
image.png

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

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

相关文章

类和对象 (中)

文章目录 类的六个默认成员函数构造函数析构函数特性使用 总结构造函数和析构函数拷贝构造函数特性拷贝构造总结 赋值运算符的重载运算符重载赋值运算符重载总结拷贝构造函数和赋值运算符重载 关于operator<<重载日期类实现const 修饰的成员函数取地址重载以及const取地址…

css实现高度是宽度一半的效果

1、方法一&#xff1a;使用变量:root、var()、clac()实现&#xff1a; 1.1 效果如下&#xff1a; 2.2 代码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title>&l…

伸手党必备之Python正则表达式常用函数

今天介绍一下Python中常用的正则表达式处理函数。Python的正则表达式主要有两种方法完成模式匹配&#xff1a;『搜索』和『匹配』 re.match re.match 尝试从字符串的开始全部或者部分匹配某个模式&#xff0c;如&#xff1a;下面的例子匹配第一个单词。 import re text &…

springboot269反欺诈平台的建设

反欺诈平台设计与实现 摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装反欺诈平台软件来发挥其高效地信息处…

Vue-Vben-Admin:中大型项目后台解决方案及如何实现页面反向传值

Vue-Vben-Admin&#xff1a;中大型项目后台解决方案及如何实现页面反向传值 摘要&#xff1a; Vue-Vben-Admin是一个基于Vue3.0、Vite、Ant-Design-Vue和TypeScript的开源项目&#xff0c;旨在为开发中大型项目提供一站式的解决方案。它涵盖了组件封装、实用工具、钩子函数、动…

学习c语言:单链表的应用

一、单链表经典算法 1.1 单链表相关经典算法OJ题1&#xff1a;移除链表元素 . - 力扣&#xff08;LeetCode&#xff09;. - 备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.…

WPF监控平台(科技大屏)[一]

跟着B站的视频敲了一个略微复杂的WPF界面,链接如下.在这里我详细的写一份博客进行设计总结. 系统介绍和配置及主窗口设计_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Wy421Y7QD?p1&vd_source4796b18a2e4c1ec8a310391a5644b6da 成果展示 实现过程 总体来说,我的…

OpenHarmony开源项目—工程管理

DevEco Studio的基本使用&#xff0c;请参考DevEco Studio使用指南。本章主要介绍如何使用DevEco Studio进行多设备应用开发。 说明&#xff1a; 本章的内容基于DevEco Studio 3.1.1 Release版本进行介绍&#xff0c;如您使用DevEco Studio其它版本&#xff0c;可能存在文档与产…

微信小程序(五十八)分步表单多页面传值

注释很详细&#xff0c;直接上代码 新增内容&#xff1a; 1.分步表单传值 2.伪数据生成 源码&#xff1a; app.json {"pages": ["pages/index/index","pages/building/building","pages/room/room","pages/logs/logs"],&qu…

关于类和对象超级初级小白知识

下面的内容只是一小部分&#xff0c;在整个面向对象的知识中并不完整&#xff0c;用于记忆和梳理 目录 前言&#xff1a;类和对象是什么&#xff1f; 一、定义类 1.如何定义类 2.类的注意事项 二.类的实例化 1.创建对象的基础知识 2.如何创建对象 3.实例化举例 4.访问对象…

【Python】python实现决策树算法和贝叶斯算法(附源代码)

使用一种你熟悉的程序设计语言&#xff0c;实现&#xff08;1&#xff09;贝叶斯算法和&#xff08;2&#xff09;决策树算法 目录 1、贝叶斯算法2、决策树算法3、两种算法比较 1、贝叶斯算法 import pandas as pd import numpy as np from sklearn.model_selection import t…

算法-DFS/BFS-XMUOJ提瓦特细胞探秘

题目 思路 这道题目描述了一个矩阵中由数字1至9组成的细胞&#xff0c;这些细胞按照特定规则相互连接。具体规则是&#xff0c;只有当细胞上下左右紧邻的数字同样属于1至9时&#xff0c;它们才会被视为同一细胞的一部分。 解决这个问题的关键在于如何遍历整个矩阵&#xff0c…

灵雀云开源ALB ,为云原生 Kubernetes 网关领域再添新星

ALB (Another Load Balancer)是一款由灵雀云倾力打造的云原生 Kubernetes 网关&#xff0c;近日&#xff0c;正式宣布将其在Github上开源。ALB 的诞生&#xff0c;旨在满足日益增长的云原生应用需求&#xff0c;为开发者提供更为强大和灵活的网关解决方案。 ALB 支持在一个集群…

Oracle PL/SQL Programming 第9章:Numbers 读书笔记

总的目录和进度&#xff0c;请参见开始读 Oracle PL/SQL Programming 第6版 本章谈3点&#xff1a; 可使用的数字数据类型如何在数字和文本间转换PL/SQL 内置数值函数 Numeric Datatypes NUMBER&#xff1a;平台无关的实现&#xff0c;适合处理货币金额PLS_INTEGER 和 BINA…

前端面试题汇总

基础面试题 1.new 操作符做了那些事 function Fun(name){this.name name } Fun.prototype.sayHi funtion(){console.log(this.name) }function mockCreate(fn, ...args){let obj Object.create({}) // 创建一个空对象Object.setPrototypeOf(obj, fn.prototype) // 空对象…

day02vue学习

day02 一、今日学习目标 1.指令补充 指令修饰符v-bind对样式增强的操作v-model应用于其他表单元素 2.computed计算属性 基础语法计算属性vs方法计算属性的完整写法成绩案例 3.watch侦听器 基础写法完整写法 4.综合案例 &#xff08;演示&#xff09; 渲染 / 删除 / 修…

速卖通批量注册买家号安全吗?怎么弄?

在速卖通等跨境电商平台上&#xff0c;买家号的注册与养号过程繁琐而复杂。传统的手动注册方式效率低下&#xff0c;难以满足大规模的需求。而跨境智星系统凭借其全自动化的功能&#xff0c;能够轻松实现买家号的批量注册与养号&#xff0c;大大提高了效率。 使用跨境智星系统进…

200W年薪的大佬 随手丢给我“Spring速成宝典”看完这些知识点直接定级P7

面试官&#xff1a;答的很好&#xff0c;最后一个问题&#xff1a;如果没有Spring&#xff0c;你打算怎么开展工作&#xff1f; 这个思考了几分钟后&#xff0c;程序员小建是这么回答的&#xff1a;Spring的核心源码&#xff0c;比如&#xff1a;IOC、AOP、Spring事务、MVC原理…

Explain详解与索引优化最佳实践

Explain工具介绍 使用EXPLAIN关键字可以模拟优化器执行SQL语句,分析你的查询语句或是结构的性能瓶颈 在select语句之前增加explain关键字,MySQL会在查询前设置一个标记,执行查询会返回执行计划的信息,而不是执行这条SQL 注意: 如果from中包含子查询,仍会执行该子查询,将结果…

DAY by DAY 史上最全的Linux常用命令汇总----man

man是按照手册的章节号的顺序进行搜索的。 man设置了如下的功能键&#xff1a; 功能键 功能 空格键 显示手册页的下一屏 Enter键 一次滚动手册页的一行 b 回滚一屏 f 前滚一屏 q 退出man命令 h 列出所有功能键 /word 搜索word字符串 注意&#xff1a…