1-Single Thread

news2024/11/25 22:41:12

单线程执行模式

案例-1

背景

模拟3个人频繁地经过同一个只能容许一个人经过的门 。

(模拟三个线程调用同一个对象的方法)

当人通过门的时候,这个程序会在计数器中,递增通过的人数。另外, 还会记录通过的人的 “ 姓名与出生地”。

(当某一个线程调用该对象方法的时候,都会进行计数(使该对象的counter属性+1),另外还会记录当前对象的name和address属性,并检查name和address属性关系是否正确对应)

Main.class

​ 创建一 个门,并操作了个人不断地穿越门的 类

Gate.class

​ 表 示 门 的 类 , 当 人经 过 时 会 记 录姓 名 与 出 生 地 表示人的类,

UserThread.class

​ 只负责处理不断地在门间穿梭通过

Gate.class
class Gate{
    //表示已经通过这道门的人数
    private int counter = 0;
    //表示通过这道门的人名
    private String name = "Nobody";
    //表示通过这道门的人的出生地
    private String address = "Nowhere";
    public void pass (String name, String address) {
        this.counter++;
        this.name = name;
        this.address = address;
        check();
    }

    public String toString() {
        return "No." + counter + ": " + name + ", " + address;
    }

    /**
     * 这里为了判断人名和地址是否匹配直接看 人名 和 出生地 的首字母是否一致进行判断了
     */
    private void check(){
        if (name.charAt(0) != name.charAt(0)){
            System.out.println("********* broken *********** " + toString());
        }
    }
}
UserThread.class
class UserThread extends Thread{
        private final Gate gate;
        private final String myname;
        private final String myaddress;
        public UserThread (Gate gate, String myname, String myaddress){
            this.gate = gate;
            this.myname = myname;
            this.myaddress = myaddress;
        }

        public void run() {
            System.out.println (myname + " start");
            while (true) {
                gate.pass(myname, myaddress);
            }
        }
}
Main.class
public class Demo {

    public static void main(String[] args) {
        System.out.println("Testing Gate, hit CIRLtC to exit");
        Gate gate = new Gate();
        new UserThread(gate,"Axxxx","Alaska").start();
        new UserThread(gate,"Bxxxx","Brazil").start();
        new UserThread(gate,"Cxxxx","Canada").start();
    }

}
执行结果

请添加图片描述

分析

我们看一下这个执行的结果,每个线程开始执行的时候都会先打印一下穿过这个门的人姓名,同时我们预想中的 broken 出现了,但是好像又和预想的有点不一样。我们预想的结果是出现broken时,后面的人名和出生地不对应的,但是咱们这里出现broken时,人名和出生地大部分都是对应的,从结果来看只有一条记录是不对应的。这是为什么呢?

同时我们也能看到第一次出现broken的时候已经是第128次了,侧面的说明了有些时候仅仅靠测试是无法验证程序的安全性。

从第一条broken的打印结果来看,明明已经是姓名和出生地不匹配了,但是打印的结果是匹配的,这也从侧面说明了,在多线程的程序调试过程中,调试的结果是不可信的,值得注意。

原因分析:

在这里是以语句当作线程的基本操作单位,事实上线程可能是以更小的单位在切换的。

我们的pass方法;

public void pass (String name, String address) {
        this.counter++;
        this.name = name;
        this.address = address;
        check();
    }

pass( ) 方法里面有四条语句,每个多线程在执行的时候,可能会是交错依次执行这四条语句的。通常线程是不会去考虑其他线程的,它只会自己一直不停的跑下去。所以在这里,可能线程A刚给name赋值完,此时线程A会继续给address赋值,但是在线程A给address赋值的同时,线程B也在给name重新赋值,然后线程A去调用check( )方法时就会出现broken,当出现broken的时候线程A会继续调用toString( )方法,此时线程B又给address赋值完了,然后toString()方法打印的就会是线程B操作的name和address,线程A在进行check( )方法检查的时候,使用的是线程B的name和线程A的address进行判断的。所以就会出现我们最终在控制台打印的结果。

修改成线程安全版本

这里我们只需要加固这个门就可以实现安全问题,我们将门的pass( )和check( )方法进行线程安全处理就可以了 。


class Gate{
    //表示已经通过这道门的人数
    private int counter = 0;
    //表示通过这道门的人名
    private String name = "Nobody";
    //表示通过这道门的人的出生地
    private String address = "Nowhere";
    public synchronized void pass (String name, String address) {
        this.counter++;
        this.name = name;
        this.address = address;
        check();
    }

    public String toString() {
        return "No." + counter + ": " + name + ", " + address;
    }

    /**
     * 这里为了判断人名和地址是否匹配直接看 人名 和 出生地 的首字母是否一致进行判断了
     */
    private synchronized void check(){
        if (name.charAt(0) != name.charAt(0)){
            System.out.println("********* broken *********** " + toString());
        }
    }
}

.

在本案例中,Gate类就担任了共享资源参与者的角色。

对于pass( )和check( )这两个不安全的方法我们使用synchronized修饰符进行修饰后,就可以保证它的线程安全问题。

synchronized

synchronized修饰符并不会对程序的安全造成危害,但是调用synchronized修饰的方法时会比调用一般方法花费比较多的一些时间,所以会使程序的性能降低。

在单线程程序中使用synchronized 方法,就好像在外独居的人,即使一个人在 家中还是将厕所的门锁住的意思一样。既然是 一个人住,即使不锁门,也不需要担 心有人会突然打开厕所的门 了。

就算是多线程程序,如果所有线程完全地独立运行,那也没有使用 单线程执行模式 的必要。我们將这个状态称为线程互不干涉(interfere)。 有些管理多线程的环境,会帮我们确保线程的独立性,这种情况下这个环境的 用户就不必考虑需不需要使用单线程执行模式。

生命性与死锁

使用 单线程执行模式 时,可能会有发生死锁 (deadlock )的危险。 所谓的死锁,是指两个线程分别获取了锁定,互相等待另一个线程解除锁定的 现象。发生死锁时,哪个线程都无法继续执行下去,所以程序会失去生命性。

死锁小案例背景

假设 A 与 B 同吃一个大盘子所盛放的意大利面。盘子的旁边只有一支汤匙与一支叉子,而要吃意大利面时,同时需要用到汤匙与叉子。

只有 一支的汤匙,被 A 拿去了,两只有一支的叉子,却被 B 拿走了。 就造成以 下的情况:

  • 握着汤匙的 A ,一直等着B 把叉子放下
  • 握着叉子的 B , 一直等着A 把汤匙放下

这么一来 A 与 B 只有面面相觑,就这样不动了。像这样,多个线程僵持不下,使程序无法继续运行的状态,就称为死锁。

死锁出现条件

单线程执行达到下面这些条件时,可能会出现死锁的现象:

  • 具有多个SharedResource(共享资源) 参与者
  • 线程锁定一个SharedResource 时,还没解除前就去锁定另 一个SharedResource。
  • 获取SharedResource 参与者的顺序不固定 (和SharedResource 参与者是 对 等 的 )。

对比一下上面吃意大利面的例子:

  • 多个SharedResource 参与者,相当于汤匙与叉子。

  • 锁定某个SharedResource 参与者后,就去锁定其他SharedResource。就相当于握着汤匙而想 要获取对方的叉子,或握着叉子而想要获取对方的汤匙这些操作。

  • SharedResource角色是对等的,就像“拿汤匙一拿叉子”与“拿叉子一拿汤匙 〞两个操作都可能发生。也就是说在里汤匙与叉子并没有优先级 。

上面的三个条件只要破坏一种条件,就可以避免死锁的发生。

synchronized 在保护什么

请读者阅读程序代码的时候,看到synchronized 时,就要思考:

“这个synchronized 是在保护什么?”
无论是synchronized 方法或是synchronized块,syncbronized势必保护着“某个 东西”。

确认 “ 保护什么” 之后,接 下来应该要思考的是:

“ 其他的地方也有妥善保护到吗?”

若这些字段还在其他许多地方使用着,这里使用synchronized 小心保护,但其他地方并没有做好保护措施, 那其实这个字段还是没被保护的。就像就算小心翼翼地把大门跟后门都锁得好好的, 如果窗户敞开, 还是没有意义一样

“ 获取谁的锁定来保护的呢?,

要调用synchronized 实例方法 (instance method)的线程, 一定会获取this 的锁 定。一个实例的锁定,同一时问内只能有 一线程可以得到。因为这个惟一性,我们 才能使用synchronized 来做到单线程执行模式 如果实例不同,那锁定也不同了。虽然我们说“使用synchronized 来保护”,但 如果有多个相异实例,那多个线程仍然可以分别执行不同实例的 synchronized 方法 。

案例-2

##### 背景:

案例-1的死锁小案例简化版,汤匙和叉子,我们这里简称左手工具和右手工具,

A和B吃饭需要左手和右手配合,两只手都需要拿到对应的工具才可以吃饭。

代码:
public class Demo {

    public static void main(String[] args) {
        System.out.println("Testing Gate, hit CIRLtC to exit");
        Tools left = new Tools("left");
        Tools right = new Tools("right");
        new EatThread("A",left,right).start();
        new EatThread("B",right,left).start();
    }

}

class EatThread extends Thread{

    private String name ;
    private final Tools leftTool;

    private final Tools rightTool;

    public EatThread(String name, Tools leftTool, Tools rightTool) {
        this.name = name;
        this.leftTool = leftTool;
        this.rightTool = rightTool;
    }

    @Override
    public void run() {
        while (true){
            eat();
        }
    }

    public void eat(){
        synchronized (leftTool){//左手工具拿起
            System.out.println(name + " " + "拿到了 " + leftTool.toString());
            synchronized (rightTool){//右手工具拿起
                System.out.println(name + " " + "拿到了 " + rightTool.toString());
                System.out.println(name + "开始吃东西了========> eat");
                System.out.println(name + "放下了右手" + leftTool.toString());
            }//右手工具放下
            System.out.println(name + "放下了左手" + rightTool.toString());
        }//左手工具放下
    }

}


class Tools{
    private final String name ;

    public Tools(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name ;
    }
}
分析

上面的代码运行后会发生死锁,是因为main方法中A和B线程传参数是对称传入,这就造成了A和B拿起左右手工具时顺序不一致,就会造成A和B分别拿到一个工具,并且另一个工具都在对方手中,都在等对方放下手中的工具。

纠正
方案1

修改main方法中的A和B的传参数顺序一致,这样A和B都同时按照同一种顺序拿取工具,先拿左手工具再去拿右手工具,当左手工具没有拿到的时候就不会再去拿右手工具。

public static void main(String[] args) {
    System.out.println("Testing Gate, hit CIRLtC to exit");
    Tools left = new Tools("left");
    Tools right = new Tools("right");
    new EatThread("A",left,right).start();
    new EatThread("B",left,right).start();
}
方案2

将左右手工具放在一起,不分开,这样工具只有一个,谁拿到就是谁的,拿到了就可以直接开吃,不用再去拿第二个工具。

public static void main(String[] args) {
        System.out.println("Testing Gate, hit CIRLtC to exit");
        Tools left = new Tools("left");
        Tools right = new Tools("right");
        Pair pair = new Pair(left,right);
        new EatThread("A",pair).start();
        new EatThread("B",pair).start();
        
    }

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

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

相关文章

一篇文章带你了解Redis持久化机制(RDB、AOF)

目录 一、简介 什么是持久化? 为什么要持久化? 两种实现方式 二、RDB详解 2.1、介绍 2.2、save指令前后对比 2.3、save指令相关配置 一些设置 RDB快照条件 2.4、RDB第一种方式:手动save 2.5、RDB第二种方式:后台执行&…

加壳与脱壳,打造铁壁铜墙的Android应用防护境地

加壳和脱壳是什么? Android逆向加壳和脱壳是与Android应用程序安全相关的概念。 逆向加壳(Reverse Engineering with Packing):逆向加壳是指在给定的Android应用程序中,通过添加一个或多个防护层或加密算法来增加应用…

AI2:仅凭开源数据,可达ChatGPT 83%表现

夕小瑶科技说 原创 作者 | Python ChatGPT强大的性能让人爱不释手,ChatGPT迟迟不开源让人恨得牙根痒痒。那仅通过开源数据,能够取得怎样的效果呢?近期,AI2的一篇论文显示,最好的65B规模的模型能够达到ChatGPT表现的8…

设计一个feed流系统

什么是feed流系统 移动互联网时代,Feed流产品是非常常见的,如朋友圈、微博、抖音等,除此之外,很多App的都会有一个模块,要么叫动态,要么叫消息广场,这些也是Feed流产品。只要大拇指不停地往下划…

【机器学习】十大算法之一 “决策树”

作者主页:爱笑的男孩。的博客_CSDN博客-深度学习,活动,python领域博主爱笑的男孩。擅长深度学习,活动,python,等方面的知识,爱笑的男孩。关注算法,python,计算机视觉,图像处理,深度学习,pytorch,神经网络,opencv领域.https://blog.csdn.net/Code_and516?typeblog个…

截至目前最强的70亿参数大语言模型:开源可商用的RedPajam 7B完全版发布!

RedPajama模型是TOGETHER发布的一个开源可商用的大模型。2023年6月6日,TOGETHER在官方宣布该模型完成训练,经过测试,该模型目前超过所有7B规模的大模型,比LLaMA-7B和Falcon-7B的效果还要好! TOGETHER公司是一家由豪华管…

HDMI之HDCP

概述 HDCP 1.4第1阶段 HDCP 1.4第2阶段 只有REPEATER设备,此阶段才会出现。 HDCP 1.4第3阶段 本文以Repeater为例,连接方式Source[Tx]=>[Rx]Repeater[Tx]=>[Rx]Sink。讲解一下HDCP1.4的通信过程。 设置SCDC 设置TMDS Configuration Rx W A8 20 00 Rx W A8 20 R …

云安全与云渗透

一、引言 随着技术的进步,云计算已成为信息技术领域的主流趋势。企业和个人都在利用云服务实现数据存储和处理的便利,但同时也带来了一系列的安全问题。对于这些问题,我们需要深入理解云安全和云渗透的重要性。本文将详细探讨这两个主题。 …

Leetcode之哈希查找

1. 哈希查找 本质上就是个搜索,但是可以将在一个集合中查找一个元素的时间复杂度降低到O(1)。python中常用的有以下方式: setdict数组模拟 2. 相关算法题 2.1. Leetcode 771 宝石与石头 题目链接题目描述 给你一个字符串 jewels 代表石头中宝石的类…

Java 图片渲染到前端,向前端一次返回多张Base64图片

文章目录 前言图片渲染到前端向前端一次返回多张Base64图片 前言 当我们从服务器读取的图片链接返回给前端,前端可以很轻松的下载和展示,但是对于临时文件,我们不需要保存到服务器,比如PPT转图片,PDF转图片等等&#…

数据结构--》从线性表说起,掌握常用基础算法

目录 初识线性表 线性表的基本操作 顺序表的定义 顺序表的基本操作 单链表的定义 单链表的基本操作 双链表的介绍 循环链表的介绍 静态链表的介绍 初识线性表 线性表是具有相同数据类型的 n (n0) 个数据元素的有限序列,其中n为表长,当n0时线性…

mysql 将date字段默认值设置为CURRENT_DATE

我们是否可以在mysql中,将Date字段的默认值设置为CURRENT_DATE(当前日期)? 答案是8.0之前不可以,8.0.13之后可以。 比如在5.7版本中使用如下sql创建表,将会提示语法错误: CREATE TABLE t_order (id bigi…

CentOS 7远程登录jupyter lab

使用cat /etc/redhat-release看到操作系统是CentOS Linux 7.6,使用uname -r看到内核是3.10.0-957.el7.x86_64。 python3 --version看一下python的版本,pip3 --version看一下pip的版本,这是我CentOS 7默认安装好的。 pip3 install jupyterla…

ASEMI代理光宝高速光耦LTV-M601参数,LTV-M601图片

编辑-Z LTV-M601参数描述: 型号:LTV-M601 平均正向输入电流IF:20mA 反向输入电压VR:5V 功耗PI:40mW 输出集电极电流IO:50mA 输出集电极电压VO:7V 输出集电极功耗Po:85mW 电…

【C++从入门到放弃】stack和queue的深度剖析及空间适配器的介绍

🧑‍💻作者: 情话0.0 📝专栏:《C从入门到放弃》 👦个人简介:一名双非编程菜鸟,在这里分享自己的编程学习笔记,欢迎大家的指正与点赞,谢谢! stack…

SMT车间贴片机Feeder管理方案

Feeder(飞达或供料器)是电子厂SMT车间贴片机上一个重要的部件,它的可用状态关系着贴片机生产的质量的稳定性,如何有效率的管理是每一位车间主管人员不可忽视的问题。根据行业协会大数据的分析发现导致贴片机大约30%的损失时间及1%的物料浪费都是因为Feed…

【Leetcode60天带刷】day14二叉树——144.二叉树的前序遍历,145.二叉树的后序遍历,94.二叉树的中序遍历

题目: 144. 二叉树的前序遍历 给你二叉树的根节点 root ,返回它节点值的 前序 遍历。 示例 1: 输入:root [1,null,2,3] 输出:[1,2,3]示例 2: 输入:root [] 输出:[]示例 3&#x…

5.4.1 虚拟专用网VPN

5.4.1 虚拟专用网VPN 我们已经学习了因特网的路由协议(5.3.1 因特网的路由协议(一)、5.3.2 因特网的路由协议(二)基于距离向量算法的RIP协议、5.3.3 因特网的路由协议(三)OSPF协议、5.3.4 因特…

【Docker】Docker的优势、与虚拟机技术的区别、三个重要概念和架构及工作原理的详细讲解

前言 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。 📕作者简介:热…

开利网络赋能祥兴事业集团推动乡村振兴数字化转型

近日,开利网络到访柳州祥兴实业集团,就即将举办的广西文旅大会数字化部署进行跟踪落地。以“祥兴百朋荷苑”为用户端,祥兴集团针对百朋景区实施了全流程的数字化系统构建,包含景区统一收银、景区导览导航讲解及扫码点餐、预约核销…