线程安全问题【snychornized 、死锁、线程通信】

目录

  • 一、线程安全
    • 1.1 线程安全问题?
    • 1.2 如何解决线程安全问题
        • 方法
        • 具体如何实现?
    • 1.3 同步方法
    • 1.4 同步代码块
    • 1.5 总结
    • 1.6 售票例子
    • 1.8 补充
  • 二、线程安全的集合
  • 三、死锁【了解】
  • 四、线程通信
    • 4.1 同步方法
    • 4.2 同步代码块
    • 4.3 wait和sleep
        • 本篇的思维导图
    • 最后

一、线程安全

1.1 线程安全问题?

  • 例如: 火车站售票例子,开启多线程同时售票,虽然设置了票数为static,但是还是会出现某一张票卖重复的情况

    • —> 这就是线程不安全 —> 当前线程的数据会被别的线程篡改
  • 为什么出现这种情况?

    • 某个线程在执行自己的任务时,还没执行完,就被别的线程执行了;
    • 如果多个线程执行的同一任务,操作同一数据,就会导致数据不一致
    • 原因就是,线程任务没执行完,就被别的线程抢走

1.2 如何解决线程安全问题

线程不安全原因是线程任务没执行完,就被别的线程抢走
所以,解决方案就是 当前线程执行时,不要被抢走

方法
  • 加锁!!! synchronized
具体如何实现?
  • 使用synchronized修饰方法 --> 同步方法
  • 使用synchronized修饰代码块 --> 同步代码块

1.3 同步方法

需求: 一个类中有两个方法,一个方法打印1,2,3,4 一个方法打印a,b,c,d, 另外再开两个线程,一个线程调用一个打印方法,保证每个打印方法执行时的完整


// 打印机类


public class Printer {

    /**
     * 方法1 打印1234
     * 保证线程安全方式1: 给方法加锁synchronized
     * 需要注意:
     *   1) 需要同步的方法都要加锁
     *   2) 锁的对象得是同一个
     */
    public synchronized void print1() {
        System.out.print(1 + " ");
        System.out.print(2 + " ");
        System.out.print(3 + " ");
        System.out.print(4 + " ");
        System.out.println( );
    }
    /**
     * 方法1 打印ABCD
     */
    public synchronized void print2() {
        System.out.print("A ");
        System.out.print("B ");
        System.out.print("C ");
        System.out.print("D ");
        System.out.println( );
    }
}

// 测试


public class TestPrinter {

    public static void main(String[] args) {
        Printer p = new Printer( );
        // 开启一个线程
        new Thread(){
            @Override
            public void run() {
                while(true){
                    p.print1();
                }
            }
        }.start();

        // 又开启一个线程
        new Thread(){
            @Override
            public void run() {
                while(true){
                    p.print2();
                }
            }
        }.start();
    }
}

注意1: 需要同步的方法都要加锁,

测试print1()加synchronized, print2()不加,运行看效果,发现锁不住!

注意2: 锁的对象得是同一个

测试,创建两个Printer类对象,分别调用print1() 和print2()

会发现不同步…

原因就是,两个对象是两个this, 同步方法锁得是同一个对象!!! 所以没有锁住!!

image-20240612100551052

可以给方法加static,静态的同步方法锁的是当前类的class文件,即虽然是两个Printer对象, 但是它俩都是Printer类的,即同一个class,照样可以锁住

image-20240612100820701

1.4 同步代码块

需求: 一个类中有两个方法,一个方法打印1,2,3,4 一个方法打印a,b,c,d, 另外再开两个线程,一个线程调用一个打印方法,保证每个打印方法执行时的完整


// 打印机类


public class Printer2 {

    private static Object lock = new Object();

    /**
     * 同步代码块,
     * 1) 需要主动设置锁对象
     * 2) 锁对象可以是任意对象
     * 3) 同步的方法锁的得是同一个对象
     */
    public void print1() {
        synchronized (lock) {
            System.out.print(1 + " ");
            System.out.print(2 + " ");
            System.out.print(3 + " ");
            System.out.print(4 + " ");
            System.out.println( );
        }
    }
    public void print2() {
        synchronized (lock){
            System.out.print("A ");
            System.out.print("B ");
            System.out.print("C ");
            System.out.print("D ");
            System.out.println( );
        }
    }
}

// 测试


public class TestPrinter2 {

    public static void main(String[] args) {
        Printer2 p = new Printer2( );
        Printer2 p2 = new Printer2( );
        // 开启一个线程
        new Thread(){
            @Override
            public void run() {
                while(true){
                    p.print1();
                }
            }
        }.start();


        // 又开启一个线程
        new Thread(){
            @Override
            public void run() {
                while(true){
                    p2.print2();
                }
            }
        }.start();
    }
}

1.5 总结

  • 区别:
    • 同步方法: 对整个方法加锁,锁的范围较大
      • 同步方法默认锁的是this
      • 静态同步方法锁的是当前对象的字节码文件(class)
    • 同步代码块: 对方法内部,部分代码加锁,范围较小
  • 相同:
    • 需要同步的方法/代码都需要加锁
    • 锁的都得是同一个对象,即得是同一把锁

1.6 售票例子

// 售票类


public class TicketWindow extends Thread {

    // private static Object lock = new Object();
    private static int ticketNum = 100;

    public TicketWindow(String name) {
        super(name);
    }

    /**
     * 售票功能,需要并行执行(多线程)
     */
    @Override
    public void run() {
        while (true) {
            synchronized (TicketWindow.class) {
                if (ticketNum > 0) {
                    System.out.println(this.getName( ) + "正在售出第" + ticketNum + "票");
                    // 模拟出票时间,稍微等待n毫秒
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace( );
                    }
                    ticketNum--;
                } else {
                    System.out.println("票已售罄~");
                    break;
                }
            }
        }
    }
}

//测试

public class TestTicker {
    public static void main(String[] args) {
        new TicketWindow("窗口1").start();
        new TicketWindow("窗口2").start();
        new TicketWindow("窗口3").start();
        new TicketWindow("窗口4").start();
    }
}

1.8 补充

保证线程安全是使用synchronized,但是其实还有很多技术可以保证线程安全

  • volatile 关键词
  • ThreadLocal
  • Lock
  • ReentrantLock
  • ReentrantReadWriteLock
  • 悲观锁,乐观锁,公平锁,自旋锁…等等

二、线程安全的集合

学习常用类时,经常见到保证同步或者不同步的类

同步即安全,不同步即不安全

StringBuilder(不同步)和StringBuffer(同步)

image-20240612112614042

ArrayList(不安全) 和 Vector(安全)

image-20240612112809392

HashMap(不安全)和Hashtable(安全)

image-20240612113014369

ConcurrentHashMap(线程安全),比Hashtable效率高

  • 使用了同步代码块,锁了部分代码
  • 采用的是分段锁机制,如果多个线程操作的是Hash表中不同的数据,不锁
    • 如果操作的是同一个hash值下的数据,再锁

image-20240612113421487

三、死锁【了解】

死锁是指在多任务系统中,各个任务由于竞争资源而造成的一种互相等待的现象,导致任务无法继续执行的情况。死锁通常发生在多个任务同时需要多个共享资源,但是这些资源又只能被一个任务独占,但是另外一个任务不释放时!

死锁通常发生在四个必要条件同时满足时:

  1. 互斥条件:资源只能被一个任务占用,其他任务需要等待释放。
  2. 占有且等待:任务至少持有一个资源,并且在等待获取其他资源。
  3. 不可抢占:已经分配给一个任务的资源不能被其他任务抢占,只能由持有资源的任务主动释放。
  4. 循环等待:一系列任务互相持有其他任务所需要的资源,形成一个循环等待的关系。

// 两把锁

public class MyLock {
    public static final Object LEFT_LOCK = new Object();
    public static final Object RIGHT_LOCK = new Object();
}

// 演示死锁

public class TestDeadLock {
    public static void main(String[] args) {
        new Thread("男朋友") {
            @Override
            public void run() {
                synchronized (MyLock.LEFT_LOCK) {
                    System.out.println(this.getName( ) + "拿到左筷子");
                    synchronized (MyLock.RIGHT_LOCK) {
                        System.out.println(this.getName( ) + "拿到右筷子");
                        System.out.println(this.getName( ) + "吃饭");
                    }
                }
            }
        }.start( );

        new Thread("女朋友") {
            @Override
            public void run() {
                synchronized (MyLock.RIGHT_LOCK) {
                    System.out.println(this.getName( ) + "拿到右筷子");
                    synchronized (MyLock.LEFT_LOCK) {
                        System.out.println(this.getName( ) + "拿到左筷子");
                        System.out.println(this.getName( ) + "吃饭");
                    }
                }
            }
        }.start( );
    }
}

为避免和解决死锁,可以采取以下策略:

  1. 避免死锁:通过破坏死锁的四个必要条件之一,来避免死锁的发生。比如,一次性获取所有需要的资源,或者按照一定的顺序获取资源。
  2. 检测和恢复:定期检测系统中是否存在死锁,一旦检测到死锁,采取恢复策略,比如中断一些任务,释放资源。
  3. 避免循环等待:为资源分配一个全局唯一的编号,任务按编号递增的顺序申请资源,释放资源则按相反的顺序进行,避免循环等待。
  4. 资源剥夺:当一个任务请求资源时,如果无法获取,可以暂时剥夺该任务已经持有的资源,让其他任务能够继续执行。
  5. 谨慎设计:在程序设计时,尽量避免使用多个资源互斥且不可抢占的情况,或者采取一些策略确保资源的合理分配和释放,减少死锁的发生可能性。

四、线程通信

线程通信是指线程间可以交互,指定信号,让线程执行或者等待

通过Object类中的方法完成通信

  • wait()
  • notify()

4.1 同步方法

需求: 两个输出的方法,保证正常输出不被打断且达到一人一次输出的效果


public class Printer {

    // 定义一个信号量
    // 1代表print1执行 2代表print2执行
    private int flag = 1;

    /**
     * 线程通信的要求
     * 1) 要保证线程安全
     * 2) 线程等待方法是wait
     *    线程唤醒方法是notify
     * 3) 必须使用锁对象调用 通信的方法
     * ---------
     * 为什么,wait和notify这些线程通信的方法要设计在Object类?
     * 答:
     */
    public synchronized void print1() throws InterruptedException {
        if (flag != 1){ // 信号不是1,说明不该print1执行,那就等待
            this.wait();
        }
        System.out.print(1 + " ");
        System.out.print(2 + " ");
        System.out.print(3 + " ");
        System.out.print(4 + " ");
        System.out.println( );

        // 改变信号量
        flag = 2;
        // 通知处于等待状态的线程启动
        this.notify();
    }
    public synchronized void print2() throws InterruptedException {
        if (flag != 2) { // 信号不是2,说明不该print2执行,那就等待
            this.wait();
        }
        System.out.print("A ");
        System.out.print("B ");
        System.out.print("C ");
        System.out.print("D ");
        System.out.println( );

        flag = 1;
        // 通知处于等待状态的线程启动
        this.notify();
    }
}
package com.qf.notify;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @date 2024/6/12
 * @desc
 */
public class TestPrinter {

    public static void main(String[] args) {
        Printer p = new Printer( );
        // 开启一个线程
        new Thread(){
            @Override
            public void run() {
                while(true){
                    try {
                        p.print1();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }.start();

        // 又开启一个线程
        new Thread(){
            @Override
            public void run() {
                while(true){
                    try {
                        p.print2();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }.start();
    }

}

4.2 同步代码块

再使用同步代码块演示一遍,再次确定一个结论

  • 锁对象是谁,就使用哪个对象来调用wait和notify
package com.qf.notify;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @date 2024/6/12
 * @desc 打印机类 --> 演示线程通信--> 使用同步代码块
 */
public class Printer2 {

    // 定义一个信号量
    // 1代表print1执行 2代表print2执行
    private int flag = 1;

    /**
     * 线程通信的要求
     * 1) 要保证线程安全
     * 2) 线程等待方法是wait
     *    线程唤醒方法是notify
     * 3) 必须使用锁对象调用 通信的方法
     * ---------
     * 为什么,wait和notify这些线程通信的方法要设计在Object类?
     * 答:
     */
    public void print1() throws InterruptedException {
        synchronized (Object.class) {
            if (flag != 1) { // 信号不是1,说明不该print1执行,那就等待
                Object.class.wait( );
            }
            System.out.print(1 + " ");
            System.out.print(2 + " ");
            System.out.print(3 + " ");
            System.out.print(4 + " ");
            System.out.println( );

            // 改变信号量
            flag = 2;
            // 通知处于等待状态的线程启动
            Object.class.notify( );
        }
    }
    public void print2() throws InterruptedException {
        synchronized (Object.class) {
            if (flag != 2) { // 信号不是2,说明不该print2执行,那就等待
                Object.class.wait( );
            }
            System.out.print("A ");
            System.out.print("B ");
            System.out.print("C ");
            System.out.print("D ");
            System.out.println( );

            flag = 1;
            // 通知处于等待状态的线程启动
            Object.class.notify( );
        }
    }
}

补充: 目前这个代码可以保证两个线程通信,如果>= 3个线程,就不一定能按照预想顺序完成

原因是,线程过多,但是notify方法只能随机唤醒一个处于等待状态的线程

解决方案: 使用notifyAll

4.3 wait和sleep

  • wait
    • 是Object类中的方法
    • wait会让线程等待
    • wait方法必须在同步方法中使用
    • wait方法方法线程等待时,会让出资源,别的线程可以执行
  • sleep
    • 是Thread类中的方法
    • sleep会让线程等待
    • 方法同步或者不同步都可以使用
      • 如果线程不安全,使用了sleep,会让出资源,别的线程执行
      • 如果线程安全,使用了sleep,不会释放资源,别的线程不会执行,会阻塞 --> 抱着锁睡

本篇的思维导图

在这里插入图片描述


最后

如果感觉有收获的话,点个赞 👍🏻 吧。
❤️❤️❤️本人菜鸟修行期,如有错误,欢迎各位大佬评论批评指正!😄😄😄

💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍在这里插入图片描述

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

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

相关文章

批量替换删除图片文件名称中相同数字:轻松管理文件结构新技巧大揭秘

特别是当图片文件名称中包含相同的数字时,想要快速找到或整理这些文件更是难上加难。今天,我要向大家揭秘一种轻松管理图片文件结构的新软件——文件批量改名高手。 进入“文件批量改命名高手”主页面,你会看到一个简洁明了的操作界面。在板…

【Linux】模拟实现一个简单的日志系统

👦个人主页:Weraphael ✍🏻作者简介:目前正在学习c和算法 ✈️专栏:Linux 🐋 希望大家多多支持,咱一起进步!😁 如果文章有啥瑕疵,希望大佬指点一二 如果文章对…

C++ 11 之 参数传递

c11参数传递.cpp #include <iostream> using namespace std;void swap1(int a, int b) {int temp a;a b;b temp;cout << "函数的a: " << a << endl;cout << "函数的b: " << b << endl; }void swap2(int *a,…

Boost-PFC电路讲解

电路拓扑图 PFC_Boost与普通Boost的区别 普通Boost&#xff1a;通常是用一次电源的输出的直流来作为它的输入&#xff0c;输入电压比较稳定&#xff0c;无需考虑 PF值THD方面的东西 PFC_Boost&#xff1a;直接是市电输入&#xff0c;输入电压是电压大小和方向时刻在变化的正弦…

AI日报|苹果生态全面整合AI功能,字节跳动被曝秘密启动AI手机研发

文章推荐 粽叶飘香&#xff0c;端午安康&#xff01;AI视频送祝福啦~ 谁是最会写作文的AI“考生”&#xff1f;“阅卷老师”ChatGPT直呼惊艳&#xff01; ⭐️搜索“可信AI进展“关注公众号&#xff0c;获取当日最新AI资讯 苹果WWDC 2024&#xff1a;AI为苹果带来了什么&am…

Linux基础之进程替换

目录 一、进程替换的基本概念 二、exec系列函数 2.1 execl系列函数 2.2 execv系列函数 2.3 替换原理 一、进程替换的基本概念 根据我们之前所学&#xff0c;我们可以知道我们所创建的所有的子进程&#xff0c;执行的代码&#xff0c;都是父进程代码的一部分。如果我们想让…

IP隔离是什么,你了解多少?

一、IP地址隔离的概念和原理 当我们谈论 IP 地址隔离时&#xff0c;我们实际上是在讨论一种网络安全策略&#xff0c;旨在通过技术手段将网络划分为不同的区域或子网&#xff0c;每个区域或子网都有自己独特的 IP 地址范围。这种划分使网络管理员可以更精细地控制哪些设备或用…

基于java的英文翻译字典

基于java的英文翻译字典&#xff0c;附有源代码&#xff0c;源数据库初始化文件 源码地址 dict_demo: 提取一段英文对话中的英文词汇&#xff0c;输出为英文单词字典形式 解析json字条 private void readFile(String pathname) {long start System.currentTimeMillis(); //…

学习笔记——路由网络基础——路由优先级(preference)

1、路由优先级(preference) 路由优先级(preference)代表路由的优先程度。当路由器从多种不同的途径获知到达同一个目的网段的路由(这些路由的目的网络地址及网络掩码均相同)时&#xff0c;路由器会比较这些路由的优先级&#xff0c;优选优先级值最小的路由。 路由来源的优先…

Goby 漏洞发布|XAMPP Windows PHP-CGI 代码执行漏洞

漏洞名称&#xff1a;XAMPP Windows PHP-CGI 代码执行漏洞 English Name&#xff1a;XAMPP PHP-CGI Windows Code Execution Vulnerability CVSS core: 9.8 漏洞描述&#xff1a; PHP是一种在服务器端执行的脚本语言,在 PHP 的 8.3.8 版本之前存在命令执行漏洞,由于 Window…

SpringMVC框架学习笔记(七):处理 json 和 HttpMessageConverter 以及文件的下载和上传

1 处理 JSON-ResponseBody 说明: 项目开发中&#xff0c;我们往往需要服务器返回的数据格式是按照 json 来返回的 下面通过一个案例来演示SpringMVC 是如何处理的 &#xff08;1&#xff09; 在web/WEB-INF/lib 目录下引入处理 json 需要的 jar 包&#xff0c;注意 spring5.x…

MYSQL数据库下载和安装(详细)

1.点击MySQL官网(后续照着图走) 2.软件下载完点击进入安装 设置要安装的路径然后点击OK,后面点击下一步 再点击下一步 MySQL推荐使用最新的数据库和相关客户端&#xff0c;mysql8换了加密插件&#xff0c;所以如果选第一种方式&#xff0c;很可能导致你的navicat等客户端连不上…

基于C#开发web网页管理系统模板流程-主界面密码维护功能完善

点击返回目录-> 基于C#开发web网页管理系统模板流程-总集篇-CSDN博客 前言 紧接上篇->基于C#开发web网页管理系统模板流程-主界面统计功能完善-CSDN博客 一个合格的管理系统&#xff0c;至少一定存在一个功能——用户能够自己修改密码&#xff0c;理论上来说密码只能有用…

使用Hadoop MapReduce分析邮件日志提取 id、状态 和 目标邮箱

使用Hadoop MapReduce分析邮件日志提取 id、状态 和 目标邮箱 在大数据处理和分析的场景中&#xff0c;Hadoop MapReduce是一种常见且高效的工具。本文将展示如何使用Hadoop MapReduce来分析邮件日志&#xff0c;提取邮件的发送状态&#xff08;成功、失败或退回&#xff09;和…

【云原生】使用kubekey部署k8s多节点及kubesphere

kubesphere官方部署文档 https://github.com/kubesphere/kubesphere/blob/master/README_zh.md kubuctl命令文档 https://kubernetes.io/zh-cn/docs/reference/kubectl/ k8s资源类型 https://kubernetes.io/zh-cn/docs/reference/kubectl/#%E8%B5%84%E6%BA%90%E7%B1%BB%E5%9E…

Nginx配置详细解释:(6)实现反向代理服务器,动静分离,负载均衡

作为代理服务器是当客户端访问代理服务器时&#xff0c;代理服务器代理客户端去访问真实web服务器。proxy_pass; 用来设置将客户端请求转发给的后端服务器的主机。 需要模块ngx_http_upstream_module支持。 单台反向代理 在第三台主机上下载安装httpd&#xff0c;在主页面/v…

VMware Ubuntu虚拟机上设置SSH连接,win直接用ssh连接虚拟机

要在Ubuntu虚拟机上设置SSH连接&#xff0c;并进行一些特定配置&#xff0c;您可以按照以下步骤进行操作&#xff1a; 步骤 1&#xff1a;安装OpenSSH Server 打开终端。 更新包列表并安装OpenSSH Server&#xff1a; sudo apt update sudo apt install openssh-server安装完…

讯飞星火高考中英文作文双料第一,力压GPT-4o

随着2024届高考落幕&#xff0c;近日&#xff0c;各家大模型的高考评测结果陆续出炉&#xff0c;多家媒体和专业人士用高考中英文作文题和数学题对大模型能力进行测评&#xff0c;讯飞星火不仅占据榜首&#xff0c;并超过GPT-4o&#xff0c;在本届的“大模型高考”中&#xff0…

Django之云存储(一)

一、介绍 用户上传的文件以及项目中使用的静态文件,除了保存在本地服务器,还在可以保存在云服务中,比如: 阿里云七牛云(课程选用)亚马逊云等1.1、使用方式 注册账号 七牛云开发者平台 实名认证 创建空间

Unity 集成 FMOD 音频管理插件 2.02

Unity 集成 FMOD 音频管理插件 2.02 3. 集成教程&#xff1a;3.1 设置Unity项目3.2 设置FMOD项目3.3 设置 FMOD for Unity3.4 添加声音&#xff1a;卡丁车引擎3.5 添加声音&#xff1a;氛围3.6 添加声音&#xff1a;音乐3.7 删除现有音频3.8 下一步 10. 脚本 API 参考10.1 基础…