【JavaEE 初阶(二)】线程安全问题

news2024/9/25 13:17:03

❣博主主页: 33的博客❣
▶️文章专栏分类:JavaEE◀️
🚚我的代码仓库: 33的代码仓库🚚
🫵🫵🫵关注我带你了解更多线程知识

在这里插入图片描述

目录

  • 1.前言
  • 2.synchronized
    • 2.1例子
    • 2.2synchronized修饰代码块
    • 2.3 synchronized修饰方法
    • 2.4synchronized特性
  • 3.死锁
    • 3.1死锁的成因
    • 3.2解决死锁
  • 4.volatile
    • 4.1内存可见性问题
    • 4.2volatile解决
  • 5.wait与notify
    • 5.1wait
    • 5.2notify
  • 6.总结

1.前言

在上一篇文章中,我们已经初步认识了线程的一些知识,但线程中一个重要问题就是线程安全问题,这篇文章我们就来了解为什么会引起线程安全问题,已经解决方法,有些代码在单个线程中执行是完全正常的,不会出现bug,但同样的代码,让多个线程,同一时间执行那么就可能出现bug,这就称为线程安全问题。


2.synchronized

2.1例子

例:我们让两个线程同时执行cou++操作,各自增5w,预期结构应该为10w,我们通过代码来观察是否符合预期结果。

public class Demo11 {
    public static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Object lock=new Object();
        //Object lock2=new Object();
        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();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count="+count);
    }
}

观察结果:
在这里插入图片描述
我们多次运行发现发现每次count的结尾都不等于10w,并且每次都不同。
出现这样的原因是为什么呢?其实是因为在执行cou++操作的时候有3步操作

load 把数据从内存读到cpu中
add 把寄存器+1
save 把寄存器中的数据保存到内存中
那么两个进程同时执行就会出现多种方式:
在这里插入图片描述
博主列出的只是部分执行情况,实际情况还有更多
在正确情况下:
在这里插入图片描述
错误情况下:
在这里插入图片描述
我们就可以知道线程安全的原因:
1.操作系统种线程的调度是随机的
2.两个线程对于同一个变量进行修改
3.修改操作不是原子性的
4.内存可见性问题
5.指令重排序问题
如果要想解决线程安全问题,就可以使修改操作变为原子性的,那么怎么变为原子性的呢?加锁操作。
最常见的加锁方法就是synchronized关键字。

2.2synchronized修饰代码块

在使用synchronized时,要搭配一个代码块{}进入{就会加锁,出了}就会解锁,我们用代码进行实现。
在这里插入图片描述
我们发现synchronized()报错,是因为()中需要表示一个用来加锁的对象,这两个对象是啥并不重要,重要的是通过这个对象来区分两个线程是否在竞争同一个锁,如果两个线程在针对同一个对象加锁就会出现锁竞争,那么由于锁的竞争,只有等一个线程解锁后,另一个线程才能再进行count++操作。

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

2.3 synchronized修饰方法

synchronized除了修饰代码块以外还可以修饰方法

class cunter{
    int count;
    public synchronized void count (){
        //相当于synchronized(this){}
        count++;
    }
//修饰静态方法
//    public static synchronized void count2 (){
     //相当于synchronized(cunter.class){}
//        count++;
//    }
}
public class Demo12 {
    public static void main(String[] args) throws InterruptedException {
        cunter counter=new cunter();
        Thread t1=new Thread(()->{
            for (int i=0;i<50000;i++){
                counter.count();
            }
        });
        Thread t2=new Thread(()->{
            for (int i=0;i<50000;i++){
                counter.count();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count="+counter.count);
    }
}

synchronized用的锁存在java对象头里面的,在一个java对象中,除了自己定义的属性和方法,还有一些自带的属性,这些自带的属性就称为对象头,其中就有属性表示当前对象是否加锁。

2.4synchronized特性

synchronized特性

1.互斥:某一个线程a如果执行某个某个对象的加锁操作时,如果其他线程也想给同一个对象加锁,那么就要 等a执行完成b才能实现加锁操作。
2.可重入:1个线程中,synchronized 代码块中针对同一把锁加锁多次,不会出现“死锁”问题。

public class Demo15 {
    public static void main(String[] args) {
        Object lock=new Object();
        Thread t1=new Thread(()->{
            synchronized (lock){
                synchronized (lock){
                    System.out.println("t1");
                }
            }//(1)
        });//(2)
        t1.start();
    }
}

上诉代码,在t线程如果第一次的lock加锁成功,又遇到了一个lock操作,但只有等第一次}(2)de的位置解锁才能执行加锁操作,可是如果使}(2)执行完,就需要先执行加锁操作,这样就导致代码一直注释,没有办法释放锁。所以就把synchronized设置为了“可重入锁”就解决了上述问题。
但此时又有了新的问题,如果在一个线程中,对一把锁多次加锁,那么在什么时候才释放锁呢?

public class Demo15 {
    public static void main(String[] args) {
        Object lock=new Object();
        Thread t1=new Thread(()->{
            synchronized (lock){
                synchronized (lock){
                 synchronized (lock){
                   synchronized (lock){
               		synchronized (lock){
               
                    }
                  }
                }
              }
            }
        });
        t1.start();
    }
}

要在这个线程的最外层才能释放锁,在锁对象中,不仅会记录是谁拿到了锁,还会记录加锁了多少次,每加锁一次,计数器++,解锁一次,计数器–,直到最后一个大括号结束。

3.死锁

在上述代码中,我们已经提到过死锁了,在1个线程中,针对一把锁连续加锁两次,如果是不可重入,就会出现死锁了。
如果是两个线程,两把锁(无论是不是可重入,都会死锁)
例如:(1)t1获取锁A,t2获取锁B (2)t1获取锁B,t2获取锁A

public class Demo13 {
    public static void main(String[] args) {
        Object lock1=new Object();
        Object lock2=new Object();
        Thread t1=new Thread(()->{
            synchronized (lock1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2){
                    System.out.println("t1线程");
                }
            }
        });
        Thread t2=new Thread(()->{
            synchronized (lock2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1){
                    System.out.println("t2线程");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

在t1线程,lock1锁中在等待lock2锁解锁,但此时lock2也在等待lock1解锁,此时会会两个线程一直僵持下去。
N个线程M把锁:哲学家问题
有5个哲学家坐在一起吃饭,但只有5根筷子,哲学家就只做两件事情,一件事情为思考,另一件事情就是吃饭,当其中一个哲学家要吃饭时,就会拿起左右两边的筷子,那么此时如果左右相邻的哲学家也想吃饭时,就需要等待正在吃饭的哲学家吃完饭,放下筷子,才能继续吃,在通常情况下,整个系统可以很好的运转,但是当5个哲学家同时拿起左边的筷子时,就会出现死锁问题.
在这里插入图片描述
死锁是一种严重的bug那么该如何解决死锁问题呢?我们就需要先了解死锁的成因。

3.1死锁的成因

1.互斥使用(锁的基本特性):当一个线程有一把锁时,另一个线程也想获取同一把锁就要阻塞等待。
2.不可抢占(锁的基本特性):当线程a拿到锁时,只有等线程a解除锁,线程b才能再使用。
3.请求保持:一个线程尝试获取多把锁
4.循环等待:等待的依赖关系形成了环。

3.2解决死锁

互斥和不可抢占性都是锁的基本特性,我们可以通过代码的结果来来避免写成“嵌套锁”但这个方案不一定好使,有的需求可能就是需要进行这种嵌套操作,所以我们最好
针对循环来解决,可以约定加锁条件避免形成循环等待,针对锁,约定加多把锁的时候,现加编号小的锁,再加编号大的锁并且所有线程都要遵守这一规则。

public class Demo13 {
    public static void main(String[] args) {
        Object lock1=new Object();
        Object lock2=new Object();
        Thread t1=new Thread(()->{
            synchronized (lock1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2){
                    System.out.println("t1线程");
                }
            }
        });
        Thread t2=new Thread(()->{
            synchronized (lock1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2){
                    System.out.println("t2线程");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

4.volatile

4.1内存可见性问题

计算机运行程序,经常要访问数据,这些数据往往存储在内存中,cpu使用这些变量的时候,要先从内存中读取数据,再对数据进行操作,cpu读取内存相对来说是非常慢的,cpu执行大部分操作都是非常快的,但一旦涉及到读取内存操作,就非常慢。为了解决上诉问题,此时编译器就可能对代码进行优化,把一些本来要读取内存的操作优化为读取寄存器,减少内存的读取次数就大大提高了程序的效率。
例:

public class Demo14 {
    public static int isQuit=0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            while (isQuit==0){

            }
            System.out.println("t1进程结束");
        });
        t1.start();
        Thread.sleep(1000);
        System.out.println("请输入isQuit");
        Scanner scanner=new Scanner(System.in);
        isQuit= scanner.nextInt();
    }
}

预期效果是当过了1s中后,输入isQuit为1,应该结束循环,输出t1进程结束。
我们来看一看实际结果:
在这里插入图片描述
很明显,实际结果和预期结果不一样,之前是两个进程修改同一个变量引起的bug,但现在是一个线程修改,另一个线程读,同样也引起了bug,是什么原因呢?
在t1线程中读取isQuit的值到寄存器中,通过cmp指令比较寄存器的值是否为0,由于这个循环执行的飞快,就需要多次从内存中load,再cmp,此时编译器就发现虽然进行了这么多次load但是load出来的结果没有任何变化,所以编译器就做了一个大胆的决定!只是第一次寻黄的时候读取内存,此后直接从寄存器中读取isQuit的值。它的初心虽然是好的,但是我此后如果修改了isQuit的值,但t1寄存器读取的仍然是isQuit修改前的值就出现了bug。这个问题就称为“内存可见性”问题。

4.2volatile解决

在多线程环境下,编译器对是否要进行优化的判定不一定准就需要通过volatile关键字告诉编译器我不需要优化!!!!

import java.util.Scanner;
public class Demo14 {
     public static volatile  int isQuit=0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            while (isQuit==0){

            }
            System.out.println("t1进程结束");
        });
        t1.start();
        Thread.sleep(1000);
        System.out.println("请输入isQuit");
        Scanner scanner=new Scanner(System.in);
        isQuit= scanner.nextInt();
    }
}

此时就可以结束线程1了:
在这里插入图片描述

5.wait与notify

5.1wait

wait是Object的一个方法,wait是使进程变为阻塞状态。
我们通过代码来进行演示:

public class Demo20 {
    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        System.out.println("wait之前");
        object.wait();
        System.out.println("wait之后");
        
    }
}

我们发现运行时依然有错:非法监视器状态异常,监视器就是指的sychronized。
在这里插入图片描述
wait在执行的时候只做三件事情:
1.释放锁资源
2.让线程进入阻塞状态
3.当线程被唤醒重新获取锁
对代码进行修改:

public class Demo20 {
    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        System.out.println("wait之前");
        synchronized (object){
            object.wait();
        }
        System.out.println("wait之后");
    }
}

在这里插入图片描述

5.2notify

这时我们会发现wait会一直持续等待,知道有其他线程调用notify唤醒它。
notify是一次唤醒一个进程,而notifyAll是一次唤醒所有进程。

public class Demo16 {
    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        Thread t=new Thread(()->{
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (object){
                System.out.println("进行通知");
                object.notify();
            }
        });
        t.start();
        System.out.println("wait之前");
        synchronized (object){
            object.wait();
        }
        System.out.println("wait之后");
    }
}

在这里插入图片描述
wait除了默认的无参版本,还有一个带参的版本,但参版本就是指定超时时间避免无休止等待。

6.总结

本篇文章主要介绍了sychronized加锁操作,死锁的成因,死锁的解决,内存可见性问题以及内存可见的解决方案,最后介绍了wait和notify的运用。

下期预告:多线程代码案例

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

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

相关文章

多模态路径:利用其他模态的无关数据改进变压器(CVPR 2024)

<Multimodal Pathway: Improve Transformers with Irrelevant Data from Other Modalities> 论文地址&#xff1a;https://arxiv.org/abs/2401.14405 项目网页&#xff1a;https://ailab-cvc.github.io/M2PT/ 开源代码&#xff1a;https://github.com/AILab-CVC/M2PT 讲…

天锐绿盾 | 公司防泄密软件

天锐绿盾是一款专为企业设计的图纸及文件加密防泄密软件&#xff0c;旨在保护企业的核心信息资产和知识产权。这款软件提供了强大的数据保护功能&#xff0c;通过透明加密技术&#xff0c;在不影响员工正常工作流程的前提下&#xff0c;自动对指定类型的文件&#xff08;如设计…

流程:采集1688店铺内有成交的商品列表||1688商品订单列表+订单详情API接口

此API目前支持以下基本接口&#xff1a; item_get 获得1688商品详情item_search 按关键字搜索商品item_search_img 按图搜索1688商品&#xff08;拍立淘&#xff09;item_search_suggest 获得搜索词推荐item_fee 获得商品快递费用seller_info 获得店铺详情item_search_shop 获得…

【intro】图卷积神经网络(GCN)

本文为Graph Neural Networks(GNN)学习笔记-CSDN博客后续&#xff0c;内容为GCN论文阅读&#xff0c;相关博客阅读&#xff0c;kaggle上相关的数据集/文章/代码的阅读三部分&#xff0c;考虑到本人是GNN新手&#xff0c;会先从相关博客开始&#xff0c;进一步看kaggle&#xff…

618有哪些你值得入手的?你值得入手的618好物清单

在618的折扣风暴中&#xff0c;我特别想向那些计划更新家电的朋友们推荐一些优质产品。家电作为日常生活中不可或缺的一部分&#xff0c;其品质与性价比尤为重要。而618正是购买家电的绝佳时机&#xff0c;但如何避免被次品所困&#xff0c;这就需要你手握这份618好物清单&…

《MySQL数据类型》

文章目录 一、理解数据本身就是一种约束1.tinyint类型和 tinyint unsigned类型2.其他的int类型 二、bit类型三、float类型1.signed版本注意2.unsigned版本 四、decimal类型float 和 decimal 总结五、char类型&#xff08;固定长度&#xff09;六、varchar类型&#xff08;可变长…

T型槽地轨承载力是如何连接整个制造过程的强力桥梁(北重公司设计)

T型槽地轨承载力的定义和计算 T型槽地轨是一种用于工业设备运输和装配的关键组件。它由世界上各行各业的生产商广泛采用&#xff0c;其有效的承载力使其成为连接整个制造过程的强力桥梁。本文将介绍T型槽地轨的承载力以及相关的设计要点和应用。 承载力的定义和计算 承载力是…

IDEA--debug

1. 单点调试的三个级别 Step into&#xff1a;在单步执行时&#xff0c;遇到子函数就进入并且继续单步执行。Step over&#xff1a;在单步执行时&#xff0c;在函数内遇到子函数时不会进入子函数内单步执行&#xff0c;而是将子函数整个执行完再停止&#xff0c;也就是把子函数…

商品期权交易怎么操作?

今天期权懂带你了解商品期权交易怎么操作&#xff1f;作为金融市场中的一种重要衍生品&#xff0c;商品期权在风险管理、套期保值和投机交易中发挥着重要的作用。 商品期权交易怎么操作&#xff1f; 选择期权合约&#xff1a;根据市场分析结果&#xff0c;选择合适的期权合约进…

什么是HTTPS证书?怎么免费申请?——值得收藏

SSL证书的核心功能在于保障互联网数据传输的安全性和网站身份的可靠性。它通过加密通信防止信息被窃取或篡改&#xff0c;同时验证网站的真实身份&#xff0c;有效抵御钓鱼攻击&#xff0c;增强用户信任。此外&#xff0c;使用SSL证书还有助于提升网站在搜索引擎中的排名&#…

上线了《學點笔录》,更方便翻阅笔录

大家好&#xff0c;我是学点&#xff0c;整理了一下自己笔记、摘要、记录《學點笔录》并且上线了为更方便翻阅 https://code.yellowcan.cn 欢迎来我的學點笔录网站&#xff01;笔录会关于与编程有关&#xff0c;比如bug记录、bug解决过程、编程笔记等等&#xff0c;帮助回忆阅…

通过七析BI自定义组件实现3D效果图表渲染

关于可视化的一些概念已经在之前的文章进行了大概的介绍&#xff0c;接下来我们会更加深入探讨关于呈现效果的内容。 为什么要用3D图表在仪表盘中进行呈现&#xff1f; 当讨论到这个问题的时候&#xff0c;自然就会回归到一个核心&#xff1a;3D与2D的呈现效果有什么区别&#…

Find My无线麦|苹果Find My技术与无线麦结合,智能防丢,全球定位

无线麦采用数字信号处理技术&#xff0c;能够实现高质量的录音效果。同时,其采用多种降噪技术,能够更好地降低底噪和环境噪声&#xff0c;提供更好的音质。无线麦在直播中的作用不仅仅是提供高质量的录音效果&#xff0c;它还能够增强直播的效果&#xff0c;提升听众的观感和体…

什么牌子的充电宝质量好且耐用?精选四款性价比高充电宝

对于我这种只要有空&#xff0c;手机基本是不离手的&#xff0c;从早到晚都是离不开手机&#xff0c;点外卖需要用到手机&#xff0c;看剧需要用到手机&#xff0c;反正各种活动都是离不开手机&#xff0c;但是手机总会有没电的时候&#xff0c;这时候要么是找个插座充电&#…

DEM(高程)数据下载及计算可见性

数据下载 下载链接: 地理空间数据云 (gscloud.cn) 数据部分介绍 ASTER是美国宇航局Terra航天器(1999年发射)上的五台仪器之一,在日本为经济产业省(METI)建造。美国/日本联合科学团队负责仪器设计、校准和数据验证。 高级星载热发射和反射辐射计(ASTER)全球数字高程…

社交媒体数据恢复:哔哩哔哩

哔哩哔哩数据恢复的方法 在哔哩哔哩平台上&#xff0c;数据恢复的过程可能会因为数据丢失的原因不同而有所差异。以下是根据搜索结果总结的几种常见数据恢复方法&#xff1a; 1. 误删历史记录的恢复 如果你不小心误删了哔哩哔哩的历史记录&#xff0c;那么很抱歉&#xff0c…

【Linux系统化学习】网络套接字(编写简单的UDP服务端和客户端)

目录 理解源IP地址和目的IP地址 认识端口号 端口号和进程ID的区别 源端口号和目的端口号 认识TCP和UDP协议 TCP协议 UDP协议 网络字节序 socket编程接口 socket常见API sockaddr结构 简单的UDP网络程序 UDP服务端 创建套接字 填充本地网络信息 绑定 收取消息 …

打造亚马逊爆款秘诀:流量、排名与自养号测评的完美结合

亚马逊是一个产品为王的平台&#xff0c;只要我们的产品好&#xff0c;就会有更多的流量&#xff0c;有流量还怕我们的产品卖不出去&#xff1f;身为新手我们店无流量该怎么办&#xff0c;今天教给你们五个获取流量的方法。 1.自然检索 那是我们常说的自然流量&#xff0c;通…

spring中的bean是线程安全的嘛

在Spring框架中&#xff0c;bean默认情况下不是线程安全的。Spring容器在初始化bean时&#xff0c;会为其创建一个单例实例&#xff0c;这个实例在整个应用中是唯一的&#xff0c;并且只会被初始化一次。由于这个特性&#xff0c;bean在默认情况下不是线程安全的。 然而&#…

在Ubuntu上安装docker

一、安装docker 更新系统包列表&#xff1a; sudo apt-get update安装必要的依赖软件包&#xff0c;使apt可以通过HTTPS使用repository。 sudo apt-get install apt-transport-https ca-certificates curl software-properties-common添加Docker的阿里云GPG密钥&#xff1a;…