【Java EE初阶四】锁及synchronized关键字

1. 加锁的目的

        对于count++这样的一个java语句,其底层是由三个基本操作组成的,我们在多线程中运行一个java语句,但是该语句的三个操作会被其他线程冲散,导致整个Java语句不能及时的一次性完成,这样就会导致我们的预期结果产生误差;

        我们加锁就是使用synchronized关键字来将一个java语句的多个底层操作包装成一个原子性的整体(该行为叫加锁),不会在多线程抢占式执行的时候冲散;

        synchronized关键字的两大特性:

        1、互斥性

        因为加锁具有互斥的特性,给一段代码加锁,当运行这段代码时,该段代码在系统上的指令就会就会被打包在一起,等这些指令被完全执行结束,其他的指令操作才能进行。如此也达到了加锁的目的:把几个操作打包成一个原子(整体)的操作。

        2、可重入性

        综上所述:加锁导致锁竞争,通过锁竞争让第二个线程的指令无法插入到第一个正在执行的线程指令中,而不是禁止第一个线程被调度出cpu;

2. 加锁和解锁

       1、未加锁前

        我们想让一个变量自增10_0000次,用两个线程来实现这一操作,分工各一半,没有加锁的操作,是有线程问题的,因为两个线程修改同一个变量的原因。代码如下:

public class ThreadDemo4 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 1; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 50000; i <= 100000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
 
        t1.join();
        t2.join();
 
        System.out.println("count: " + count);
    }
}

          按照我们的逻辑,从1自增到10_0000,肯定是自增了10_0000次,但是结果如下图所示:

      

2、给代码加锁

       给count++加上锁操作后的代码:

package thread;
import java.util.Currency;
public class ThreadDemo19 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //随便创建一个对象就行
        Object locker = new Object();
        //创建两个线程,每个线程都针对上述的count变量循环自增5w次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
               synchronized(locker) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        //打印count的结果
        System.out.println("count:" + count);
    }
}

        执行结果如下,如此是我们预期的效果:

前1

3、加锁的最核心规则:

        对于一个线程,针对一个锁对象进行加锁后,但也有其他线程,也尝试对这个对象进行加锁,如此就会产生阻塞,我们就把这种现象称为锁竞争 / 锁冲突。(假如我们为了保证代码的原子性,设计出锁对象对象,多线程运行的时候,每个线程在运行时都要求与锁对象在一起进行加锁后执行,但是考虑到只有一个锁对象,所以其他没有锁的线程只能阻塞等待,当加锁后的线程解锁后,其他线程在抢占这个锁对象来执行代码),图解如下:

        4、加锁和解锁的执行过程,针对上述代码,简单的画图来讲解一下关于底层指令的执行逻辑:

3. 加锁之后的线程安全问题 

3.1 两个线程,针对不同对象加锁

        加锁用的不是同一个对象,则依旧存在线程安全问题,如下代码所示:

package thread;

public class ThreadDemo21 {

        private static int count = 0;
        public static void main(String[] args) throws InterruptedException {
            Object locker1 = new Object();
            Object locker2 = new Object();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 50000; i++) {
                    synchronized(locker1) {
                        count++;
                    }
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 50000; i++) {
                    synchronized(locker2){
                        count++;
                    }
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(count);
        }

}

        结果如下:

       

        通过结果可知结果所示与我们的预期结果二者存在不同,故此可以判断依旧存在线程安全问题;

3.2 一个线程加锁,一个线程不加锁 

        代码如下:

package thread;
public class ThreadDemo21 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
//        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized(locker1) {

                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

        结果如下:

        通过结果可知结果所示与我们的预期结果二者存在不同,故此可以判断依旧存在线程安全问题;

3.3 关于加锁操作的一些错误理解

3.3.1 多个线程调用同一个类的方法,对其方法中的变量加锁

        修改最初的代码,把count放到Test t 对象中,在这里面count++,并且对其加锁,加锁对象是 this,其他线程再来调用Test中的方法。

package thread;
class Test {
    public int count = 0;
    public void add() {
        synchronized(this) {
            count++;
        }
    }
}
public class ThreadDemo21 {
    public static void main(String[] args) throws InterruptedException {
        Test t = new Test();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                t.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                t.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(t.count);
    }
}

        结果如下:

        

        如结果所示,这种情况是线程安全的。关于this的指向分析如下图:

        分析:这里的Test类中add方法里对其加锁引用的对象是this,也就是当前Test类的实例对象,所以两个线程调用者方法的时候会产生锁竞争,结果也就可以达到我们的预期效果了。(将上述的count+=操作单独的创建了一个类来分离出来count变量,同时添加add方法来完善count自加操作,即该部分代码中的加锁对象是test类的实例对象(足以完成count++操作)),故此不存在线程安全问题;

 3.3.2 Test类里的add方法里面,加锁的对象换成Test.class

       相对于前一部分代码, 只有一点小改动。

        代码如下:

package thread;
class Test {
    public int count = 0;
    public void add() {
        synchronized(Test.class) {
            count++;
        }
    }
}
public class ThreadDemo21 {
    public static void main(String[] args) throws InterruptedException {
        Test t = new Test();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                t.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                t.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(t.count);
    }
}

        结果如下:

      

        

        如结果所示,这种情况是线程安全的。

        分析:这里的 Test.class 是类对象(反射),而 t1 和 t2 拿到的都是同一个对象,就会有锁竞争,还是能保障线程安全的。

        番外:关于反射的知识如下图所示:

        在java进程中,由反射的知识可知,一个类只有一个类对象;

3.3.3  还可以把synchronized加到方法上(静态和普通方法都行) 

        上图所示表示锁的对象是当前方法所在的类;

3.4 一个线程被一个锁对象加锁两次(可重入性的例子 )

          代码如下:

package thread;

public class ThreadDemo21 {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t = new Thread(() -> {
            synchronized (locker) {
                synchronized (locker) {
                    // ... 随便写点啥都行
                    System.out.println("你好,不要温柔的走进良夜,微晚待续");
                }
                // 其他逻辑~~
            }
        });

        t.start();
    }
}

        结果如下:

        分析:我们得到如此结果就是因为synchronized有可重入性的特性,可以对一个线程用同一个锁对象加锁多次。

        通过如下图解,我们来了解一下可重入的底层逻辑:

        

        synchronized用计数器的方式,就能避免两个锁之间的逻辑从而失去锁的保护,上述所说的就是锁的可重入的特性。

        对于可重入锁来说,内部会持有两个信息:

1、当前这个锁被哪个线程持有的。

2、加锁次数的计数器。

3、这里的可重入性只针对java的synchronized关键字才有

4. 死锁 

        加锁能解决线程安全问题,但是如果加锁方式不当,就可能会产生死锁。

4.1 死锁的三种经典场景

4.1.1 一个线程,一把锁(钥匙锁屋里了)

锁是不可重入锁,在C++中,一个线程对这把锁加锁两次,就会产生死锁,代码如下:

public class ThreadDemo1 {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
           synchronized(locker)  {
                synchronized (locker) {
                    System.out.println("hello");
                }
           }
        });
        t1.start();
    }
}

        以上这种情况也就像现实生活中,我们的钥匙锁房间里了,我们要想进入房间,就要拿到房间里的钥匙,但是房间是被锁着的,我们拿不到。

        代码分析:遇到第一个synchronized,进行加锁,代码里的内容遇到第二个synchronized,锁对象是一样的,就要阻塞,等待第一个synchronized里的代码执行完才能执行第二个synchronized里的代码,因为第二个被阻塞等待了,所以会一直这样的阻塞的等待下去。

4.1.2 两个线程,两把锁 

        线程1获取到了锁A,线程2获取到了锁B,在这个条件下,线程A想获取锁B,线程B想获取锁A。这种情况发生时,就会产生死锁。

代码如下:

public class ThreadDemo2 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
 
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    System.out.println("我获得了两把锁");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (B) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (A) {
                    System.out.println("我获得了两把锁");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

        结果如下:

        这里会一直阻塞等待,我们可以用jconsole看当前线程的状态,如下图所示,显示线程正在阻塞等待:

        这种情况,就像是钥匙锁车里了,车钥匙锁屋里了。

        代码分析:线程1获取到了A锁,线程2也获得了B锁,因为线程的调度是随机的,以线程1为例子。当线程1想获取B锁时,因为线程2已经获取了锁B,就要等待线程2的锁B解锁后,线程1才能获取到B锁,但是线程2获取锁B后,它想获取锁A,因为锁A已经被线程1获取了,就要等线程1的A锁解锁后才能获取到锁A,这样就造成了阻塞等待,线程1等待B锁解锁,线程2等待锁A解锁,两个线程里的锁都无法解锁,就一直卡着不动,就这样线程之间互相循环等待,就形成了死锁;

        解决方案:

        给加锁指定一定规则,例如:1线程获取A锁后,再获取B锁,2线程获取A锁后,再获取B锁。

代码如下:

public class ThreadDemo21 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
 
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    System.out.println("我获得了两把锁");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (A) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    System.out.println("我获得了两把锁");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

        结果如下:

4.1.3 N个线程,M把锁

        哲学家就餐问题:

假设有五个哲学家,五把锁,一个哲学家就是一个线程,一个筷子就是一把锁,如图所示:

 

规则:一个哲学家要吃苗条的话要有一双筷子才能吃,而且只能拿哲学家左右两边的筷子,哲学家在吃面条的时候,不能被别的哲学家抢筷子

        这时,如果每个哲学家,都同时拿起左边的筷子,这时,就没有多余的筷子给哲学家使用吃面条了,谁都不能吃到,谁都在等待,如图所示:

        这就像一个线程,自己已经持有了一把锁,但还在尝试获取一把锁,但是每个不同锁都被不同线程加锁了,这时就等待别的线程释放锁,但是大家也都在阻塞等待别的锁释放,就会产生死锁问题。

        此时想要解决这个问题,只要破坏产生死锁的4个必要条件的其中一个,就能解决死锁的问题,其中破坏循环等待是最简单的,具体方案如下:

        我们规定,每个哲学家拿筷子编号比自己编号小的筷子,从编号为2的哲学家开始,如图:

        到最后,1号哲学家就不能拿筷子了,5号能吃到面条,等5号吃完,4号就能吃,依次类推,每个哲学家就都能吃到面条了。

4.2  产生死锁的四个必要条件 

        1、互斥使用(获取锁的过程是互斥的,一个线程拿到了这把锁,其他线程想要拿到这把锁,就要阻塞,等待这个线程释放这把锁后,才能拿这把锁)

        2、不可抢占(一个线程拿到一把锁,其他线程不能强行把这把锁抢走)

        3、请求保持(一个线程拿到A锁,在持有A锁的前提下,同时尝试拿到B锁)

        4、循环等待 / 环路等待。

破坏上述条件的难易程度:

        1、互斥使用:锁的基本特性,不好破坏。

        2、不可抢占:锁的基本特性,不好破坏。

        3、请求保持:代码结构的原因,看实际需求,有时候能破坏,有时候不能破坏。

        4、循环等待:代码结构的原因,最好破坏,指定一定的规则即可避免循环等待。

ps:本次的内容就到这里了,如果喜欢的话就请一键三连哦!!!

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【WordPress插件】热门关键词推荐v1.3.0 Pro开心版

介绍&#xff1a; WordPress插件-WBOLT热门关键词推荐插件&#xff08;Smart Keywords Tool&#xff09;是一款集即时关键词推荐、关键词选词工具及文章智能标签功能于一体的WordPress网站SEO优化插件。 智能推荐&#xff1a; 热门关键词推荐引擎-支持360搜索、Bing、谷歌&a…

极智开发 | 解读英伟达软件生态 深度神经网络库cuDNN

欢迎关注,获取我的更多经验分享 大家好,我是极智视界,本文来介绍一下 解读英伟达软件生态 深度神经网络库cuDNN。 邀您加入我的知识星球「极智视界」,星球内有超多好玩的项目实战源码下载,链接:https://t.zsxq.com/0aiNxERDq cuDNN,全称为 NVIDIA CUDA Deep Neural Net…

3D动态路障生成

3D动态路障生成 介绍设计实现1.路面创建2.空物体的创建3.Create.cs脚本创建 总结 介绍 上一篇文章介绍了Mathf.Lerp的底层实现原理&#xff0c;这里介绍一下跑酷类游戏的动态路障生成是如何实现的。 动态路障其实比较好生成&#xff0c;但是难点在哪里&#xff0c;如果都是平面…

RHCE9学习指南 第11章 网络配置

11.1 网络基础知识 一台主机需要配置必要的网络信息&#xff0c;才可以连接到互联网。需要的配置网络信息包括IP&#xff0c;子网掩码&#xff0c;网关和DNS。 11.1.1 IP地址 在计算机中对IP的标记使用的是32bit的二进制&#xff0c;例如&#xff0c; 11000000 10101000 00…

基于梯度和频率域的深度超分辨率新方法笔记二

一、实现方法和网络结构的剖析 1.1 网络结构 梯度校准模块(GCM)和频率感知模块&#xff08;FAM&#xff09; 1&#xff09;梯度校准模块(GCM) 1、使用梯度映射函数&#xff08;如下图所示&#xff09;&#xff0c;将RGB和LR深度图映射到梯度域 2、再梯度域中使用RGB的梯度特…

【 C语言 】 | C程序百例

【 C语言 】 | C程序百例 时间&#xff1a;2023年12月28日13:50:43 文章目录 【 C语言 】 | C程序百例1.参考2.练习 1.参考 1.【 C语言 】 | C程序百例-CSDN博客 2.100Example: C程序百例-酷勤网&#xff08;kuqin.com&#xff09;提供.pdf (gitee.com) 3.cProgram/LinuxC - 码…

IPD-PDP产品开发流程-PDT产品开发计划Charter文档模板(word)2

书接上回&#xff0c;继续为大家分享PDT的产品开发计划Charter模板的主要内容。 据华研荟了解&#xff0c;大部分国内的企业在推行IPD的时候就直接像华为一样&#xff0c;把开发计划&#xff08;任务书&#xff09;叫做Charter&#xff0c;而不翻译为中文。其实这也是一种很好…

高校怎么搭建虚拟数字人动作捕捉实训室?

如今&#xff0c;虚拟数字人市场迎来爆发风口&#xff0c;在产业推动下&#xff0c;虚拟数字人动作捕捉技术人才需求旺盛&#xff0c;高校搭建虚拟数字人动作捕捉实训室&#xff0c;可以让学生接触前沿的全身动作捕捉系统&#xff0c;能够培养具有开阔眼界和先进技术接轨的专业…

鸿蒙系列--组件介绍之其他基础组件(上)

上回介绍了基础组件中最常用的组件常用的基础组件&#xff0c;接下来还有其他基础组件 一、Blank 描述&#xff1a;空白填充组件 功能&#xff1a;在容器主轴方向上&#xff0c;具有自动填充容器空余部分的能力。只有当父组件为Row/Column时生效 子组件&#xff1a;无 Blan…

羊大师讲解,羊奶在本草纲目中的作用及其益处

羊大师讲解&#xff0c;羊奶在本草纲目中的作用及其益处 羊奶是一种传统的中药材&#xff0c;早在《本草纲目》中就被广泛记录和应用。它是从母羊妊娠后分泌出来的乳汁&#xff0c;具有丰富的营养成分和独特的药用价值。小编羊大师发现&#xff0c;羊奶被认为是一种滋补养颜、…

3D 渲染如何帮助电商促进销售?

在线工具推荐&#xff1a; 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 3D 渲染图像因其高转化率而成为亚马逊卖家的最新趋势。它是电子商务平…

Linux iptables防火墙(一)

1.1 Linux防火墙基础 在 Internet 中&#xff0c;企业通过架设各种应用系统来为用户提供各种网络服务&#xff0c;如 Web 网站、 电子邮件系统、 FTP 服务器、数据库系统等。那么&#xff0c;如何来保护这些服务器&#xff0c;过滤企业不 需要的访问甚至是恶意的入侵呢&a…

作为开发人员掌握 GitHub Copilot:15 个提示和技巧

目录 Copilot 的炫酷用例 为您完成代码 从代码编写测试 TDD&#xff1a;通过测试编写代码 测试/模拟数据生成 从注释中编写代码 问&#xff1a;&答&#xff1a; 颜色生成 使用测量单位 自然语言翻译 自动化脚本 正则表达式 配置 命令行命令 文档和自述文本 if (true) 技巧 …

QT与MATLAB混合编程详解

写在前面&#xff1a;由于matlab和qt的环境配置复杂&#xff0c;请大家一定先根据自己电脑的路径来替换本文中的路径 本文使用环境&#xff1a; MATLAB 2022b qt 6.6 编译环境&#xff1a;MSVC2019 64位 matlab的环境还需要配置在环境变量中 此电脑&#xff08;右键&#xf…

Docker 实践之旅:项目迁移与高效部署

目录 1 引言2 初识 Docker2.1 Docker简介2.2 Docker优势 3 传统部署流程的问题4 学习 Docker 的过程5 Docker 解决项目部署的实践5.1 迁移关键服务5.2 定制化打包与快速部署 6 项目实践收获6.1 简化了部署流程6.2 节约了部署成本 7 克服难点和经验分享7.1 版本兼容性问题7.2 网…

Android Studio修改创建新布局时默认根布局

将Android Studio默认布局ConstraintLayout切换成LinearLayout 打开设置&#xff0c; Editor> File and Code Templates > Other > layoutResourceFile.xml 备注&#xff1a;创建时提示根布局仍然为ConstraintLayout&#xff0c;不用管这个&#xff0c;实际创建的…

MybatisX逆向工程方法

官方文档链接&#xff1a;MybatisX快速开发插件 | MyBatis-Plus (baomidou.com) 使用MybatisX可以快速生成mapper文件&#xff0c;实体类和service及实现 效果 方法&#xff1a;首先下载mybatisX插件 然后创建数据库信息 然后选中表&#xff0c;右键&#xff0c;点击Mybatis…

WeakMap 和 Map 的区别,WeakMap 原理,为什么能被 GC?

垃圾回收机制 我们知道&#xff0c;程序运行中会有一些垃圾数据不再使用&#xff0c;需要及时释放出去&#xff0c;如果我们没有及时释放&#xff0c;这就是内存泄露 JS 中的垃圾数据都是由垃圾回收&#xff08;Garbage Collection&#xff0c;缩写为 GC&#xff09;器自动回…

JMeter逻辑控制器之While控制器

JMeter逻辑控制器之While控制器 1. 背景2.目的3. 介绍4.While示例4.1 添加While控制器4.2 While控制器面板4.3 While控制器添加请求4.3 While控制器应用场景 1. 背景 存在一些使用场景&#xff0c;比如&#xff1a;某个请求必须等待上一个请求正确响应后才能开始执行。或者&…

Springboot整合JSP-修订版本(Springboot3.1.6+IDEA2022版本)

1、问题概述&#xff1f; Springboot对Thymeleaf支持的要更好一些&#xff0c;Springboot内嵌的Tomcat默认是没有JSP引擎&#xff0c;不支持直接使用JSP模板引擎。这个时候我们需要自己配置使用。 2、Springboot整合使用JSP过程 现在很多的IDEA版本即使创建的项目类型是WAR工…