synchronized,volatile关键字

news2024/10/11 4:31:09

目录

一,synchronized的特性

1.1 互斥性

1.2 可重入性

二, 死锁

2.1 死锁产生的原因

三,volatile 关键字

3.1 能保证内存可见性


一,synchronized的特性

1.1 互斥性

当两个线程对同一个对象加锁时,后加锁的线程要 "阻塞等待" ,直到第一个线程的锁释放。

1.2 可重入性

我们都知道当两个不同的线程正对同一个对象进行 "加锁" 时,会出现锁竞争,那么如果当一个线程对同一个对象连续 "加锁" 时,会怎么样呢?,比如下面的代码:

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker){
                synchronized (locker){
                    System.out.println("t1执行");
                }
            }
        });
        t1.start();
        t1.join();
    }
}

按照之前说的,当 t1 线程第一次 "加锁" 成功后,此时的 locker 就属于是 "被锁定" 状态,那么当 t1 线程进行第二次 "加锁" 时,原则上是要 "阻塞等待" ,等到锁(locker)被释放了之后,才能再次 "加锁" ,但是在上述的代码中,只要第二次(locker)不加锁,第一个锁(locker)就不会被释放,而第一个锁(locker)不释放,第二次(locker)又不能加锁,这样的话,在逻辑上,上述代码中的线程 t1 就会产生死锁从而卡死,很明显,这是一个BUG。

但是在日常开发中,这个BUG又很难避免,这时候有人会说,你上面的代码不是一眼就看出来了,这还不好避免?我在这里再举一个例子:

class Test1{
    Object locker = new Object();
    public void fun1(){
        synchronized (locker){
            fun2();
        }
    }
    public void fun2(){
        fun3();
    }
    public void fun3(){
        fun4();
    }
    public void fun4(){
        synchronized (locker){
            System.out.println("4444");
        }
    }
}
//当出现上述代码时,当我们调用 fun1 方法,根本看不出来有什么问题!!!

所以为了解决上述的BUG,设计Java的那群人就把 synchronized 设计成 "可重入锁" ,就是说当一个线程对同一个对象连续加锁时,这个锁会自己记录是哪个线程给它 "加锁" ,这样后续再次加锁时,如果加锁线程就是当前持有锁的线程,就直接加锁成功。

这里我要提出一个问题:synchronized 虽然是可重入锁,避免了死锁,但是当一个线程对同一个对象加 N 层锁时,我们的锁什么时候释放?以及计算机如何判断锁释放的时机?第一个问题很简单,肯定是要第一次加锁的{}执行结束锁才能释放,第二个问题:锁对象不光会统计是谁拿到了锁,还会记录锁被加了几次,每一次加锁,计数器+1;每一次解锁,计数器-1。当出了最后一个{},计数器恰好为0,释放锁。

二, 死锁

产生死锁的情况:

1. 如果 synchronized 没有可重入性,对同一个对象连续加锁

2. 两个线程,两把锁,synchronized嵌套使用(可能产生!!!)。例如:

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker1){
                try {
                    Thread.sleep(1);//为了让t1拿到locker1,t2拿到locker2
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2){
                    System.out.println("t1结束");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker2){
                synchronized (locker1){
                    System.out.println("t2结束");
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

3.  M个线程,N把锁(相当于2的推论)

2.1 死锁产生的原因

四个必要条件(只要下面4个条件中有一个不成立,那么就不会形成死锁)

  1. 互斥使用(锁的基本特性)即当两个线程对同一个对象加锁时,后加锁的线程要 "阻塞等待" ,直到第一个线程的锁释放。
  2. 不可抢占(锁的基本特性)与第一点相同
  3. 请求保持,即锁和锁之间可以嵌套使用
  4. 循环等待/环路等待,即等待的依赖关系形成了环,比如:车钥匙锁家里了,家钥匙锁车里了。与上述代码一个情况

那么我们如何解决死锁? 

因为上述的4个条件中,前两个条件是锁自带的属性,无法干预,因此我们只能从后两个条件入手。对于条件3:只要我们避免编写锁嵌套的逻辑就行。(但是有的情况下,这是无法避免的)对于条件4:给锁编号,约定加锁的顺序,比如:约定先加编号大的锁,后加编号小的锁,所有的线程都要遵守。

三,volatile 关键字

3.1 能保证内存可见性

什么是内存可见性?

在计算机运行代码时,要经常访问数据,这些数据往往存储在内存中(定义一个变量,这个变量就存储在内存中)。而cpu读取内存的这个操作,比cpu读取寄存器慢了几万倍,这时就会出现,cpu在解决大部分的情况时,速度很快,一旦读取内存,速度瞬间就慢下来了的情况。

为了解决上述问题,提高运行效率,此时编译器就会对代码做出优化,把一些原本读取内存的操作,优化成读取寄存器,这样就减少了读内存的次数,从而提高了整体的效率。举一个例子:

public class Demo3 {
    static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(flag){

            }
            System.out.println("循环结束!");
        });
        t1.start();
        Thread.sleep(10);
        flag = false;
        t1.join();
    }
}

 很明显,即使我们将flag改成false,线程也没有停止循环,这就是 "内存可见性" 引起的,由于该循环没有任何其他操作,循环速度非常快,就会进行大量的 load(将flag读取到内存) ,cmp 操作。此时编译器发现,虽然进行了多次 load 操作,但是 flag 的值没有改变,而 load 操作又很浪费时间,所以编译器直接就在第一次循环的时候,读内存,将flag存到寄存器中,后续就不读内存了,直接从寄存器中取出 flag,这就是导致 "内存可见性" 问题。

而 volatile 关键字可以解决这一问题,只要被 volatile 修饰的变量,编译器就不可以对其进行优化,也就是说,该变量必须从内存中读取。例如:

public class Demo3 {
    volatile static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(flag){

            }
            System.out.println("循环结束!");
        });
        t1.start();
        Thread.sleep(10);
        flag = false;
        t1.join();
    }
}

 还有一点需要注意的是,编译器优化的触发是不确定的,我们不知道它什么时候触发,什么时候不触发。所以最好使用 volatile 关键字!!!

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

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

相关文章

Error: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试

Error: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试 django运行时报错 报错原因 django运行时的端口被其他服务占用了,此时需要关闭占用端口的服务, django的默认端口为8000 解决办法1 netstat -ano | findstr 8000 // cmd操作获取占用…

天翼云HBlock:盘活存储资源,释放数据价值

马克安德森曾言:“软件正在吞噬世界。” 回顾数据存储过去二十年发展,硬件的更新迭代固然重要,但软件的价值亦不可低估。从2013年软件定义存储首次入围Gartner技术成熟度曲线,再到在虚拟化和云计算环境中大显身手,软件…

运维Shell脚本小试牛刀(八): case模式忽略命令行参数大小写演示

运维Shell脚本小试牛刀(一) 运维Shell脚本小试牛刀(二) 运维Shell脚本小试牛刀(三)::$(cd $(dirname $0); pwd)命令详解 运维Shell脚本小试牛刀(四): 多层嵌套if...elif...elif....else fi_蜗牛杨哥的博客-CSDN博客 Cenos7安装小火车程序动画 运维Shell脚本小试…

SpringCloud-GetWay 路由网关

接上文 SpringCloud-Hystrix 服务降级与熔断 微服务也是如此,不是所有微服务需要直接暴露给外部调用,就需要使用路由机制,添加一层防护,让所有的请求全部通过路由来转发到各个微服务,并转发给多个相同微服务实例&#…

Linux编辑器 VI VIM

vim 命令模式 插入模式 ex模式 \ 命令模式 /查找关键字后,按n键在找到的结果之前来换的切换、 EX模式

响应式布局(3种) + flex计算

响应式布局 1.媒体查询2.使用百分比、rem、vw、vh等相对单位来设置元素的宽度、高度、字体大小等1.rem与em2.vw、vh、vmax、vmin 3.Flexboxflexbox计算题 响应式布局是指同一个页面在不同屏幕尺寸下有不同的布局。 1.媒体查询 媒体查询是最基础的实现响应式的方式 使用media关键…

再见 MySQL 5.7 !

点击下方名片,设为星标! 回复“1024”获取2TB学习资源! 对从事互联网 IT 技术岗位的来说,数据库也是我们日常必备的技能之一,而 MySQL 数据库更是常见、常用的数据库之一。 根据 DB-Engines 的数据显示,MyS…

Matlab之DICOM(数字图像和通信医学)格式图像数据读取函数dicomread

一、DICOM是什么? DICOM是数字图像和通信医学格式的图像数据,在MATLAB中,可以使用dicomread函数读取DICOM格式的图像数据。 二、dicomread函数 使用方法如下: imageData dicomread(filename);其中,filename表示DI…

fail-safe 机制与 fail-fast 机制

Fail-fast 表示快速失败,在集合遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出 ConcurrentModificationException 异常,从而导致遍历失败,像这种情况 定义一个 Map 集合,使用 Iterator 迭代器进行数据…

live-server安装

天行健,君子以自强不息;地势坤,君子以厚德载物。 每个人都有惰性,但不断学习是好好生活的根本,共勉! 文章均为学习整理笔记,分享记录为主,如有错误请指正,共同学习进步。…

【算法专题突破】滑动窗口 - 长度最小的子数组(9)

目录 1. 题目解析 2. 算法原理 3. 代码编写 写在最后: 1. 题目解析 题目链接:209. 长度最小的子数组 - 力扣(Leetcode) 要注意的是,题目给的是正整数, 而题目要求并不难理解,就是找最短的…

听说背包问题很难? 这篇总结篇来拯救你了

文章转自代码随想录 已经把背包问题都讲完了,那么现在要对背包问题进行总结一番。 背包问题是动态规划里的非常重要的一部分,所以我把背包问题单独总结一下,等动态规划专题更新完之后,我们还会在整体总结一波动态规划。 关于这…

【HCIE】02.IGP高级特性

OSPF转发地址 5类LSA报文格式 5类LSA有一个Forwarding address字段,防止次优路由产生 次有路径的产生 如图,A与C运行OSPF协议,A与B运行ISIS协议,现在将ISIS导入到了OSPF中,C去访问X默认会先经过ASBR即R1,…

【多线程】Timer任务定时器实现与盲等原子性问题的解决

目录 一、定时器 二、标准库中的Timer 三、代码实现 四、死锁 一、定时器 代码中的定时器通常是在一定的时间执行对应的代码逻辑 二、标准库中的Timer public static void main(String[] args){Timer timer new Timer();timer.schedule(new TimerTask() {Overridepublic…

【周末闲谈】如何利用AIGC为我们创造有利价值?

个人主页:【😊个人主页】 系列专栏:【❤️周末闲谈】 系列目录 ✨第一周 二进制VS三进制 ✨第二周 文心一言,模仿还是超越? ✨第二周 畅想AR 文章目录 系列目录前言AIGCAI写作AI绘画AI视频生成AI语音合成 前言 在此之…

持续集成/技术交付全流程流水线工具的设计与落地

文章目录 持续集成/技术交付全流程流水线工具的设计与落地概述工具架构设计主要功能模块代码库Jenkins 流水线代码构建自动化测试产品部署监控报警 使用方法步骤一:安装 Jenkins步骤二:创建 Jenkins 流水线步骤三:配置监控报警步骤四&#xf…

circleMidpoint(scrPt c, GLint r) 未定义的标识符,openGL第四章例子 ,画饼状图。

以下是完整的例子。在第四版 《计算机图形学 with openGL》第四章的例子中,竟然只调用了circleMidpoint(scrPt &c, GLint r) ,没有实现,我认为是系统方法,怎么找都找不到。openGL 官方文档也没找到,这不会是自定义…

生信教程|替代模型选择

摘要 由于教程时间比较久远,因此不建议实操,仅阅读以了解学习。 在运行基于可能性的系统发育分析之前,用户需要决定模型中应包含哪些自由参数:是否应该为所有替换假设单一速率(如序列进化的 Jukes-Cantor 模型&#xf…

新版DBeaver调整编辑窗口字体大小

网上有DBeave字体设置了,但看了下,目前最新版的已经更改了首选项分组,层级发生了变化,这里记录一下2022.08.21版的设置。 默认字体是10,比较小,改为11或更大会好看些。

强大的JTAG边界扫描(1):基本原理介绍

文章目录 1. 什么是边界扫描?2. JTAG硬件接口3. 边界扫描相关的软硬件4. 学习资料5. 总结 我是怎么了解到边界扫描的呢? 这就要从我淘到一块FPGA板卡的事情说起了。 前段时间我在某二手平台上淘了一块FPGA板子,它长这样: 板子的…