使用synchronized 加锁你加对了么?

news2024/11/26 8:41:58

本文讲解使用synchronized只是对synchronized的使用,底层原理将在后续文章

目录

从实际中理解共享带来的问题

Java代码实现共享带来的问题进行分析

临界区(Critical Section) 与 竞态条件(Race Condition)

临界区

竞态条件

synchronized解决方案

怎么理解synchronized中的 同步 与 互斥

理解synchronized关键字

synchronized加在方法上

从实际中理解共享带来的问题

有一个实际的例子 比如 老王有一个功能非常强大的算盘(操作系统),但是由于自己忙于工作,没有时间用,放着也是放着,于是就想把这个算盘租出去赚点钱. 于是有两个同学小明和小红就像租用.

好接下来,老王就把算盘租给小明用,但是由于小明在一些特殊情况没有使用,比如 睡觉的时候(sleep),吃饭的时候(比如读文件->InputStream),在比如上厕所的时候(写文件-->OutputStream)这种阻塞IO操作,还有的时候会放松一下(wait操作),这些时间小明都会进行阻塞状态.也就是这些时间内小明并没有使用算盘,而是占用着.

于是,老王就会想我这一天都把算盘租给你自己了,那么赚的钱还少,于是老王就让小红用一会,小明用一会.这样就能在相同的时间赚够足够的钱了.

由于小红和小明在计算过程中,中间的计算结果过多,导致两个人脑容量(工作内存)不足计算的结果记不住,于是又去申请了一个笔记本,来记录中间结果.

比如此时小明计算的结果为1,小红计算的结果为-1,所以合起来结果为0.

但是由于分时系统的原因(小明用一会,小红用一会-->上下文切换),就会导致这个最终的计算结果出现问题

比如 小明用了一会算盘刚算出结果,就是要+1,但是还没有写到笔记本中,老王就会给他说,你的时间到了(CPU时间片用光了),于是就给小红进行计算,从笔记本拿到值为0,小红计算的是要将其结果-1,所以为0-1=-1,于是将-1写到笔记本中,小红写到笔记本之后,小红就算完了,老王就会让小明继续算,小明刚才算出来了于是就把写过写到笔记本上,现在最终结果为1.

这就出现问题了,我们想要的最终结果为0,但是由于分时系统的原因(上下文切换的原因),在多个人(线程)协作计算结果的时候,就会出现的结果并不是我们所预期的那样.--->这就是线程安全问题

Java代码实现共享带来的问题进行分析

上述的操作我们用Java代码进行实现,就是两个线程对共享变量执行++操作.我们看如下代码 :

public static int counter = 0;
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(()->{
        for(int i =0;i<5000;++i){
            counter++;
        }
    });
    Thread t2 = new Thread(()->{
        for(int i =0;i<5000;++i){
            counter--;
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter);
}

以上出现的结果是不可预测的,可能是正数也可能是负数,也可能是0.

这个问题的根本原因就是counter++ 和 counter-- 不是原子操作,也就是一个++,--操作会分为多个指令.

要想理解这个就需要从JVM的字节码角度去理解 :

对于 ++ 操作会执行以下指令 :

  • getstatic counter // 获取静态变量counter的值
  • iconst_1 //准备常量1
  • iadd //进行自增操作
  • putstatic counter //将修改后的值存入静态变量counter

对于 -- 操作会执行以下指令 :

  • getstatic counter //得到静态变量counter的值
  • iconst_1 //准备常量1
  • isub //进行自减操作
  • putstatic counter //把修改后的值存入静态变量counter

在Java中,会在Java内存模型(JMM)中的主内存和工作内存之间进行数据交换.

但是,如果是在单线程的场景下,不会进行交错的情况,按照代码的顺序执行,所以不会发生非0的情况,结果就为0.

正常情况 :

对于单线程条件下,不会出现指令交错的情况,都是一步一步按照代码的顺序执行,指令之间不会出现交错,所以计算的结果是我们所预期的那样

出现负数的情况 :

如果是多线程可能出现负数,也可能出现正数.(线程1执行i++操作,线程2执行i--操作)

我们先来看负数的情况 :

线程2读取静态变量,然后准备常数1,然后对i做--操作,i变为-1,但是还没有把结果写入到变量i中,这个时候CPU时间片用完了,进行线程上下文切换,切换到线程1,然后线程1也是按照指令进行执行从静态变量中读取,读取的是0,然后准备常数1,在执行自增操作i变为1,写入到i变量中,这时线程1执行完了,又进行线程的上下文切换,然后线程2接着刚才没有执行的操作继续执行,于是将其-1写入到变量i中.最终i的值不是我们预期的0,而是-1--->出现负数情况

出现正数的情况 :

线程1取静态变量,值为0,然后准备常数1,然后对i做++操作,i变为1,但是还没有把结果写入到变量i中,这个时候CPU时间片用完了,进行线程上下文切换,切换到线程2,然后线程2也是按照指令进行执行从静态变量中读取,读取的是0,然后准备常数1,在执行自减操作i变为-1,写入到i变量中,这时线程2执行完了,又进行线程的上下文切换,然后线程1接着刚才没有执行的操作继续执行,于是将其1写入到变量i中.最终i的值不是我们预期的0,而是1--->出现正数情况

临界区(Critical Section) 与 竞态条件(Race Condition)

临界区

什么情况下会出现问题 ? 什么情况下不会出现问题呢 ?

  • 当我们一个程序运行多个线程本身是没有线程安全问题的.
  • 多个线程访问共享资源就会出现线程安全问题
  1. 多个线程读共享资源不会出现线程安全问题
  2. 多个线程对共享资源进行读写操作的时候就会发生指令交错,就会出现线程安全问题

怎么理解临界区呢 ?

一段代码块中,如果存在多个线程对共享变量进行读写操作时候,这个代码块就被我们称为临界区.

public static int counter = 0;//共享变量

public static void increment()
// 临界区
{
    counter ++;
}

public static void decrement()
//临界区
{
    counter --;
}

竞态条件

就是多个线程在临界区执行代码,但是由于代码的执行序列不同而导致出现结果不符合预期就为竞态条件.

还是拿上面的代码counter为例 :

  • counter在第一个线程for循环里面用到,发生结果不可预测的情况这就说明发生了竞态条件
  • 同理 : counter在第二个线程for循环里面用到,发生结果不可预测的情况这也就说明发生了竞态条件

synchronized解决方案

解决多个线程访问共享变量对其进行操作(线程安全问题)有两种解决方案可以解决:

1.阻塞式的解决方案 : synchronized,Lock --->加锁

2.非阻塞的解决方案 : 原子变量

我们先来看看synchronized是怎么进行解决的.

synchronized又称为对象锁,它是采用互斥的方式能够在同一时刻只能有一个线程持有[对象锁],其他线程在想获取这个[对象锁]时候就会阻塞(进入阻塞状态),这样就能够保证拥有锁的线程就可以安全的执行临界区的代码.-->不用在关心线程的上下文切换.

怎么理解synchronized中的 同步 与 互斥

在java中 互斥 和 同步都是可以利用synchronized来实现的.

互斥就是保证临界区的代码发生竞态条件(多个线程读写共享变量),同一时刻只能够有一个线程执行临界区的代码.

同步就是由于线程的执行先后,顺序不同,就需要一个线程等待其他线程运行到某一个点.

理解synchronized关键字

synchronized(对象(多个线程共享的对象)){
    //临界区
}

我们来实际通过代码先简单理解一下synchronized关键字

比如现在有两个线程(线程1,线程2)来竞争锁,比如线程1先获取到对象锁(锁的持有者),线程2没有获取到这个对象锁只能阻塞等待(阻塞状态->blocked状态,因为同一时刻只能有一个线程持有对象锁),当线程1执行完临界区的代码->执行完synchronized代码块了,线程1就会释放该对象锁,同时唤醒线程2(唤醒阻塞状态的线程,从blocked状态->可运行的状态). 再由线程2来执行临界区的代码

通过这种互斥方式,在同一时刻只能有一个线程持有对象锁,其他线程竞争这个锁都会进行阻塞.那么当线程执行临界区的代码的时候,就会安全的执行,临界区的代码就相当于是串行执行的

(只有一个线程执行完毕了,下一个线程才能执行)

public static int counter = 0;//共享变量counter
public static Object locker = new Object();//共享的锁对象

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(()->{
        for(int i =0;i<5000;++i){
            synchronized (locker){//加锁
                //临界区
                counter++;
            }
        }
    });
    Thread t2 = new Thread(()->{
        for(int i =0;i<5000;++i){
            synchronized (locker){//加锁
                //临界区
                counter--;
            }
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter);
}

再次利用图解的方式来深入仔细理解synchronized

还是之前的例子 : 老王想租这个算盘,只有进入这个房间才能进行计算,现在有3个人想租这个算盘

我们可以将这个synchronized(对象),这个线程共享的对象看做是一个房间,这个房间一次只能有一个人进入(持有对象锁),其他人只能在门外等着(运行状态->阻塞状态),可以把门外的三个人想象成3个线程.

  • 当t1人进入到房间(线程t1执行到synchronized(对象)),t1人就拿到这个房间的钥匙了(t1线程持有当前对象锁),进入房间后就会拿算盘计算(执行临界区的代码).
  • 这个人也想进入到这个房间发现这个房间已经被锁住了,无法进入(其他线程t2,t3从运行状态->阻塞状态(上下文切换)),只能在门外等着(阻塞等待)
  • 这个时候如果t1这个人用的算盘时间到了(CPU时间片用完了),他就会被老板赶出去,但是这个时候门还是锁住的,只有t1这个人有这个房间的钥匙,t2,t3这两个人还是在门外等着(t2,t3没有钥匙,只能阻塞等待),只有下次再次轮到t1的时候才能进入房间(下次任务调度器再次调度t1的时候,才会获取CPU时间片,继续持有锁,继续执行临界区代码)
  • 当t1这个人全部计算完毕之后(t1线程执行完synchronized代码块了),就会先去解开房间的锁(t1线程释放锁),同时也会告诉t2,t3说我用完算盘了,你们可以用了(同时也会唤醒t2,t3线程),t2人和t3人会竞争算盘的使用(t2,t3线程会竞争锁,然后继续...,竞争到锁之后就可以执行临界区代码,然后没有竞争到的就会阻塞等待).

再次通过流程图理解synchronized

c5260b7bdbe7b2e95eef8057d16141fa.png

理解synchronized关键字保证原子性

怎么理解原子性呢 ?

原子性就是不可分割的整体,在代码执行的中间过程不会被打断.

理解synchronized保证原子性

synchronized就是利用对象锁保证了临界区内代码的原子性.->临界区的代码对外是不可分割的,不会被线程切换所打断.

就比如 count++,在JVM字节码这一行代码分为4个指令,这四个指令在一个线程执行的时候,四个指令必须都执行,不会出现在多线程的场景下因为线程上下文切换,而导致发生指令交错的情况.

怎么理解把synchronized(obj)放在for循环的外面呢 ?

我们经过前面的学习知道 ++,--这样的操作符是包含4个指令的,所以对整个for循环加锁就相当于 整个for循环的指令(5000*4)这么多指令都是原子的---->对for循环里面的指令做了原子保护.--->在执行的期间不会有其他线程来干扰

怎么理解如果把t1位synchronized(obj1) 而synchronized(obj2) 会怎么运作呢 ?

Thread t1 = new Thread(()->{
    synchronized (locker1){//加锁
        for(int i =0;i<5000;++i){
            //临界区
            counter++;
        }
    }
});
Thread t2 = new Thread(()->{
    synchronized (locker2){//加锁
        for(int i =0;i<5000;++i){
            //临界区
            counter--;
        }
    }
});

这样还是不能保证原子性,因为保护的是不同对象,不起互斥效果,就好像是t1去的是一个房间,而t2又去的另外一个房间,保护的是不同的资源-->不保证原子性->不互斥

这就给我们一个提示在进行加锁的时候,多个线程读写共享变量的时候,一定要进一个房间-->一定要对相同的对象加锁-->多个线程的锁对象一定要相同

多个线程读写共享资源,一定锁的是同一个对象,不能锁不同的对象.

如果t1synchronized(obj) 而 t2没有加锁,又该如何理解呢 ?

Thread t1 = new Thread(()->{
    for(int i =0;i<5000;++i){
        synchronized (locker){//加锁
            //临界区
            counter++;
        }
    }
});
Thread t2 = new Thread(()->{
    for(int i =0;i<5000;++i){
        //临界区
        counter--;
    }
});

这个还是不能保证原子性,因为在t1线程持有锁的时候进行上下文切换(比如CPU时间片用完了),线程t2不会尝试获取锁,不会进行阻塞,会接着继续执行,所以不能保证原子性.

这也给了我们提示,当多个线程读写同一个共享变量的时候,也就是要保护临界区的代码,每一个线程都要对其加锁

总结 :

当多个线程进行读写共享变量的时候,每一个线程都必须进行加锁,并且锁对象必须是同一个,才可以其互斥效果,保证一个线程在执行临界区代码的时候,其他线程不会干扰---->保证了原子性

面向对象改进

我们利用面向对象的思想对上面代码进行改进,我们不自己new一个object对象了,我们自己创建一个对象(room对象也为锁对象)来保护共享资源counter,类的内部封装自增,自减方法.

//自己封装一个类,来保护共享资源-->Room就是共享的锁对象-->对共享资源进行保护
class Room{
    private int counter;//共享资源counter-->要对齐进行++,--操作
    //提供自增的方法对++操作进行保护
    public void increase(){
        synchronized (this){//给当前对象加锁-->当前对象为锁对象
            counter++;
        }
    }
    //提供自减方法对--操作进行保护
    public void decrease(){
        synchronized (this){//给当前对象加锁-->当前对象为锁对象
            counter--;
        }
    }
    //获取的时候,也需要加锁,为了保证获取的是准确地结果,而不是中间的结果
    public int getCounter() {
        synchronized(this){
            return counter;
        }
    }
}

public class IOPSynchronizedDemo {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();//利用room对象(锁对象)来保护共享资源
        Thread t1 = new Thread(()->{
            for(int i =0;i<5000;++i){
                room.increase();
            }
        },"t1");

        Thread t2 = new Thread(()->{
            for(int i =0;i<5000;++i){
                room.decrease();
            }
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(room.getCounter());
    }
}

把互斥的逻辑都封装在Room来内部.对外只需要调用自增自减的这些方法,对共享资源进行保护由内部来实现

synchronized加在方法上

synchronized加在成员方法上

synchronized加在成员方法上就相当于对当前对象this加锁

synchronized加在静态方法上

synchronized加载静态方法上就相当于对类对象加锁


参考 : 黑马程序员JUC视频-->那老师真的超级牛x,原理讲的非常透彻,并发和JVM还有Spring原码讲的最好的-->爱死你了牛B满老师

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

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

相关文章

【web前端期末大作业】基于HTML+CSS+JavaScript实现代理商销售管理系统后台(8页)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

mathtype在word内的简单使用

一、简单使用 1、快捷键 快捷键说明ctrlaltQ行内公式altshiftQ右编号&#xff08;行间公式&#xff09;\ ;{空格、大括号都需要转义ALTF4关闭mathtype窗口 2、小技巧 \left与\right 一定要配对使用&#xff0c;且对于对称的符号&#xff08;如(), {}等&#xff09;来说&…

【Linux】---进程控制(创建、终止、等待、替换)

文章目录进程创建fork()进程退出进程退出场景进程退出方法退出码exit、_exit进程等待进程等待的方法waitwaitpid阻塞和非阻塞进程替换替换的原理替换所用到的函数execlexeclpexecle简易的shell进程创建 fork() fork函数在之前的文章中也已经提到过了。其主要作用是从已存在的…

excel提示stdole32.tlb的解决方法

大家在使用excel时有遇到stdole32.tlb错误提示吗&#xff1f;出现这个问题直接导致excel无法启动&#xff0c;非常影响用户的工作效率。为了顺利解决问题&#xff0c;小编给大家带来了详细的解决办法&#xff0c;希望可以帮到你。 win7系统打开excel提示stdole32.tlb的解决方法…

Nosql inject注入

0x00 Nosql inject 最近主要在看那个 YApi 的注入漏洞&#xff0c;也是一个 mongodb的注入 所以来写一下这个东西&#xff0c;其实现在越来越常见的Nosql注入 感觉很多分布式和一些新的系统已经大量使用这种nosql数据库&#xff0c;这个注入和传统的关系型数据库有一点点不同…

【Hack The Box】linux练习-- Meta

HTB 学习笔记 【Hack The Box】linux练习-- Meta &#x1f525;系列专栏&#xff1a;Hack The Box &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年11月27日&#x1f334; &#x1f36d…

[附源码]计算机毕业设计springboot“科教兴国”支教门户网站

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

复旦MBA项目GNW海外课程|连线全球顶尖商学院,跨时空学习精彩无限!

10月下旬&#xff0c;复旦MBA为期一周的GNW海外课程落下帷幕&#xff0c;复旦在职MBA的同学们通过云端连线&#xff0c;走进全球多所顶级商学院&#xff0c;与深谙商道、学术造诣深厚的教授学者&#xff0c;以及来自不同地域不同文化背景的精英学生取经论道。      复旦MBA…

深入理解死锁问题

死锁问题&#x1f3de;️1. 死锁概念&#x1f301;2. 为什么发生死锁&#x1f320;3. 产生死锁的条件&#x1f301;4. 如何避免死锁&#x1f4d6;4.1 循环等待&#x1f4d6;4.2 持有并等待&#x1f4d6;4.3 非抢占&#x1f4d6;4.4 互斥&#x1f33f;5. 通过调度避免死锁&#…

【Python开发】一文详解Flask-Login

一文详解Flask-LoginFlask-Login 为 Flask 提供用户会话管理。它处理登录、注销和长时间记住用户会话等常见任务。 Flask-Login 不绑定到任何特定的数据库系统或权限模型。唯一的要求是您的 用户对象实现一些方法&#xff0c;并且您向能够 从用户 ID 加载用户 的扩展提供回调。…

Kotlin 开发Android app(十二):Android布局FrameLayout和ViewPager2控件实现滚动广告栏

在上一节中我们简单的介绍了RecyclerView 的使用&#xff0c;他是整个开发的重点控件&#xff0c;这一节我们来看看FrameLayout 布局结合ViewPager2&#xff0c;开发一个广告控件。 新模块banner 先创建一个新的模块&#xff0c;取名为banner&#xff0c;用来创建我们的滚动广…

Spring Boot自定义Namespace

Spring Boot 自定义Namespace 在学些Spring Boot 自定义Namespace之前&#xff0c;先来看一个简单的案例。在Spring Boot出现之前&#xff0c;所有的bean都是在XML文件的格式 中定义。为了管理方便&#xff0c;一些大型复杂的应用系统&#xff0c;通常定个多个xml文件来共同满…

【笑小枫的按步照搬系列】JDK8下载安装配置

笑小枫&#x1f495; 欢迎来到笑小枫的世界&#xff0c;喜欢的朋友关注一下我呦&#xff0c;大伙的支持&#xff0c;就是我坚持写下去的动力。 微信公众号&#xff1a;笑小枫 笑小枫个人博客&#xff1a;https://www.xiaoxiaofeng.com 一、安装 1、方式一&#xff1a;进入官网…

Apifox:成熟的测试工具要学会自己写接口文档

好家伙&#xff0c; 在开发过程中&#xff0c;我们总是避免不了进行接口的测试&#xff0c; 而相比手动敲测试代码&#xff0c;使用测试工具进行测试更为便捷&#xff0c;高效 今天发现了一个非常好用的接口测试工具Apifox 相比于Postman&#xff0c;他还拥有一个非常nb的功…

读《基于深度学习的跨视角步态识别算法研究》

2020 背景&#xff1a; 作为一种新兴的识别技术&#xff0c;步态识别具有在非受控、远距离、低分辨率的场景下进行身份识别的优点&#xff0c;并且步态不易改变和伪装&#xff0c;所以近年来得到的关注逐渐增多。 步态识别作为一种新兴的身份识别技术&#xff0c;可以根据人…

jsp美食管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 美食管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统采用serlvet dao bean mvc模式开发&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式 开发。开发环境为TOMCAT7.0,Myeclipse8…

ZYNQ之FPGA学习----UART串口实验

1 UART串口简介 UART串口基础知识学习&#xff1a;硬件设计基础----通信协议UART 2 实验任务 上位机通过串口调试助手发送数据给 Zynq&#xff0c;Zynq PL 端通过 RS232 串口接收数据并将接收到的数据发送给上位机&#xff0c;完成串口数据环回&#xff0c;管脚分配如下&…

软件测试的分类

这里先讲一些概念&#xff0c;改日从这里边挑几个细讲。&#xff08;给小白看的&#xff09; 按测试对象划分&#xff1a; 界面测试&#xff1a; 软件只是一种工具&#xff0c;软件与人的信息交流是通过界面来进行的&#xff0c;界面是软件与用户交流的最直接的一层&#xff…

基于二次近似(BLEAQ)的双层优化进化算法_matlab程序

参考文献如上。 双层优化问题是一类具有挑战性的优化问题&#xff0c;包含两个层次的优化任务。在这些问题中&#xff0c;下层问题的最优解成为上层问题的可能可行候选。这样的要求使得优化问题难以解决&#xff0c;并使研究人员忙于设计能够有效处理该问题的方法。尽管付出了…

Redis常见面试问题总结

文章目录Redis 基础面试说说你对Redis的了解?说说Redis中的数据类型&#xff1f;说说Redis数据类型对应的数据结构&#xff1f;说说Redis对应的Java客户端有哪些&#xff1f;说说Redis 中持久化发生了什么&#xff1f;说说Redis中持久化以及方式&#xff1f;如何理解Redis中RD…