深入学习锁--Synchronized各种使用方法

news2024/11/15 12:51:58

一、什么是synchronized

在Java当中synchronized通常是用来标记一个方法或者代码块。在Java当中被synchronized标记的代码或者方法在同一个时刻只能够有一个线程执行被synchronized修饰的方法或者代码块。因此被synchronized修饰的方法或者代码块不会出现数据竞争的情况,也就是说被synchronized修饰的代码块是并发安全的。synchronized是java内置关键字,是内置锁,JVM中内置了,颗粒度比较大

二、synchronized的四种用法

2.1、修饰一个代码块

被修饰的代码块称为同步语句块,其作用的范围是大括号{} 括起来的代码,作用的对象是调用这个代码块的对象;

2.2、修饰一个方法

 被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

非静态方法使用 synchronized 修饰的写法,修饰实例方法时,锁定的是当前实例对象

2.3、修饰一个静态的方法

其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

2.4、修饰一个类

其作用的范围是synchronized后面括号括起来的部分,作用对象是这个类的所有对象

三、使用案例分析

3.1、修饰一个方法

class SyncDemo {
    private int count;

    public void add() {
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        SyncDemo syncDemo = new SyncDemo();
        Thread t1 = new Thread(()->{
            for (int i = 0; i <1000 ; i++) {
                syncDemo.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i <1000 ; i++) {
                syncDemo.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();  //线程的阻塞方法,线程t1执行完毕,再执行main主线程打印语句
        t2.join();  //线程的阻塞方法,线程t2执行完毕,再执行main主线程打印语句
        System.out.println(syncDemo.count);
    }
}

由于add方法没有使用synchronized修饰,线程t1和线程t2可能同时去执行add方法,那么就会导致最终count的结果小于20000,因为count++操作不具备原子性。 

将上述add方法被synchronized修饰

 public synchronized void add() {
        count++;
  }

由于add方法被synchronized修饰,因此每一个时刻只能有一个线程执行add方法,因此上面打印的结果是20000

总结: 

synchronized修饰的add方法一个时刻只能有一个线程执行的意思是对于一个SyncDemo类的对象来说一个时刻只能有一个线程进入。比如现在有两个SyncDemo的对象s1s2,一个时刻只能有一个线程进行s1add方法一个时刻只能有一个线程进入s2add方法,但是同一个时刻可以有两个不同的线程执行s1s2add方法,也就说s1add方法和s2add是没有关系的一个线程进入s1add方法并不会阻止另外的线程进入s2add方法,也就是说synchronized在修饰一个非静态方法的时候“锁”住的只是一个实例对象并不会“锁”住其它的对象。其实这也很容易理解,一个实例对象是一个独立的个体别的对象不会影响他,他也不会影响别的对象。

3.2、修饰一个静态的方法

class SyncDemo {
    private static int count;  //静态变量
    public static synchronized void add() { //静态方法
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        SyncDemo syncDemo = new SyncDemo();
        Thread t1 = new Thread(()->{
            for (int i = 0; i <1000 ; i++) {
                syncDemo.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i <1000 ; i++) {
                syncDemo.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();  //线程的阻塞方法,线程t1执行完毕,再执行main主线程打印语句
        t2.join();  //线程的阻塞方法,线程t2执行完毕,再执行main主线程打印语句
        System.out.println(syncDemo.count);  //输出结果为2000
    }
}

上面的代码最终输出的结果也是20000,但是与前一个程序不同的是。这里的add方法用static修饰的,在这种情况下真正只能有一个线程进入到add方法,因为用static修饰的add方法是静态方法,静态方法所有对象公共的方法,因此和前面的那种情况不同,不存在两个不同的线程同一时刻执行不同实例对象的add方法。

你仔细想想如果能够让两个不同的线程执行add代码块,那么count++的执行就不是原子的了。那为什么没有用static修饰的代码为什么可以呢?因为当没有用static修饰时,每一个对象的count都是不同的,内存地址不一样,因此在这种情况下count++这个操作仍然是原子的!

3.3、修饰一个代码块

class SyncDemo {
    private  int count;  //静态变量
    public  void add() {
        System.out.println("进入了add方法");
        synchronized (this){
            count++;
        }
    }
    public  void minus() {
        System.out.println("进入了minus方法");
        synchronized (this){
            count--;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        SyncDemo syncDemo = new SyncDemo();
        Thread t1 = new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                syncDemo.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                syncDemo.minus();
            }
        });
        t1.start();
        t2.start();
        t1.join();  //线程的阻塞方法,线程t1执行完毕,再执行main主线程打印语句
        t2.join();  //线程的阻塞方法,线程t2执行完毕,再执行main主线程打印语句
        System.out.println(syncDemo.count);  //输出结果为0
    }
}

有时候我们并不需要用synchronized修饰一个方法因为这样并发度就比较低了,一个方法一个时刻只能有一个线程在执行。因此我们可以选择用synchronized去修饰代码块只让某个代码块一个时刻只能有一个线程执行,除了这个代码块之外的代码还是可以并行的。

比如上面的代码当中addminus方法没有使用synchronized进行修饰,因此一个时刻可以有多个线程执行这个两个方法。在上面的synchronized代码块当中我们使用了this对象作为锁对象,只有拿到这个锁对象的线程才能够进入代码块执行,而在同一个时刻只能有一个线程能够获得锁对象。也就是说add函数和minus函数用synchronized修饰的两个代码块同一个时刻只能有一个代码块的代码能够被一个线程执行,因此上面的结果同样是0

这里说的锁对象是this也就SyncDemo类的一个实例对象,因为它锁住的是一个实例对象,因此当实例对象不一样的时候他们之间是没有关系的,也就是说不同实例用synchronized修饰的代码块是没有关系的,他们之间是可以并发的。

3.4、修饰一个类

class SyncDemo {
    private  int count;  //静态变量
    public  void add() {
        System.out.println("进入了add方法");
        synchronized (SyncDemo.class){
            count++;
        }
    }
    public  void minus() {
        System.out.println("进入了minus方法");
        synchronized (SyncDemo.class){
            count--;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        SyncDemo syncDemo = new SyncDemo();
        Thread t1 = new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                syncDemo.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                syncDemo.minus();
            }
        });
        t1.start();
        t2.start();
        t1.join();  //线程的阻塞方法,线程t1执行完毕,再执行main主线程打印语句
        t2.join();  //线程的阻塞方法,线程t2执行完毕,再执行main主线程打印语句
        System.out.println(syncDemo.count);  //输出结果为0
    }
}

上面的代码是使用synchronized修饰类,锁对象是SyncDemo.class,这个时候他不再是锁住一个对象了,而是一个类了,这个时候的并发度就变小了,上一份代码当锁对象是SyncDemo的实例对象时并发度更大一些,因为当锁对象是实例对象的时候只有实例对象内部是不能够并发的,实例之间是可以并发的。但是当锁对象是SyncDemo.class的时候,实例对象之间时不能够并发的,因为这个时候的锁对象是一个类。

四、Synchronized与可见性和重排序

4.1互斥性/排他性(非公平锁)

synchronized(非公平锁)会起到互斥效果,某个线程执⾏到某个对象的 synchronized 中时,其他线程如果也执⾏到同⼀个对象 synchronized 就会阻塞等待。

  • 进⼊ synchronized 修饰的代码块, 相当于加锁。
  • 退出 synchronized 修饰的代码块, 相当于解锁。

PS:公平锁 VS 非公平锁
公平锁:按资排辈,先到先得。需要“唤醒”这一步操作,会牺牲一定的性能。(线程发现锁占用,尝试获取锁一段时间后,进入休眠状态,进入排队队列中等待。上一个线程释放锁后,会唤醒排队等候的其他线程中最前面的线程从阻塞状态又切换至运行状态)
非公平锁:来得早不如来得巧,不需要“唤醒”,性能高。(线程1发现锁占用,尝试获取锁一段时间后,进入休眠状态。此时线程2来了,处于运行状态,尝试获取锁,此时刚好上一个线程释放了锁,那么线程2直接得到了锁并去运行它的任务了。排队等待的其他线程获取锁的顺序不是按照访问的顺序先来先到的,而是由线程自己竞争,随机获取到锁)Java里所有的锁默认是非公平锁。

4.2可见性

  • 当一个线程进入到synchronized同步代码块的时候,将会刷新所有对该线程的可见的变量,也就是说如果其他线程修改了某个变量,而且线程需要在Synchronized代码块当中使用,那就会重新刷新这个变量到内存当中,保证这个变量对于执行同步代码块的线程是可见的。

  • 当一个线程从同步代码块退出的时候,也会将线程的工作内存同步到内存当中,保证在同步代码块当中修改的变量对其他线程可见。

4.3可重入性(可重入性锁)

synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题。

/**
 * synchronized 可重入性测试
 */
public class ThreadSynchronized4 {
    public static void main(String[] args) {
        synchronized (ThreadSynchronized4.class) {
            System.out.println("当前主线程已经得到了锁"); //当执行到此行代码时,表示已经获得锁
            synchronized (ThreadSynchronized4.class) { //同一个线程获取锁两次
                System.out.println("当前主线程再次得到了锁"); //若两行代码都能打印,说明具备可重入性
            }
        }
    }
}

 

五、synchronized实现原理

(面试必问)synchronized是如何实现的?
①在Java代码层面:

synchronized加锁的对象里有一个的隐藏的对象头,这个对象头(可看作一个类)里有很多属性,其中比较关注的两个属性是:是否加锁的标识和拥有当前锁的线程id。

每次进⼊ synchronized 修饰的代码块时,会去对象头中先判断加锁的标识,再判断拥有当前锁的线程id,从而决定当前线程能否往下继续执行。

判断加锁标识为false->对象头未加锁,当前线程可以进入synchronized 修饰的代码块,并设置加锁标识为true,设置拥有当前锁的线程id为此线程id。
判断加锁标识为true->对象头已加锁,需进一步判断拥有当前锁的线程id是否为此线程id,若是,则继续往下执行;否则,不能往下执行,需要等待锁资源释放后重新竞争再获取锁。
②在JVM层面和操作系统层面:

synchronized同步锁是通过JVM内置的Monitor监视器实现的,而监视器又是依赖操作系统的互斥锁Mutex实现的。↓
————————————————
原文链接:https://blog.csdn.net/WWXDwrn/article/details/129115774

六、synchronized历史发展进程

  • 在JDK1.6之前(多使用Lock)synchronized使用很少,那时synchronized默认使用重量级锁实现,所以性能较差。
  • 在JDK1.6时,synchronized做了优化。锁升级流程如下:

1,无锁:没有线程访问时默认是无锁状态,加了synchronized也是无锁状态。有线程访问时才加锁。更大程度上减少锁带来的程序上的开销。
2,偏向锁:当有一个线程访问时会升级为偏向锁。(在对象头里存了这样一把锁,后面再来线程时会判断,如果线程id和拥有锁的线程id相同,会让它进去,只偏向某一个线程,其他线程来了之后要继续等)
3,轻量级锁:当有多个线程访问时会升级为轻量级锁。
4,重量级锁:当大量线程访问同时竞争锁资源的时候会升级为重量级锁。

原文链接:https://baijiahao.baidu.com/s?id=1740505389877266267&wfr=spider&for=pc

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

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

相关文章

Spring Boot中使用Swagger

1. 启用Swagger 1.1 启用注解扫描和文档接口 直接在POM文件引入依赖 <dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version> </dependency>1.2 启动swagger-u…

ARM与大模型,狭路相逢

编辑&#xff1a;阿冒 设计&#xff1a;沐由 从去年底至今&#xff0c;伴随着OpenAI旗下ChatGPT的火爆&#xff0c;一波AI大模型推动着AI应用全面进入了大模型时代。与此同时&#xff0c;随着边缘算力的提升&#xff0c;AI大模型的部署也逐渐从云端涉入到边缘。 世界对AI算力的…

黑马一站制造数仓实战1

1. 项目目标 一站制造 企业中项目开发的落地&#xff1a;代码开发 代码开发&#xff1a;SQL【DSL SQL】 SparkCore SparkSQL 数仓的一些实际应用&#xff1a;分层体系、建模实现 2. 内容目标 项目业务介绍&#xff1a;背景、需求 项目技术架构&#xff1a;选型、架构 项目环境…

Selenium 自动化高级操作与解决疑难杂症,如无法连接、使用代理等

解决 Selenium 自动化中的常见疑难杂症 这里记录一些关于 Selenium的常用操作和疑难杂症。 有一些细节的知识点就不重复介绍了&#xff0c;因为之前的文章中都有&#xff01; 如果对本文中的知识点有疑问的&#xff0c;可以先阅读我以前分享的文章&#xff01; 知识点&…

1-4、调试汇编程序

语雀原文链接 文章目录 1、执行过程第一步&#xff1a;源程序第二步&#xff1a;编译连接第三步&#xff1a;执行 2、DOSBox运行程序第1步 进入EDIT.EXE第2步 编写源程序第3步 编译第4步 连接第5步 执行完整过程 3、DEBUG跟踪执行过程加载程序到内存执行程序debug和源程序数字…

【导航控制器总结-导航控制器栈 Objective-C语言】

一、导航控制器总结 1.我们接着上一堂课的内容继续 我们上节课说到哪里了,是不是就是对这个导航控制器的一个总结啊 然后,使用的注意事项 2.导航控制器使用注意事项: 1)第一点,使用导航控制器,你在创建的时候,需要给它指定一个根控制器 创建导航控制器的同时,指定…

【译】如何在调试时分析CPU和内存(Analyze CPU and Memory while Debugging)

您想了解如何使您的代码运行得更快&#xff0c;使用更少的内存&#xff0c;或者只是找出您的代码是否有CPU或内存问题?你当然会——你是一名开发人员!但是&#xff0c;内存和性能调优经常会遇到“重要但不紧急”的任务&#xff0c;因为真正紧急的事情&#xff0c;您似乎根本无…

优化 SQL 日志记录的方法

为什么 SQL 日志记录是必不可少的 SQL 日志记录在数据库安全和审计中起着至关重要的作用&#xff0c;它涉及跟踪在数据库上执行的所有 SQL 语句&#xff0c;从而实现审计、故障排除和取证分析。SQL 日志记录可以提供有关数据库如何访问和使用的宝贵见解&#xff0c;使其成为确…

Dockerfile脚本编写流程及示例

学习dockerfile指令 Dockerfile 指令 说明 FROM 指定基础镜像 MAINTAINER 声明镜像的维护者 LABEL 添加元数据标签 RUN 在容器中执行命令 CMD 容器启动后默认执行的命令 EXPOSE 暴露容器的端口 ENV 设置环境变量 ADD 将文件、目录或远程文件添加到容器中 COP…

[ 蓝桥杯Web真题 ]-外卖给好评

目录 介绍 准备 目标 效果 规定 思路 解答参考 介绍 外卖是现代生活中必备的一环。收到外卖后&#xff0c;各大平台软件常常会邀请用户在口味&#xff0c;配送速度等多个方面给与评分。在 element-ui 组件中&#xff0c;已经有相应的 Rate 组件&#xff0c;但是已有组件…

论文解读--PointPillars- Fast Encoders for Object Detection from Point Clouds

PointPillars--点云目标检测的快速编码器 摘要 点云中的物体检测是许多机器人应用(如自动驾驶)的重要方面。在本文中&#xff0c;我们考虑将点云编码为适合下游检测流程的格式的问题。最近的文献提出了两种编码器;固定编码器往往很快&#xff0c;但牺牲了准确性&#xff0c;而…

【latex笔记】双栏格式下插入单栏、双栏格式图片

双栏格式下插入单栏、双栏格式图片 1.缘起multicols2.双栏格式 插入单栏图片3.双栏格式 插入双栏图片 1.缘起multicols 插入双栏格式图片问题被困扰了有很长一段时间&#xff0c;查看网络资源也一直没找到解决方法&#xff0c;今天查看Latex官方文档&#xff0c;才发现因为mul…

spring cloud 整合Feign经行远程调用

文章目录 Feign远程调用Feign替代RestTemplate1&#xff09;引入依赖2&#xff09;添加注解3&#xff09;编写Feign的客户端4&#xff09;测试5&#xff09;总结 自定义配置配置文件方式Java代码方式 Feign使用优化 Feign远程调用 先来看我们以前利用RestTemplate发起远程调用…

将rtsp视频流发送到AWS Kinesis Video Streams的方案——使用Gstreamer(C++) Command Line

大纲 1 创建Kinesis Video Streams1.1 创建视频流1.2 记录Creation Time 2 创建策略2.1 赋予权限2.2 限制资源2.3 Json格式描述&#xff08;或上面手工设置&#xff09;2.4 注意事项 3 创建IAM用户3.1 生成密钥对3.2 附加策略3.3 记录访问密钥对 4 编译C 创建者库5 发送6 检查参…

聊聊 Jetpack Compose 的 “状态订阅自动刷新” -- mutableStateListOf

Jekpack Compose “状态订阅&自动刷新” 系列&#xff1a; 【 聊聊 Jetpack Compose 的 “状态订阅&自动刷新” - - MutableState/mutableStateOf 】 【 聊聊 Jetpack Compose 的 “状态订阅&自动刷新” - - remember 和重组作用域 】 【 聊聊 Jetpack Compose 的 …

非标设计之气缸类型

空压机&#xff1a; 空压机又称空气压缩机&#xff0c;简单来说就是将机械能转化为压力能来进行工作的&#xff0c;空压机在电力行业应用比较多&#xff0c;除了在电力行业应用较多外&#xff0c;其实空压机还有一个比较常见的用途就是用来制冷和分离气体&#xff0c;输送气体…

java SSM毕业生信息管理myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

前言 学校的规模不断扩大&#xff0c;学生数量急剧增加&#xff0c;有关学生的各种信息量也成倍增长。面对庞大的信息量需要有学生信息管理系统来提高学生管理工作的效率。通过这样的系统可以做到信息的规范管理、科学统计和快速查询、修改、增加、删除等&#xff0c;从而减少管…

【力扣热题100】207. 课程表 python 拓扑排序

【力扣热题100】207. 课程表 python 拓扑排序 写在最前面207. 课程表解决方案&#xff1a;判断是否可以完成所有课程的学习方法&#xff1a;拓扑排序实现步骤Python 实现性能分析结论 写在最前面 刷一道力扣热题100吧 难度中等 https://leetcode.cn/problems/course-schedule…

[leetcode ~二叉树] 模版

文章目录 1. 左叶子之和2. 翻转二叉树 E 1. 左叶子之和 :::details 给定二叉树的根节点 root &#xff0c;返回所有左叶子之和。 示例 1&#xff1a; 输入: root [3,9,20,null,null,15,7] 输出: 24 解释: 在这个二叉树中&#xff0c;有两个左叶子&#xff0c;分别是 9 和 15&…

Proteus仿真--基于1602LCD与DS18B20设计的温度报警器

本文介绍基于1602LCD与DS18B20设计的温度报警器设计&#xff08;完整仿真源文件及代码见文末链接&#xff09; 仿真图如下 其中温度传感器选用DS18B20器件&#xff0c;主要用于获取温度数据并上传&#xff0c;温度显示1602LCD液晶显示器&#xff0c;报警模块选用蜂鸣器&#…