5. 多线程(3) --- synchronized

news2025/3/9 22:40:59

文章目录

  • 前言
  • 1. 如何解决线程安全问题 [回顾]
  • 2. synchronized 关键字
    • 2.1. 示例
    • 2.2.对示例进行变化
    • 2.3 synchronized的其他写法
    • 2.4 synchronized的特性
      • 2.4.1 互斥
      • 2.4.2. 刷新内存
      • 2.4.3. 可重入


前言

前面我们通过在两个线程中共同对count进行加一操作,最后得到的结果和预期不一样,并且还通过画图得到了原因。在这个博客中,我们来解决一下问题—引入synchronized关键字


1. 如何解决线程安全问题 [回顾]

  1. 操作系统对于线程的调度随机的,抢占式的。
    这个是操作系统对于线程的底层设定,我们无法左右。

  2. 多个线程同时修改同一个变量。
    这个和代码的结果直接相关,通过调整代码结构,规避一些线程不安全的代码,但是在有些场景下,必须使用这种方案。
    例如,在超卖 / 超买 的问题中,某个商品,库存100件,不可以创建出101个订单吧。
    这个就是需求的情况下,需求就是需要多线程同时修改一个变量的。

  3. 修改操作,不是原子的
    通过加锁操作,把之前不是原子的count++ 包裹起来,在count++之前,先加锁,然后进行 count++,计算完毕之后,在解锁。
    执行完这三步,其他线程就无法插队了。
    加锁操作,不是把线程锁死到 CPU上,禁止这个线程被调度走,而是禁止让其他线程重新加这个锁,避免其他线程的操作,在当前线程的执行过程中插队。
    在这里插入图片描述


2. synchronized 关键字

2.1. 示例

加锁 / 解锁 本身是操作系统提供的 api,很多编程语言都对于这样的 api 进行了封装,大多数的封装风格,都是采用两个函数 lock() 和 unlock()

  1. lock(); // 加锁
  2. // 执行一些要保护起来的逻辑
  3. unlock(); // 解锁

在Java中,使用 synchronized 这样的关键字,搭配代码块,来实现类似的效果。

synchronized(){ // 进入代码块,就相当于加锁
// 执行一些保护的逻辑
} // 出了代码块,就相当于 解锁

在这里插入图片描述
我在在代码中使用synchronized关键字。


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

其中synchronized() 括号中的参数应该填什么呢?
填写的是用来加锁的对象,要加锁,要解锁,顾名思义,前提得有一把锁,在Java中,任何一个对象都有用作成"锁"。
这个对象的类型不重要,重要的是,是否有多个线程尝试针对这同一个对象进行加锁,换言之,是否多个线程同时竞争同一把锁。
在这里插入图片描述
那么我们现在就创建一把锁,放到参数中,然后运行看一下结果。


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

在这里插入图片描述
生成了一个object的锁对象,两个线程,使用同一把锁,才会产生互斥的效果。这是因为,一个线程加上了锁,另一个线程就得阻塞等待,等到第一个线程释放锁之后,才有机会拿到锁。
反之,如果采用不同的锁对象,此时不会产生互斥的效果,线程安全就没有得到改变。
在这里插入图片描述
我们可以对上面的进行修改,例如 把 synchronized放到 for循环的外面,或者是把锁对象类型进行变化,然后观察一下现象是否发生改变,我们说干就干!

2.2.对示例进行变化

  1. 把 synchronized放到 for循环的外面,为什么这个操作是可以的呢?
    这是因为t1和t2线程在并发执行过程中,相当于只有 count++ 这个操作,会涉及到互斥,for 循环里的条件判断 (i<50000) 和 i++ 这两个操作不涉及到互斥,所以可以直接把 synchronized放到 for 循环外面。
    在这里插入图片描述
    Demo16
public class Demo16 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000000; i++) {
                synchronized (object){
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0;i < 50000000;i++){
                synchronized (object){
                    count++;
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count:"+count);
    }
}

Demo17

public class Demo17 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker){
                for (int i = 0; i < 50000000; i++) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker){
                for (int i = 0; i < 50000000; i++) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count:"+count);
    }
}

这两种写法,都可以得到我们想要的结论,我们可以通过画图,来分析一下他们的在底层上的细微差别。
这个是Demo16的
在这里插入图片描述
这个是Demo17的
在这里插入图片描述
通过我们的画图分析,我们发现第一种的情况是比较好的,相较于 第二种。
第二种还有一种写法,其实在上一篇博客中写过了,我们再拿过来看看。

/**
 *
 * @author admin
 * @date 2024/11/25
 * @Description 
 */
public class Demo15 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0 ;i<50000;i++){
                count++;
            }
        });

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

这个代码不就是串行执行吗,t1一直在join阻塞等待,直到t1结束以后,t2才开始执行,跟上面synchronized的效果是一样的。
解释完第一个,那我们就看一下第二个吧。
2. 把锁对象类型进行变化,然后观察一下现象是否发生改变
我们把锁对象都换成t1,看看情况如何。

/**
 * @Author: XXHH
 * @CreateTime: 2024-12-05
 */
public class Demo18 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            Thread thread = Thread.currentThread();
            for (int i = 0; i < 500000; i++) {
                synchronized (thread){
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 500000; i++) {
                synchronized (t1){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count:"+count);
    }
}

在这里插入图片描述
在这里插入图片描述

2.3 synchronized的其他写法

我们之前再讲String的时候,提到过StringBuilder和StringBuffer这两个类的时候,我们讲到了StringBuilder是不安全的,StringBuffer是安全的。我们现在观察一下他们的源码,从哪里可以看到是否安全?
在这里插入图片描述

在这里插入图片描述
根据上面的截图或者是大家看源码,我们可以发现,StringBuffer 的主要方法都有synchronized关键字,而StringBuilder则没有,因此StringBuffer 可以有效保证线程安全。
当然我们看这个源码还有一个用途,我们发现synchronized可以修饰方法,我们也可以把上面的代码改成方法的形式。

/**
 * @Author: XXHH
 * @CreateTime: 2024-12-05
 */
class Counter{
    private int count = 0;
    synchronized public void add(){
        count++;
    }
    public int getCount(){
        return count;
    }
}
public class Demo18 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 500000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 500000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count:"+counter.getCount());
    }
}

在这里插入图片描述

我们为此直接创建一个Counter类,里面实现一个具有synchronized的add方法,然后观察现象。
在这里插入图片描述
同理我们也可以为静态方法来使用synchronized进行修饰。

/**
 * @Author: XXHH
 * @CreateTime: 2024-12-05
 */
class Counter{
    // private int count = 0;
    public static int count;
    /*synchronized public void add(){
        count++;
    }
    public int getCount(){
        return count;
    }*/
    public synchronized static void add(){
        count++;
    }
    public static int getCount(){
        return Counter.count;
    }
}
public class Demo19 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 500000; i++) {
                Counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 500000; i++) {
                synchronized (Counter.class){
                    Counter.count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count:"+Counter.getCount());
    }
}

在这里插入图片描述
我们主要是来观察

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

	synchronized (Counter.class){
		Counter.count++;
	}

这俩段代码,通过观察我们发现,效果一样。
在这里插入图片描述

2.4 synchronized的特性

分为下面三个,互斥刷新内存可重入

2.4.1 互斥

前面的所有代码产生的效果,都是来源于互斥,我们可以用一个例子,形象的比喻一下,我们欢迎我们的助教老师 — 滑稽老铁
现在有很多滑稽老铁,都要去上厕所,但是只有一个卫生间,首先 滑稽老铁A 先进入到 卫生间,为了防止他人偷窥,插入了一把锁(synchronized),剩余的滑稽老铁只能阻塞等待,等到滑稽老铁A上完了,锁释放了,其余的滑稽老铁蜂拥而至,谁先抢到,谁就先进去,可以不遵守先来后到,这就是整个过程。

在这里插入图片描述
在这理解一下阻塞等待
针对每一把锁,操作系统内部维护了一个等待队列,当这个锁被某个线程占有的时候,其他线程尝试进行加锁,就加不上了,就会阻塞等待,一直等到之前的线程解锁之后,由操作系统唤醒一个新的线程,再来获取到这个锁。
注意

  • 上一个线程解锁之后,下一个线程并不是立即就能获取到锁,而是要靠操作系统来 “唤醒”,这也就是操作系统调度的一部分工作。
  • 假设有 A B C 个线程,线程A 先获取到锁,然后 B 尝试获取锁,然后 C 再尝试获取锁,此时B和C都在阻塞队列中排队等待,但是当A释放锁之后,虽然B比C先来的,但是B不一定就能获取到锁,而是要和C重新竞争,并不遵守先来后到的规则。

synchronized的底层是使用操作系统的 mutex lock 来实现的。

2.4.2. 刷新内存

synchronized的工作过程:
(1) 获得互斥锁
(2) 从主内存拷贝变量的最新副本到工作的内存
(3) 执行代码
(4) 将更改后的共享变量的值刷新到主内存中。
(5) 释放互斥锁
所以 synchronized 也能保证内存可见性,具体的代码请看下一个博客volatile部分。

2.4.3. 可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
例如我们写个代码来观察一下。

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

在这里插入图片描述
大家一看到这种代码,毋庸置疑,觉得程序员有毛病,这种问题都会犯。但是如果是这样写呢?

/**
 * @Author: XXHH
 * @CreateTime: 2024-12-05
 */
class Counter2{
    private int count;
    synchronized public void add(){
        count++;
    }
    public int getCount(){
        return count;
    }
}
public class Demo20 {
//    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Counter2 counter2 = new Counter2();
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0;i<50000;i++){
                /*synchronized (locker){
                    synchronized (locker) {
                        count++;
                    }
                }*/
                synchronized (locker){
                    counter2.add();
                }
            }
        });
        t1.start();
        t1.join();
        System.out.println("count:"+counter2.getCount());
    }
}

在这里插入图片描述
这两段代码块,在不同的位置上,都使用了 synchronized 关键字,这就不容易发现问题。
我们还是看一下用locker和locker2两个锁的情况吧,分析一下

  1. 第一次进行加锁操作,能够成功 (锁没有人使用)
  2. 第二次进行加锁,此时意味着,锁对象已经被占用了,第二次加锁,就会触发阻塞等待

要想解除阻塞,需要往下执行才可以,但是要想往下执行,就需要等到第一次的锁被释放,出现这样的问题,称之为“死锁”。
根据我们的分析,上面的代码会出现严重的bug,但是执行成功,说明JVM对 synchronized 引入了可重入的功能和概念。

我们来分析一下,JVM是如何得知的此处是可重入的。

/**
 * @Author: XXHH
 * @CreateTime: 2024-12-05
 */
class Counter2{
    private int count;
    synchronized public void add(){
        count++;
    }
    public int getCount(){
        return count;
    }
}
public class Demo20 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Counter2 counter2 = new Counter2();
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0;i<50000;i++){
                synchronized (locker){
                    synchronized (locker) {
                        synchronized (locker) {
                            count++;
                        }
                    }
                }
             }
        });
        t1.start();
        t1.join();
        System.out.println("count:"+count);
    }
}

其实是 JVM 是 先引入一个变量,计数器 0, 每次触发 { 的时候,把计数器 ++,每次触发 } 的时候,把计数器 - -,当计数器- - 到 0 的时候,就是真正需要解锁的时候。
JVM 中如何区分 synchronized 的 大括号呢?
{ } 只是 我们看Java 代码的角度理解的,
JVM 看到的是对应的字节码。
字节码中,对应的是不同的指令 { 涉及到加锁指令,} 对应到解锁指令
当然了 if while 的 { } 不会被编译成加锁解锁的指令。
综上,可重入锁的实现原理,关键在于让锁对象,内部保存,当前是线程持有的这把锁。后续有线程针对这个锁加锁的时候,对比一下,所持有者的线程和当前加锁的线程是否是同一个。
在这里插入图片描述


写一篇我们讲解 死锁问题,我们不见不散!

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

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

相关文章

阿尔法linux开发板ping不通百度

我使用的阿尔法linux板子&#xff0c;发现按照《03【正点原子】I.MX6U网络环境TFTP&NFS搭建手册V1.3.2》一套操作下来&#xff0c;还是没办法实现板子上网。 我总结了下面方法&#xff0c;我如何实现联网和互ping通&#xff0c;大致总结下三步 一、pc端的wifi网络&#xf…

Qt之屏幕录制设计(十六)

Qt开发 系列文章 - screencap&#xff08;十六&#xff09; 目录 前言 一、实现原理 二、实现方式 1.创建录屏窗口 2.录屏窗口类定义 3.自建容器对象定义 4.用户使用 5.效果演示 总结 前言 利用Qt实现屏幕录制设计&#xff0c;可以通过使用Qt自带的类QScreen、QPixma…

AI通过数据构建一个独有对话机器人

AI通过数据构建一个独有对话机器人&#xff0c;尝试构建快速构建专有知识的机器人。 前端使用tinker实现一个简单的对话窗口&#xff0c; 后端使用自己的数据进行不断的训练&#xff0c;有需要的可以依据自己的实际情况进行修改&#xff0c;和优化 import tkinter as tk fro…

xml格式化(1):使用python的xml库实现自闭合标签

前言 最近一段时间一直想要写一个urdf格式化插件。 至于为什么嘛&#xff0c;因为使用sw2urdf插件&#xff0c;导出的urdf&#xff0c;同一标签的内容&#xff0c;是跨行的&#xff0c;这就导致&#xff0c;内容比较乱&#xff0c;而且行数比较多。影响阅读。 因此&#xff…

【免费】2004-2019年各省规模以上工业企业RD经费面板数据

2004-2019年各省规模以上工业企业R&D经费面板数据 1、时间&#xff1a;2004-2019年 2、来源&#xff1a;国家统计局、统计年鉴 3、指标&#xff1a;行政区划代码、地区、年份、规模以上工业企业R&D经费(万元) 4、范围&#xff1a;31省 5、规模以上工企&#xff0c…

电路学习(一)之电阻

电阻在电路中具有限制电流、分流、分压等功能&#xff0c;是电路中必不可少的组成部分。 1.什么是电阻&#xff1f; 电阻是一种符合欧姆定律&#xff08;R&#xff09;、限制电流流动的线性元件。简单来说&#xff0c;电阻就是可以限制电流流过的电子器件&#xff0c;其主要功…

Facebook元宇宙项目中的智能合约应用:提升虚拟空间的自治能力

近年来&#xff0c;Facebook在元宇宙领域的探索引起了广泛关注。元宇宙是一个融合虚拟现实&#xff08;VR&#xff09;、增强现实&#xff08;AR&#xff09;和互联网的沉浸式数字空间。在这个过程中&#xff0c;智能合约技术被认为是提升虚拟空间自治能力的关键工具。通过自动…

SSR 【1】【nuxt安装】

文章目录 前言如何解决 前言 nuxt提供了nuxi脚手架工具&#xff0c;让开发者便捷生成nuxt模板项目。nuxt官网 npx nuxilatest init <project-name>但是几乎大部分的人在安的时候都会遇到这个问题 如何解决 在C:\Windows\System32\drivers\etc\hosts中增加如下解析记录…

mv指令详解

&#x1f3dd;️专栏&#xff1a;https://blog.csdn.net/2301_81831423/category_12872319.html &#x1f305;主页&#xff1a;猫咪-9527-CSDN博客 “欲穷千里目&#xff0c;更上一层楼。会当凌绝顶&#xff0c;一览众山小。” 目录 基本语法 主要功能 常用选项详解 1. …

【APP】5分钟上手基于BurpSuite的APP抓包

step 1 手机和电脑连上同一个wifi step 2 ipconfig -all查看电脑在WLAN下的IP 这里为10.0.23.80 step3 bp设置监听的端口和ip&#xff0c;ip设置为上一步看到的ip step4 bp导出证书 der后缀改为cer 传给手机 step5 在设置中搜索证书&#xff0c;按步骤安装证书 step6 在…

【工业场景】用YOLOv8实现工业安全帽识别

工业安全帽识别是一项重要的工作安全管理措施&#xff0c;旨在防止工作场所发生头部伤害事故。通过使用YOLOv8等深度学习模型&#xff0c;可以实时准确地检测出工人是否佩戴安全帽&#xff0c;及时发现违规行为&#xff0c;为工人提供更安全的工作环境。 使用YOLOv8实现工业安全…

51单片机——共阴数码管实验

数码管中有8位数字&#xff0c;从右往左分别为LED1、LED2、...、LED8&#xff0c;如下图所示 如何实现点亮单个数字&#xff0c;用下图中的ABC来实现 P2.2管脚控制A&#xff0c;P2.3管脚控制B&#xff0c;P2.4管脚控制C //定义数码管位选管脚 sbit LSAP2^2; sbit LSBP2^3; s…

云安全博客阅读(二)

2024-05-30 Cloudflare acquires BastionZero to extend Zero Trust access to IT infrastructure IT 基础设施的零信任 不同于应用安全&#xff0c;基础设置的安全的防护紧急程度更高&#xff0c;基础设施的安全防护没有统一的方案IT基础设施安全的场景多样&#xff0c;如se…

深入探讨 Android 中的 AlarmManager:定时任务调度及优化实践

引言 在 Android 开发中&#xff0c;AlarmManager 是一个非常重要的系统服务&#xff0c;用于设置定时任务或者周期性任务。无论是设置一个闹钟&#xff0c;还是定时进行数据同步&#xff0c;AlarmManager 都是不可或缺的工具之一。然而&#xff0c;随着 Android 系统的不断演…

SAP销售订单的计划行类别是什么?销售订单是如何传递需求给MRP的?

文章目录 一、销售订单计划行类别的参数二、销售订单的项目类别的配置VOV4三、计划行类别的配置VOV6四、对销售订单项目类别分配计划行类别VOV5五、自定义计划行类别 【SAP系统PP模块研究】 #SAP #SD #PP #计划 #需求传递 一、销售订单计划行类别的参数 销售订单主体包括Head…

英伟达 RTX 5090 显卡赋能医疗大模型:变革、挑战与展望

一、英伟达 RTX 5090 与 RTX 4090 技术参数对比 1.1 核心架构与制程工艺 在探讨英伟达 RTX 4090 与 RTX 5090 的差异时&#xff0c;核心架构与制程工艺无疑是最为关键的基础要素&#xff0c;它们从根本上决定了两款显卡的性能上限与应用潜力。 1.1.1 核心架构差异 RTX 4090…

Web渗透测试之XSS跨站脚本 原理 出现的原因 出现的位置 测试的方法 危害 防御手段 面试题 一篇文章给你说的明明白白

目录 XSS介绍的原理和说明 Cross Site Scripting 钓鱼 XSS攻击原理 XSS漏洞出现的原因&#xff1a; XSS产生的原因分析 XSS出现位置&#xff1a; XSS测试方法 XSS的危害 防御手段&#xff1a; 其它防御 面试题: 备注&#xff1a; XSS介绍的原理和说明 嵌入在客户…

【C++】字符串与字符数|组操作详解:strcpy 和 strcat 的使用与解析

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;一、字符串数组的基本操作&#x1f4af;二、strcpy 的用法详解1. strcpy 的功能与原型2. 使用示例与代码演示3. 注意事项4. 扩展&#xff1a;为什么不能直接用 &#xff1f…

玩机搞机基本常识-------列举安卓机型一些不常用的adb联机命令

前面分享过很多 常用的adb命令&#xff0c;今天分享一些不经常使用的adb指令。以作备用 1---查看当前手机所有app包名 adb shell pm list package 2--查看当前机型所有apk包安装位置 adb shell pm list package -f 3--- 清除指定应用程序数据【例如清除浏览器应用的数据】 …

UI自动化测试保姆级教程①

欢迎来到阿妮莫的学习小屋慢也好&#xff0c;步子小也好&#xff0c;在往前走就好 目录 自动化测试 简介 作用 分类 优缺点 优点 缺点(误区) UI自动化测试 自动化测试使用场景 自动化测试实现时间 Selenium框架 特点 Web自动化测试环境部署 Selenium包安装 浏览…