Skip to main content

moregeek program

浅谈自旋锁和 jvm 对锁的优化_crmeb中邦科技的博客-多极客编程

背景

先上图

浅谈自旋锁和 JVM 对锁的优化_加锁

由此可见,非自旋锁如果拿不到锁会把线程阻塞,直到被唤醒;自旋锁拿不到锁会一直尝试

为什么要这样?

好处

阻塞和唤醒线程都是需要高昂的开销的,如果同步代码块中的内容不复杂,那么可能转换线程带来的开销比实际业务代码执行的开销还要大。

在很多场景下,可能我们的同步代码块的内容并不多,所以需要的执行时间也很短,如果我们仅仅为了这点时间就去切换线程状态,那么其实不如让线程不切换状态,而是让它自旋地尝试获取锁,等待其他线程释放锁,有时我只需要稍等一下,就可以避免上下文切换等开销,提高了效率。

用一句话总结自旋锁的好处,那就是自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。

AtomicLong 的实现

getAndIncrement 方法

public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
复制代码
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
v = getLongVolatile(o, offset);
//如果修改过程中遇到其他线程竞争导致没修改成功,死循环,直到修改成功为止
} while (!compareAndSwapLong(o, offset, v, v + delta));
return v;
}
复制代码

实验

package com.reflect;

import java.util.concurrent.atomic.AtomicReference;

class ReentrantSpinLock {
private AtomicReference<Thread> owner = new AtomicReference<>();
private int count = 0;

public void lock() {
Thread t = Thread.currentThread();
if (t == owner.get()) {
++count;
return;
}
while (!owner.compareAndSet(null, t)) {
System.out.println("自旋了");
}
}

public void unlock() {
Thread t = Thread.currentThread();
if (t == owner.get()) {
if (count > 0) {
--count;
} else {
owner.set(null);
}
}
}

public static void main(String[] args) {
ReentrantSpinLock spinLock = new ReentrantSpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始尝试获 取自旋锁");
spinLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获取到 了自旋锁");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了 了自旋锁");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
复制代码

很多 "自旋了",说明自旋期间 CPU 依然在不停运转

缺点

虽然避免了线程切换的开销,但是在避免线程切换开销的同时带来新的开销:不停尝试获取锁,如果这个锁一直不能被释放那么这种尝试知识无用的尝试,浪费处理器资源,就是说一开始自旋锁开销低于线程切换,但是随着时间增加,这种开销后期甚至超过线程切换的开销,得不偿失

适用场景

  • 并发不是特别高的场景
  • 临界区比较短小的情况,利用避免线程切换提高效率

如果临界区很大,线程拿到锁很久才释放,那自旋会一直占用 CPU 但无法拿到锁,浪费资源

JVM 对锁做了哪些优化?

相比于 JDK 1.5,在 JDK 1.6 中 HotSopt 虚拟机对 synchronized 内置锁的性能进行了很多优化,包括自适应的自旋、锁消除、锁粗化、偏向锁、轻量级锁等。有了这些优化措施后,synchronized 锁的性能得到了大幅提高,下面我们分别介绍这些具体的优化。

自适应的自旋锁

在 JDK 1.6 中引入了自适应的自旋锁来解决长时间自旋的问题。自适应意味着自旋的时间不再固定,而是会根据最近自旋尝试的成功率、失败率,以及当前锁的拥有者的状态等多种因素来共同决定。自旋的持续时间是变化的,自旋锁变 “聪明” 了。比如,如果最近尝试自旋获取某一把锁成功了,那么下一次可能还会继续使用自旋,并且允许自旋更长的时间;但是如果最近自旋获取某一把锁失败了,那么可能会省略掉自旋的过程,以便减少无用的自旋,提高效率。

锁消除

public class Person {
private String name;
private int age;

public Person(String personName, int personAge) {
name = personName;
age = personAge;
}

public Person(Person p) {
this(p.getName(), p.getAge());
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}

class Employee {
private Person person;

public Person getPerson() {
return new Person(person);
}

public void printEmployeeDetail(Employee emp) {
Person person = emp.getPerson();
System.out.println("Employee's name: " + person.getName() + "; age: " + person.getAge());
}
}
复制代码

在这段代码中,我们看到下方的 Employee 类中的 getPerson () 方法,这个方法中使用了类里面的 person 对象,并且新建一个和它属性完全相同的新的 person 对象,目的是防止方法调用者修改原来的 person 对象。但是在这个例子中,其实是没有任何必要新建对象的,因为我们的 printEmployeeDetail () 方法没有对这个对象做出任何的修改,仅仅是打印,既然如此,我们其实可以直接打印最开始的 person 对象,而无须新建一个新的。

如果编译器可以确定最开始的 person 对象不会被修改的话,它可能会优化并且消除这个新建 person 的过程。根据这样的思想,接下来我们就来举一个锁消除的例子,,经过逃逸分析之后,如果发现某些对象不可能被其他线程访问到,那么就可以把它们当成栈上数据,栈上数据由于只有本线程可以访问,自然是线程安全的,也就无需加锁,所以会把这样的锁给自动去除掉。

例如,我们的 StringBuffffer 的 append 方法如下所示:

@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
复制代码

从代码中可以看出,这个方法是被 synchronized 修饰的同步方法,因为它可能会被多个线程同时使用。

但是在大多数情况下,它只会在一个线程内被使用,如果编译器能确定这个 StringBuffffer 对象只会在一个线程内被使用,就代表肯定是线程安全的,那么我们的编译器便会做出优化,把对应的 synchronized 给消除,省去加锁和解锁的操作,以便增加整体的效率。

锁粗化

释放了锁,紧接着什么都没做,又重新获取锁

public void lockCoarsening() { 
synchronized (this) {
}
synchronized (this) {
}
synchronized (this) {
}
}
复制代码

那么其实这种释放和重新获取锁是完全没有必要的,如果我们把同步区域扩大,也就是只在最开始加一次锁,并且在最后直接解锁,那么就可以把中间这些无意义的解锁和加锁的过程消除,相当于是把几个 synchronized 块合并为一个较大的同步块。这样做的好处在于在线程执行这些代码时,就无须频繁申请与释放锁了,这样就减少了性能开销。

不过,我们这样做也有一个副作用,那就是我们会让同步区域变大。如果在循环中我们也这样做,如代码所示:

for (int i = 0; i < 1000; i++) { 
synchronized (this) {
}
}
复制代码

也就是我们在第一次循环的开始,就开始扩大同步区域并持有锁,直到最后一次循环结束,才结束同步代码块释放锁的话,这就会导致其他线程长时间无法获得锁。所以,这里的锁粗化不适用于循环的场景,仅适用于非循环的场景。

锁粗化功能是默认打开的,用 -XX:-EliminateLocks 可以关闭该功能

偏向锁 / 轻量级锁 / 重量级锁

这三种锁是特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态

  • 偏向锁

对于偏向锁而言,它的思想是如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只要打个标记就行了。一个对象在被初始化后,如果还没有任何线程来获取它的锁时,它就是可偏向的,当有第一个线程来访问它尝试获取锁的时候,它就记录下来这个线程,如果后面尝试获取锁的线程正是这个偏向锁的拥有者,就可以直接获取锁,开销很小。

  • 轻量级锁

JVM 的开发者发现在很多情况下,synchronized 中的代码块是被多个线程交替执行的,也就是说,并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决。这种情况下,重量级锁是没必要的。轻量级锁指当锁原来是偏向锁的时候,被另一个线程所访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的方式尝试获取锁,不会阻塞

  • 重量级锁

这种锁利用操作系统的同步机制实现,所以开销比较大。当多个线程直接有实际竞争,并且锁竞争时间比较长的时候,此时偏向锁和轻量级锁都不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。

锁升级

偏向锁性能最好,避免了 CAS 操作。而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差。

浅谈自旋锁和 JVM 对锁的优化_加锁_02

JVM 默认会优先使用偏向锁,如果有必要的话才逐步升级,这大幅提高了锁的性能


完整附件: ​点击此处下载附件​

©著作权归作者所有:来自51CTO博客作者CRMEB众邦科技的原创作品,请联系作者获取转载授权,否则将追究法律责任

swoole 进程模型分析_crmeb中邦科技的博客-多极客编程

在这边文章中我们将介绍以下内容:1、Swoole Server 的运行模式2、Swoole 进程模型分析swoole 进程上图是 Swoole 官网提供的各个进程相互关系图,可以说理解了这张图,你就理解了 Swoole 的进程模型。1、Swoole Server 的运行模式​​Swoole 服务​​常见的运行模式有单线程模式和进程模式两种,两种方式介绍如下:单线程模式 (SWOOLE_BASE) 

nginx 实现高并发的原理分析_crmeb中邦科技的博客-多极客编程

本文将讲解一下内容:1、Nginx 的进程模型分析2、Nginx 实现高并发原理分析这篇文章首先会讲解一下 Nginx 的进程模型,只有先理解了 Nginx 进程模型,才能深入理解 Nginx 实现高并发的原理。1、Nginx 进程模型介绍Nginx 的进程模型也是采用 ​​Master/Worker 形式​​。当 Nginx 启动时,会先创建一个 Master 进程,Master 进程会 for

php 运行方式详解_crmeb中邦科技的博客-多极客编程

1、​​CGI 协议模式​​ CGI 的含义是通用网关协议(Common Gateway Interface),它允许 web 服务器通过特定的协议与应用程序通信,调用原理如下:用户请求  -> Web 服务器接收请求  -> fork 子进程 调用程序 / 执行程序  -> 程序返回内容 / 程序调用结束  -> Web 服务器接收内容 -> 返回给用户由于每次用户

基于 swoole 搭建 websocket 服务详解_crmeb中邦科技的博客-多极客编程

本节将会详解以下 4 个问题:什么是 swoole?什么是 Websocket?如何基于 Swoole 构建 WebSocket 服务?基于 Swoole 的 WebSocket 服务和 Http 服务是什么关系?一、 Swoole 简介Swoole 是一个面向生产环境的 PHP 异步网络通信引擎,使 PHP 开发人员能够编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP 和

vue 组件间的通信方式_crmeb中邦科技的博客-多极客编程

前言在Vue组件库的开发过程中,组件之间的通信一直是一个重要的课题。虽然官方的Vuex状态管理方案可以很好的解决组件之间的通信问题,但是组件库内部对Vuex的使用往往比较繁重。本文列举了几种实用的不使用Vuex的组件间通信方法,供大家参考。组件之间通信的场景在进入我们今天的主题之前,我们先来总结下 Vue 组件之间通信的几种场景,一般可以分为如下几种场景:父子组件之间的通信兄弟组件之间的通信隔代组

swoole 的异步 task 任务详解_crmeb中邦科技的博客-多极客编程

本文将从下面两方面讲述 Swoole Task 任务:1. 如何在 Swoole 中实现异步 Task 任务?2.Swoole 的异步 Task 任务在 CRMEB 电商系统中的使用场景有哪些?一、如何在 Swoole 中实现异步 Task 任务?如果一些耗时的操作要在服务器端程序中执行 (例如,在 Web 服务器中发送电子邮件和短消息等。),如果直接按顺序执行这些操作,程序会阻塞当前进程,导致服

#yyds干货盘点# 前端歌谣的刷题之路-第八十九题-生成页码_前端歌谣的博客-多极客编程

前言我是歌谣 我有个兄弟 巅峰的时候排名c站总榜19 叫前端小歌谣 曾经我花了三年的时间创作了他 现在我要用五年的时间超越他 今天又是接近兄弟的一天人生难免坎坷 大不了从头再来 歌谣的意志是永恒的 放弃很容易 但是坚持一定很酷 本题目源自于牛客网 微信公众号前端小歌谣题目请补全JavaScript代码,要求将字符串参数URL中的参数解析并以对象的形式返回。示例1输入:getParams('http

c4d搭配椭圆动态及闪动控制动画效果_wx612751f2ed44d的博客-多极客编程

前言上一章讲述了利用 C4D 图切割制作闪光效果,本章将讲述如何在上一章描述的效果基础上,加入椭圆动态效果以及闪动控制。如下图所示,椭圆运动主要分成两部分:外圈运动、内圈运动。其中内圈运动看似一个椭圆,实际上是由两个椭圆不同角度组成的。红色指向的是运动的小球,绿箭头是小球运动的方向。下面将讲述如何展示圆球运动,主要是运用 canvas 画布制作,探测小球的运动轨迹,以及到点则控制对应块的闪烁。一、

#yyds干货盘点#css开发技巧总结_文本、的博客-多极客编程

使用pointer-events禁用事件触发要点:通过​​pointer-events:none​​​禁用事件触发(默认事件、冒泡事件、鼠标事件、键盘事件等),相当于​​<button>​​​的​​disabled​​场景:限时点击按钮(发送验证码倒计时)、事件冒泡禁用(多个元素重叠且自带事件、a标签跳转)例子:pointer-events: none;使用writing-mode排版

对 node.js 事件驱动模型的深入理解_crmeb中邦科技的博客-多极客编程

本文主要讨论以下问题:1.Node.js 的事件驱动模型分析2.Node.js 如何处理高并发请求?3.Node.js 的缺点介绍先简单介绍一下 Node.js,Node.js 是基于事件驱动、非阻塞 I/O 模型的服务器端 JavaScript 运行环境,是基于 Google 的 V8 引擎在服务器端运行的单线程、高性能的 JavaScript 语言。一、Node.js 事件驱动模型分析 看懂上

#yyds干货盘点#uniapp通过命令行打包_文本、的博客-多极客编程

如果有需要,就按照步骤来实现npm install -g cross-env之后就可以用各种命令了# 切换node版本(不一定需要)nvm use v16.2.0# 进入HBuild的cli目录# uni-app打包相关命令都封装在cli里面了cd /Applications/HBuilderX.app/Contents/HBuilderX/plugins/uniapp-cli/# 指定项目根地址

vue3.2前端开发系列一(环境搭建)_suooc博客的博客-多极客编程

1-nodejs安装下载地址:​​https://nodejs.org/dist/v16.17.0/node-v16.17.0-x64.msi​​安装路径建议 D:/nodejs/2-VSCode安装下载地址:​​https://az764295.vo.msecnd.net/stable/74b1f979648cc44d385a2286793c226e611f59e7/VSCodeUserSetu