synchronized从入门到踹门

news2024/11/18 14:47:06

synchronized是什么

synchronized是Java关键字,为了维护高并发是出现的原子性问题。技术是把双刃剑,多线程并发给我带来了前所未有的速率,然而在享受快速编程的过程,也给我们带来了原子性问题。

如下:

public class Main {
    private static int i = 0;
    public static void main(String[] args) throws InterruptedException {

        Main main = new Main();

        Thread a = new Thread(() -> {
            main.add10K();
        }, "A");  // 线程A

        Thread b = new Thread(() -> {
            main.add10K();
        }, "B");  // 线程B

        a.start();  // 启动线程A
        b.start();  // 启动线程B

        a.join();   // 等待线程A执行完毕
        b.join();   // 等待线程B执行完毕

        System.out.println(i); // 打印i的值,期望20000

    }
    // +10000操作
    public void add10K(){
        for (int j = 0; j < 10000; j++) {
            i++;
        }
    }
}

上面的程序,你细细品味一下结果会是多少?然后再回来看下面的结果。或者自己编程一下上面的代码,然后带着思考运行一下(面试高频点)。






下面的分析请耐心看,并思考。这就是面试要跟面试官聊的东西,聊越多,聊越细,证明你思考得越多。

答案是小于20000,其实了解过JVM的同学都知道,i++在CPU中其实不是一条CPU指令,而是三条。

  1. 读取i的值;

  1. 对i进行+1操作;

  1. 装载i的值。

那么多线程并发,其实就是每个线程分配一个时间片执行,时间片执行完毕后就轮到下一个线程。在上面的程序,可能会发生的事情:当线程A做到第2步的时候(对i进行+1操作),可能时间片得分给线程B了,此时线程A和线程B假设都读到i的值为0,这时线程B对i进行了+1操作后i的值为1,然后轮到线程A执行,线程A此时到了第三步,把刚才i+1的值装载回去(i=1)。问题就在这了,期望两个线程对i都+1后,期望值应该为2,然而此时却为1。这种情况还不少见,所以导致最终的结果小于期望值20000。

那么怎么解决这个问题呢?通过上面得分析我们知道,就是操作系统搞着时间片轮转运行造成的,不要轮转不就行了,确实可以。但是如果这么做了,又回到单线程时代,况且现在已经不是单核时代了,每个人得电脑至少双核起步吧,所以思路是对的,但是现实场景是骨感的。那么有没有一个可能,就是在线程A对i进行+1操作的时候,我把i这个参数给他锁住,先不要让别的线程操作它呢?这就对了,现在的synchronized、Lock就是这个思想,在操作某个变量时,我先在这个变量前面加个"栅栏"(也可以理解成锁),只有当我撤了这个栅栏(或者撤了这把锁),其他人才可以对这个变量进行操作,这不就没什么问题了。

synchronized其实就是利用这个原理做的这个关键字,但是它是隐式的,没有展现出来,但是其实在底层的"汇编指令",它其实是有展现的,带你们看一下。


这是Java代码

public class Main {
    public static void main(String[] args) throws InterruptedException {

    }

    public void operate(){
        synchronized(this){

        }
    }

}

这是"汇编指令",JVM自己约定的汇编指令,所以我加了双引号。(这个是通过:Javap -c Main.class指令得到的,大家有兴趣可以试试!)

可以看到上图,我画圈圈的东西,monitorenter、monitorexit、monitorexit,这其实就是synchronized的两个隐式"锁"指令了,monitorenter代表加锁,monitorexit代表解锁。为什么monitorexit有两个呢?原因其实也很简单,为了预防死锁用的,因为我们正常情况下当然是一个解锁就可以了,万一没运行到解锁那一行,程序挂了呢?那此时是不是在异常时设置一条解锁会好点?所以两个monitorexit是有道理的!


synchronized作用范围

锁非静态方法

public class Main {
    public static void main(String[] args) throws InterruptedException {

    }

    public synchronized void operate(){

    }

}

像上面的程序,锁的就是方法,这个方法是来源某个实例的,所以根据传递原则,其实锁的就是你new出来的那个实例,应该很好理解。下面来个例子,带你走走坑。

这个程序代码务必认真看,比你看100篇synchronized讲解有用!因为很多都是走马观花,没有落实到具体实践讲解,只让你知道锁的是实例,而实际场景中遇到的坑,你可能自己都理所当然,不知所以。


public class Main {
    public static void main(String[] args) throws InterruptedException {

        A a = new A();
        B b = new B();
        
        a.addMoney(b.money);
        
    }
    
}


class A {

    public Integer money = 100;
    
    public synchronized void addMoney(Integer targetMoney){
        money += targetMoney;
        System.out.println(money);
    }
    
}

class B {
    
    public Integer money = 200;

}

问题:假设在执行addMoney方法的时候,有其他线程修改了B的money为300,那么addMoney执行的结果是什么呢?

思考一下,可以评论区说一下答案+理解,这个真的很重要。这是synchronized最关键的点了,我先设个坑,评论区回答认真看的,因为真的很重要、很重要、很重要。


非静态代码块


public class Main {
    public static void main(String[] args) throws InterruptedException {

        A a = new A();
        B b = new B();

        a.addMoney(b.money);

    }

}


class A {

    public Integer money = 100;

    public void addMoney(Integer targetMoney){
        synchronized(this){
            money += targetMoney;
            System.out.println(money);
        }
    }

}

class B {

    public Integer money = 200;

}

跟锁非静态方法其实一样的,就是锁的实例,也存在上面的问题,所以说他真的很重要,笔试很容易就把分丢了,面试很容易就把印象说没了。


public class Main {
    public static void main(String[] args) throws InterruptedException {

        A a = new A();
        B b = new B();

        a.addMoney(b.money);

    }

}


class A {

    public Integer money = 100;

    public void addMoney(Integer targetMoney){
        synchronized(Main.class){
            money += targetMoney;
            System.out.println(money);
        }
    }

}

class B {

    public Integer money = 200;

}

这个就不一样了哦,我换成了Main.class,说明锁的是对象,那么有关该对象的变量和方法都会被锁住哦,其他形成访问该类的东西时,都会阻塞,等待该线程释放锁。


锁静态方法


public class Main {
    public static void main(String[] args) throws InterruptedException {

        A a = new A();
        B b = new B();

        a.addMoney(b.money);

    }

}


class A {

    public Integer money = 100;

    public static void addMoney(Integer targetMoney){
        synchronized(Main.class){
        }
    }

}

class B {

    public Integer money = 200;

}

这个跟锁静对象一样,锁的就是类,解释如上。


synchronized的优化

JDK1.6之后,JDK有对synchronized关键字进行了优化,主要是做了一些锁升级的过程:无锁--偏向锁--轻量级锁--重量级锁。

好好读下文,这个也很重要,不懂评论区留言,看到必回!

反向思考一下,加这个synchronized是为了干嘛?不就是为了当某个线程操作某个变量的时候,不然其他线程操作该变量吗?那就是阻塞咯。这个阻塞其实也就是我们上面一直讲解的重量级锁,确实一开始就是这样(JDK1.6之前)。那这很损耗性能的耶,所以搞JDK那群家伙就开始想办法优化这些思想了,我把synchronized做成一个动态化锁。


无锁

如果程序不会造成线程安全的,那我把synchronized去掉,变成无锁化。如下程序,只对i进行读操作,我锁它干嘛?


public class Main {
    public static void main(String[] args) {

        A a = new A();
        a.readI();

    }

}


class A {

    public Integer i = 100;

    public synchronized void readI(){
        System.out.println(i);

    }

}

看似有锁,其实我们从"汇编指令"看,已经被JDK偷偷优化成无锁了。


偏向锁

但是实际场景可不是一直读哦,也会有某个线程一直在那里频繁的写写写,但是也无所谓啦,以为就你这个线程是把,那我就在实例对象头那里,直接把偏向锁ID,设置成你这个线程ID就可以了,只要是你这个家伙来访问这个变量,我直接也把锁优化掉。

Idea开启偏向锁VM参数:-XX:+UseBiasedLocking,偏向锁开启后,默认是4秒才会生效


没有等4秒直接用,没使用到偏向锁(non-biasable)

public class Main {
    public static void main(String[] args) throws InterruptedException {

//        TimeUnit.SECONDS.sleep(5);

        A a = new A();

        new Thread(()->{
            a.writeI();
        }).start();


        // 打印一下加锁后的实例a的对象头信息
        System.out.println(ClassLayout.parseInstance(a).toPrintable());

    }

}


class A {

    public Integer i = 100;

    public synchronized void writeI(){
        i += 1;
    }

}

这里顺带说一下,对象头的打印是使用了ClassLayout工具类,可以在maven添加以下两个依赖使用

<dependencies>
    <!--查看对象头工具-->
    <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.9</version>
    </dependency>

    <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.16</version>
    </dependency>
</dependencies>

等待4秒,使用偏向锁,value为偏向锁ID

public class Main {
    public static void main(String[] args) throws InterruptedException {

        TimeUnit.SECONDS.sleep(5);

        A a = new A();

        new Thread(()->{
            a.writeI();
        }).start();


        // 打印一下加锁后的实例a的对象头信息
        System.out.println(ClassLayout.parseInstance(a).toPrintable());

    }

}


class A {

    public Integer i = 100;

    public synchronized void writeI(){
        i += 1;
    }

}

轻量级锁

偏向锁其实指的是一般都是某个线程进行变量操作,但是实际场景其实是有多个线程进行操作的,因此在其他线程检查操作的对象头不是自己的ID时,通过CAS尝试再次获取锁,获取不到则转变成轻量级锁,获取到了就还是偏向锁。


重量级锁

这个场景一般是高并发时,都是重量级,因为有多个线程同时操作同个共享资源。如果按锁的锁的升级流程,无非就是浪费时间。


以上则是synchronized的所有概述,欢迎共勉。

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

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

相关文章

【微服务】认识微服务

目录 1.1 单体、分布式、集群 单体 分布式 集群 1.2 系统架构演变 1.2.1 单体应⽤架构 1.2.2 垂直应⽤架构 1.2.3 分布式架构 1.2.4 SOA架构 1.2.5 微服务架构 1.3 微服务架构介绍 微服务架构的常⻅问题 1.4 SpringCloud介绍 1.4.1 SpringBoot和SpringCloud有啥关…

[1.3_2]计算机系统概述——中断和异常

文章目录第一章 计算机系统概述中断和异常&#xff08;一&#xff09;中断的作用&#xff08;二&#xff09;中断的类型&#xff08;三&#xff09;中断机制的基本原理小结第一章 计算机系统概述 中断和异常 中断的作用中断的类型 内中断&#xff08;也称“异常”&#xff09;…

ES之DSL查询文档基础查询

分类 query查询分类 总体规律就是逻辑性的&#xff0c;从外层的你干嘛&#xff0c;到下一层的查询类型&#xff0c;再到下一层的查询字段&#xff08;如果需要的话&#xff09;和然后是查询内容 查询所有 语法 get /索引库名/_serarch {"query":{"查询条件…

【Linux】配置动态IP

动态IP 服务器重启完成之后&#xff0c;我们可以通过linux的指令 ip addr 来查询Linux系统的IP地址&#xff0c;具体信息如 下: 从图中我们可以看到&#xff0c;并没有获取到linux系统的IP地址&#xff0c;这是为什么呢&#xff1f;这是由于启动服务器时未 加载网卡&#x…

2D图像处理:Qt + Opencv使用光度立体法检测Halcon中提供的缺陷图像

文章目录 不需知道光源方向一、光度立体法(后续有时间在查资料研究)1.1 问题1:Slants和Tilts的理解(暂时是理解的)1.2 问题1:Gradient通道数为1,为何像素点对应的值会有两个?1.3 问题2:F(r,c)=(u(r,c),v(r,c)) 关于高斯曲率和平均曲率如何计算的?二、非标定光源实现光…

C++经典20题型,满满知识,看这一篇就够了(含答案)

今天找了20道c的经典题型&#xff0c;看这一篇就够了&#xff0c;全是干货 目录 1、题目&#xff1a;有一对兔子&#xff0c;从出生后第3个月起每个月都生一对兔子&#xff0c;小兔子长到第三个月后每个月又生一对兔子&#xff0c;假如兔子都不死&#xff0c;问每个月的兔子总…

【蓝桥杯集训11】BFS(4 / 4)

目录 844. 走迷宫 - BFS求最短路 1233. 全球变暖 - BFS 845. 八数码 - 最短路BFS 状态表示 一二维坐标转换 为什么BFS保证走的是最短路&#xff1f; 一二维坐标转换&#xff08;nn矩阵&#xff09; 1562.微博转发 - BFS按层遍历 有向图 844. 走迷宫 - BFS求最短路 活…

Centos7安装中文字体

一、背景 最近一直在重写2021年毕设的前端页面&#xff0c;用vue3vite实现的响应式布局&#xff0c;目前完成10%。但在部署到Linux上时&#xff0c;遇到了服务端生成的中文验证码混乱的问题&#xff0c;通过远程断点&#xff0c;排除编码的问题&#xff0c;原来是由于Linux没有…

Linux网络编程 第六天

目录 学习目标 libevent介绍 libevent的安装 libevent库的使用 libevent的使用 libevent的地基-event_base 等待事件产生-循环等待event_loop 使用libevent库的步骤&#xff1a; 事件驱动-event 编写一个基于event实现的tcp服务器&#xff1a; 自带buffer的事件-buff…

深圳大学计软《面向对象的程序设计》实验14 运算符重载2和函数模板

A. 日期比较&#xff08;运算符重载之类型转换&#xff09; 题目描述 定义一个日期类CDate&#xff0c;包含属性&#xff1a;年、月、日&#xff0c;都是整数。 构造函数含单个参数&#xff0c;参数是八位整数&#xff0c;默认值为0&#xff0c;实现整数转为日期类型&#x…

【基于感知损失的无监督泛锐化】

PercepPan: Towards Unsupervised Pan-Sharpening Based on Perceptual Loss &#xff08;PercepPan&#xff1a;基于感知损失的无监督泛锐化&#xff09; 在基于神经网络的全色锐化文献中&#xff0c;作为地面实况标签的高分辨率多光谱图像通常是不可用的。为了解决这个问题…

C++初学笔记整理

目录 1. C关键字 2. 命名空间 1&#xff09;命名空间的引入和概述 2&#xff09;命名空间的定义 3&#xff09;std与命名空间的使用 4).相关特性 3. C输入&输出 4. 缺省参数 1 &#xff09;缺省参数概念 2&#xff09;使用及分类 a.全缺省 b.部分缺省 5. 函数…

力扣-337打家劫舍III(dp)

力扣-337打家劫舍III 1、题目 337. 打家劫舍 III 小偷又发现了一个新的可行窃的地区。这个地区只有一个入口&#xff0c;我们称之为 root 。 除了 root 之外&#xff0c;每栋房子有且只有一个“父“房子与之相连。一番侦察之后&#xff0c;聪明的小偷意识到“这个地方的所有…

【FMCW 01】中频IF信号

FMCW信号 调频连续波(frequency modulated continuous wave&#xff0c;FMCW)顾名思义&#xff0c;就是对信号的频率进行线性调制的信号。 从时域上看&#xff0c;对频率的调制&#xff0c;就像一把连续的锯齿波。其中每一个锯齿叫做一个chirp&#xff0c;其持续的时间叫做ch…

Android仿微信选择图片

效果展示首先先添加用到的权限<uses-permission android:name"android.permission.INTERNET" /><!--获取手机存储卡权限--><uses-permission android:name"android.permission.READ_EXTERNAL_STORAGE"/><uses-permission android:nam…

java 包装类 万字详解(通俗易懂)

前言简介和溯源拆装箱String类和基本类型的相互转化String类和包装类型的相互转化八大包装类的常用方法汇总&#xff08;含代码演示&#xff09;一、前言 : 本节内容是我们《API-常用类》专题的最后一节了。本节内容主要讲包装类&#xff0c;内容包括但不限于包装类的诞生&…

Linux磁盘占满,如何清理磁盘空间

目录解决思路&#xff1a;先查linux磁盘为什么占满了&#xff0c;是什么导致的&#xff0c;这样才好去定位目录&#xff0c;清楚空间写者的问题是测试环境磁盘占满&#xff0c;原因是测试环境tomcat的日志都会保留&#xff0c;日志空间占用太大把linux磁盘占满&#xff0c;导致…

HTML 表单

HTML 表单和输入 HTML 表单用于收集不同类型的用户输入。 在线实例 创建文本字段 (Text field) 本例演示如何在 HTML 页面创建文本域。用户可以在文本域中写入文本。 创建密码字段 本例演示如何创建 HTML 的密码域。 &#xff08;在本页底端可以找到更多实例。&#xff09; …

信息安全与数学基础-笔记-①整数的可除性

知识目录整除素数带余除法最大公因数&#xff08;欧几里德算法&#xff09;裴蜀等式最小公倍数❀标准分解式❀标准分解式求最大公因数标准分解式求最小公倍数整除 a bq 公式表达的意思&#xff1a;b整除a&#xff0c;a可被b整除 用符号表示&#xff1a;b | a 否则&#xff1a…

【SpringMVC】SpringMVC程序开发

SpringMVC程序开发 文章目录SpringMVC程序开发:one:认识SpringMVC什么是SpringMVCMVC思想:two:获取请求参数创建SpringMVC项目建立路由连接获取请求参数获取urlEncoded格式参数获取表单标签参数获取Json格式参数获取URL中的参数上传图片获取请求信息获取请求header获取cookie创…