谈谈线程安全问题及其解决方法

news2024/12/29 10:46:12

本文讲述一下线程的安全问题及解决方法。

一 线程安全问题举例说明
在电影院售票的场景,假使有100张票待售,有3个窗口在售票,如图所示:
在这里插入图片描述

三个窗口都卖出票1,一个票被卖了3次,多线程访问共享数据“票”,出现了线程安全问题。
问题代码见下:

public class SellTicketImpl1 implements Runnable {
    private int ticket=100;
    @Override
    public void run() {
        while (true){
            if(ticket>0){
                System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
}
public class ThreadSafeDemo1 {
    public static void main(String[] args) {
        SellTicketImpl1 sellTicket=new SellTicketImpl1();
        Thread t1=new Thread(sellTicket);
        Thread t2=new Thread(sellTicket);
        Thread t3=new Thread(sellTicket);
        t1.start();
        t2.start();
        t3.start();
    }

}

运行后出现下图所示:
在这里插入图片描述

二 解决线程安全问题三种方式

为了保证每个线程都能正常执行原子操作,java引入了线程同步机制。
有三种方式完成同步操作:

  1. 同步代码块
  2. 同步方法
  3. 锁机制

下面依次讲述三种同步操作:

1 同步代码块

1)说明:
解决线程安全问题的第一种方案是使用同步代码块,同步代码块的格式见下:

synchronized(锁对象){
       可能会出现线程安全问题的代码(访问了共享数据的代码)
   }

注意:

通过代码块中的锁对象,可以使用任意的对象,例如Object类对象;
但是必须保证多个线程使用的锁对象是同一个锁对象:把同步代码块锁住,只让一个线程在同步代码块中执行。
2)代码

public class SellTicketImpl2 implements Runnable {

    private int ticket=100;
    //创建一个Object类对象,当做锁对象
    Object obj=new Object();
    @Override
    public void run() {
        while (true){
            //使用synchronized同步代码块
            synchronized (obj) {
                if (ticket > 0) {
                    try {
                        //让它睡眠1秒
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
                }
            }
        }
    }
}
public class ThreadSafeDemo2 {
    public static void main(String[] args) {
        SellTicketImpl2 sellTicket=new SellTicketImpl2();
        Thread t1=new Thread(sellTicket);
        Thread t2=new Thread(sellTicket);
        Thread t3=new Thread(sellTicket);
        t1.start();
        t2.start();
        t3.start();
    }

}
  1. 同步方法

1)说明
解决线程安全问题的第二种方案是使用同步方法,同步方法的格式见下:

 修饰符 synchronized 返回值类型 方法名(参数列表){
     可能会出现线程安全问题的代码(访问了共享数据的代码)
 }

2)代码

public class SellTicketImpl3 implements Runnable  {

    //定义一个多个线程共享的票源
    private int ticket=100;
    //设置线程任务:卖票
    @Override
    public void run() {
        while (true) {
            sellTicket();
        }
    }

    //定义同步方法
    public synchronized void sellTicket(){
        //使用死循环,让卖票重复操作执行
        while(true){
            //先判断票是否存在
            if(ticket>0){
                try {
                    //提高安全问题出现的概率,让程序睡眠
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //如果票存在,卖票ticket
                System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
}
public class ThreadSafeDemo3 {
    public static void main(String[] args) {
        SellTicketImpl3 sellTicket=new SellTicketImpl3();
        Thread t1=new Thread(sellTicket);
        Thread t2=new Thread(sellTicket);
        Thread t3=new Thread(sellTicket);
        t1.start();
        t2.start();
        t3.start();
    }
}

3 Lock锁

1)说明:解决线程安全问题的第三种方案是使用Lock锁,即java.util.concurrent.locks.Lock接口。
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Lock 接口中的方法有:
void lock() 获取锁
void unlock() 释放锁

2)使用步骤:

(1)在成员位置创建一个ReentrantLock 对象;
(2)在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁;
(3)在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁;

3)代码

public class SellTicketImpl4 implements Runnable {

    //定义一个多个线程共享的票源
    private int ticket=100;
    //1.在成员位置创建一个ReentrantLock对象
    //使用多态
    Lock lk=new ReentrantLock();
    //设置线程任务:卖票
    @Override
    public void run() {
        while (true){
            //使用死循环,让卖票重复操作执行
            while(true){
                //2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
                lk.lock();
                //先判断票是否存在
                if(ticket>0){
                    try {
                        //提高安全问题出现的概率,让程序睡眠
                        Thread.sleep(10);
                        //如果票存在,卖票ticket
                        System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
                        ticket--;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        //3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
                        lk.unlock();   //无论是否出现异常,都释放锁
                    }

                }

            }
        }
    }
}
public class ThreadSafeDemo4 {
    public static void main(String[] args) {
        SellTicketImpl4 sellTicket=new SellTicketImpl4();
        Thread t1=new Thread(sellTicket);
        Thread t2=new Thread(sellTicket);
        Thread t3=new Thread(sellTicket);
        t1.start();
        t2.start();
        t3.start();
    }
}

运行后,如下图所示:
在这里插入图片描述

三 同步技术原理:

n个线程t1,t2,t3,…,tn,一起抢夺cpu的执行权,谁抢到了谁执行run方法进行卖票。假设t1抢到了cpu的执行权,执行run方法,遇到synchronized代码块,这时t1会检查synchronized代码块是否有锁对象(上述的obj),如果有,就会获取到锁对象,进入到同步执行;这时候t2也抢到了cpu执行权但是没有检查到锁对象,那么t2就会进入阻塞状态,会一直等待t1线程归还锁对象,一直到t1线程执行完同步中的代码,会把对象归还给synchronized代码块,t2才能获取到锁对象进入到同步中执行。

总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。同步保证了只能有一个线程在同步中执行共享数据。

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

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

相关文章

【代码阅读】MSC-VO

MSC-VO是ICRA2022的一篇点线视觉SLAM论文,本身是在ORBSLAM2的基础上改进的,改进的部分在于为SLAM系统引入了线段,并且使用了曼哈顿坐标系与结构化约束进行优化,之前看过的论文记录可以参考链接,年前把线段匹配和均匀化…

CMake的介绍

1.示例代码其实都非常简单,直接使用 GCC 编译器编译即可,连 Makefile 都不需要。在实际的项目中, 一个工程中可能包含几十、成百甚至上千个源文件, 这些源文件按照其类型、功能、模块分别放置在不同的目录中;面对这样的…

Kafka-生产者分区

一、分区的好处 便于合理使用存储资源,每个Partition在一个Broker上存储,可以把海量的数据按照分区切割成一块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡的效果。提高并行度,生产者可以以分区为单位发送数…

Git自学日记

添加暂存区 git add 提交本地库 git commit -m “日志信息” 修改文件 vim 修改文件名 按i进入编辑模式 按esc退出编辑摸模式 :wq 保存更改 历史版本 git reflog 查看版本信息 git log 查看版本详细信息 版本穿梭 git reset --hard 版本号 分支操作 创建分支: git br…

【数据结构】7.1 查找的基本概念

文章目录1. 查找表2. 关键字3. 查找4. 动态查找表和静态查找表5. 平均查找长度1. 查找表 问题:在哪里找? 答:在一个新的数据结构查找表上面找。 查找表: 查找表是由同一类型的数据元素(或记录)构成的集合…

操作系统真相还原_第5章第3节:加载内核(ELF格式分析)

文章目录用C语言写内核(例)二进制程序的运行方法ELF格式的二进制文件ELF文件格式数据类型ELF header的结构Elf32_Phdr的结构ELF文件实例分析将内核载入内存当前的OS信息当前内存规划源码boot.incmbr.sloader.s内核编译并写入硬盘用C语言写内核(例) 源码: int func…

图像处理 手写体英文字母的目标检测与识别 实验报告

获取本实验的项目代码和实验报告&#xff0c;请>点击此处< [0] 摘要 近年来&#xff0c;随着python的迅速崛起&#xff0c;人工智能、图像识别、计算机视觉等新兴学科变得火热起来。Python的发展也伴随着它的各种衍生库、衍生编辑器的发展&#xff0c;其中OpenCV是比较经…

恶意代码分析实战 7 WinDbg

配置WinDbg双机调试。 下载Windbg&#xff08;WDK&#xff09;。 事实上你自己的win10上应该会自带。 配置WinXP虚拟机的boot.ini 改成如图所示的样子 修改主机上Windb的属性。 修改成如图所示&#xff1a; 启动WinXP选择调试状态&#xff0c;启动Windbg即可开始调试。 7.…

【JavaWeb】JavaScript基础语法(下)

✨哈喽&#xff0c;进来的小伙伴们&#xff0c;你们好耶&#xff01;✨ &#x1f6f0;️&#x1f6f0;️系列专栏:【JavaWeb】 ✈️✈️本篇内容:JavaScript基础语法(上)&#xff01; &#x1f680;&#x1f680;代码托管平台github&#xff1a;JavaWeb代码存放仓库&#xff01…

程序员的自我修养第七章——动态链接

继续更新《程序员的自我修养》这个系列&#xff0c;主要是夏天没把它看完&#xff0c;补上遗憾。本篇来自书中第七章。 再说动态链接前&#xff0c;我们先阐明为什么要动态链接&#xff1a; 动态链接的产生来自静态链接的局限性。随着静态链接的发展&#xff0c;其限制也越来越…

zookeeper可视化工具

参考资料&#xff1a; 参考网址 使用过程&#xff1a; 首先打开网址&#xff0c;将资源克隆下来解压压缩包 打开压缩目录下 startup.bat ,填入对应地ip即可&#xff08;记得优先启动zookeeper&#xff0c;否则会报错&#xff09;

我的1周年创作纪念日

机缘 我目前还是一名六年级小学生&#xff0c;下半年便升入初中了。 我是在2021年上半年&#xff08;我四年级下期时&#xff09;开始学习C的。后来我自己想做一些小游戏&#xff0c;便经常要在百度上搜索&#xff0c;后来就发现CSDN中的内容比较全面&#xff0c;便加入了CSD…

MacOS下在Pycharm中配置Pyqt5工具(2023年新版教程)

前提&#xff1a;使用Anaconda的包管理工具进行管理。创建environment&#xff0c;然后在该Environment上进行下载操作&#xff01;&#xff01;&#xff01;一、安装相关模块安装pyqt5、pyqt5-tools两个基础包&#xff0c;命令如下&#xff1a;pip install -i https://pypi.tu…

CSS文本与字体(文本格式化/对齐/装饰/转换/间距/阴影/字体/样式/大小/简写属性)

目录 文本颜色 文本颜色和背景色 文本对齐 文本方向 垂直对齐 文字装饰 文本转换 文字缩进 字母间距 行高 字间距 空白 文本阴影 所有 CSS 文本属性 字体选择很重要 通用字体族 Serif 和 Sans-serif 字体之间的区别 一些字体的例子 CSS font-family 属性 字…

vue-countTo不兼容vue3解决方案

我们想要做一个数值增长的过度效果可以使用vue-count-to 官网的地址&#xff1a;https://www.npmjs.com/package/vue-count-to 官网的截图 vue2的使用方式 cnpm install -S vue-count-to在main.js import vueCountTo from "vue-count-to"; Vue.component("C…

【JavaEE】如何开始基础的Servlet编程(基于Tomcat服务器)

如何开始最简单的Servlet编程&#xff1f;&#xff08;基于Tomcat服务器&#xff09;知道了如何借助Tomcat开始进行最简单的Servlet编程后&#xff0c;我们就可以进一步完善功能制作一个基础的网站了。在此之前我们先了解一下Servlet的生命周期。Servlet的生命周期初始化init -…

C++ list

目录 一. 初步了解 1.构造、析构、赋值 2.容量 3.元素访问 4.增删 二. 模拟实现 框架 push_back 迭代器 带参构造、析构、赋值 增删 反向迭代器 所有代码 说白了&#xff0c;就是一个双向循环带头链表&#xff0c;由于我们在数据结构中已经学习过链表的知识&a…

macOS Big Sur 11.7.3 (20G1116) Boot ISO 原版可引导镜像

本站下载的 macOS Big Sur 软件包&#xff0c;既可以拖拽到 Applications&#xff08;应用程序&#xff09;下直接安装&#xff0c;也可以制作启动 U 盘安装&#xff0c;或者在虚拟机中启动安装。 请访问原文链接&#xff1a;https://sysin.org/blog/macOS-Big-Sur-boot-iso/&a…

Nginx学习整理|入门记录

目录 1. Nginx概述 1.1 Nginx介绍 1.2 Nginx下载和安装 1.3 Nginx目录结构 2. Nginx命令 3. Nginx配置文件结构 4. Nginx具体应用 4.1 部署静态资源 4.2 反向代理 4.3 负载均衡 1. Nginx概述 1.1 Nginx介绍 Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件…

积分电路的并联电阻、反向放大电路的并联电容的区别?

运放反相比例放大电路中反馈电阻两端经常并联一个电容&#xff0c;而运放积分电路的反馈电容上常常并联一个电阻&#xff0c;两者电路结构相似&#xff0c;如下所示&#xff08;隐去阻容值&#xff09;&#xff0c;二者有何区别呢&#xff1f;电阻、电容分别又起到什么作用&…