(Java)多线程带来的的风险-线程安全 (第一部)

news2024/11/25 10:09:42

前言:线程安全是整个多线程中,最为复杂的部分,也是最重要的部分

目录

什么是线程安全问题?

线程不安全的原因

⁜⁜总结 :线程安全问题的原因 ⁜⁜

解决方法1 ——加锁 synchronized (监视器锁monitor lock)

 synchronized 的 特性

 —— 互斥 

—— 可重入 

 死锁

 可重入锁

 死锁拓展

哲学家就餐问题   

如何解决或者避免死锁?

 死锁的的成因

 解决死锁(重点⁜⁜)

—— 3. 请求保持

—— 4. 循环等待/环路等待

 解决方法2 —— volatile 关键字

 内存可见性

volatile


 

什么是线程安全问题?

有些代码,在单个线程中执行,完全正确,但如果同样的代码,让多个线程同时执行,就可能会出现bug。代码运行的结果不符合我们预期,

这种情况我们就称为 “线程安全问题” 或 “线程不安全”。

接下来我们结合具体的代码示例来理解一下 :

注意:这两个 join 必须得有,不然上面两个线程还没自增完,就开始打印了,count很有可能就打印个0 

ps: join在之前的博客中有介绍,线程等待那部分,链接https://blog.csdn.net/iiiiiihuang/article/details/132587614?spm=1001.2014.3001.5501

那 我们来看看上述代码的执行结果是否是我们所预期的:

很明显,这不是我们预期的结果100000,那我们把这个代码再执行一遍,发现这个结果又变了 

每次运行结果都不一样。

上述的情况就是典型的线程安全问题 。

这个代码 如果放在单个线程中执行,那一定是对的,但是当两个线程 并发执行了上述循环,此时逻辑就可能出现问题了 

线程不安全的原因

----- 上述代码为什么会出现这样的情况呢 ?

因为两个线程 并发执行了。

当我们把join 的位置换一下

这意味着当 t1 正在运行的过程中,t2 是不会启动的,也就是说,虽然上述代码是写在两个线程中的,但是并不是 “同时” 执行的 。

此时结果就是预期的100000

---- 那为什么同时(并发)执行时就出问题了呢? 

因为count++, 这个操作,本质上是分三步执行的,(站在CPU的角度上,count++ 是由CPU通过三个指令来实现的) 

  • 指令一: load      把数据从内存 读到 CPU寄存器中
  • 指令二: add       把寄存器中的数据进行 +1.
  • 指令三: save     把寄存器中的数据,保存到内存中。

上述过程单个线程肯定是没什么问题,

但是多个线程就要出问题了,因为当多个线程执行上述代码时,由于线程之间的调度顺序时随机的,就会导致在有些调度顺序下,上述的逻辑会出现问题。 

接下来我们画图看一下多个线程执行的情况。 

还有其他情况 

............

--- 上面已经有7种情况了,那这里到底有多少种情况呢? 

无数种 

因为也可能存在,t1 执行一次 count++ 的时候,t2执行了 2 次 ,3次 ...... 

结合了上述的讨论,我们意识到了,在多线程程序中,最困难的一点线程的随机调度 ,

使两个线程执行逻辑的顺序有诸多可能(不一定都是对的)。我们必须保证在所有可能的情况下,代码都是正确的!

⁜⁜总结 :线程安全问题的原因 ⁜⁜

1.在操作系统中,线程的调度顺序是 随机的(抢占式执行)。【万恶之源】

2.两个线程,针对同一个变量进行修改。 

3.修改操作,不是原子的。

   (前面的 count++, 就属于是 非原子 的操作。(先读,再修改),类似的,如果一段逻辑        中,需要根据一定的条件来决定是否修改,也是存在类似的问题)

4.内存可见性问题。

5.指令重排序问题。

(4,5后面会介绍)

ps: 什么是原子性?
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
 

解决方法1 ——加锁 synchronized (监视器锁monitor lock

 前两个原因,我们一般很难调整

那我们就集中在第三条了,我们可以想办法让 count++ 成为原子的------ 加锁!

--- 那如何给 Java 中的代码加锁呢? 

就是使用 synchronized 关键字,接下来我们就来介绍 synchronized 关键字.

 synchronized 的 特性

 —— 互斥 

 synchronized 在使用时要搭配代码块{ }进入 代码块{ } 就会加锁 出了代码块{ } 就会解锁

在已经加锁的状态中,另一个线程尝试同样加这个锁,就会产生 “锁冲突/锁竞争”,后一个线程就会堵塞等待,一直到前一个线程解锁为止。(本质上是把 “并发执行” 改为 “串行执行”)。

接下来我们具体写一下代码

执行结果是100000 ,因为加锁的对象是同一个,此时就产生了 ”锁冲突“。

synchronized 除了修饰代码块之外,还可以修饰一个 实例方法,或者修饰一个 静态方法 。

 ----- 修饰一个 实例方法

我们把count 搞成一个方法 。

如果没有加什么锁,那结果仍然会出现线程安全问题 

那我们现在就进行一个加锁的大动作:

直接在这个方法这加 

 

这时候就是我们所期待的10000了。 

  

这么看,synchronized 似乎没有 锁对象,其实是有的,此时就相当于 使用 this 作为 锁对象 。

这两种等价,可以认为 1 是 2 的简化版本。

----- 修饰一个 静态方法 

 如果修饰静态方法,相当于是针对 类对象 加锁。

这两个写法也是等价的 ,1 是 2 的简化版本。

但大家注意:我强调一下,这个锁对象是什么,不重要!!! 重要的是两个线程中 锁对象 是否是同一个对象。 

synchronized 用的锁是存在 Java 对象头 里的。

Java 的一个对象,对应的内存空间中,除了我们自己定义的属性之外,还有一些自带的属性 

这些自带的属性就是 对象头 在对象头中,就有属性表示当前对象 是否加锁。

—— 可重入 

 所谓的可重入锁,指的是,一个线程 连续针对一把锁  加锁两次,不会出现死锁

 满足这个要求,就是 “可重入”, 不满足这个要求,就是 “不可重入”。

 我们上述内容中,涉及到了死锁,那我们接下来就来介绍一些什么是死锁:

 死锁

类似下面的代码,针对同一个对象,连续加锁两次 

 

假设 第一次 加锁成功,那 locker 就属于是 “被锁定” 的状态,

那接下来,进行第二次 加锁,问题来了,此时locker 已经是锁定状态了,原则上来说,第二次加锁操作,应该是要 “阻塞等待” 的。应该要等待到,第一次的锁被释放之后,第二次才能加成功。

但是实际上,第二次加锁阻塞等待,那第一次加锁 的代码块就走不完,锁就释放不了,那第二次加锁就加不上 .......  无法进行了,这就死锁了(线程卡死了)。bug!!!

在日常开发中, 这种情况难以避免,当然不是像上边那样的啦~~~,举例:

可能一个加锁在func1里,一个在func4里,这些方法里,又存在调用关系 ,然后就死锁了

为了避免这种情况,我们就把 synchronized 设计成 “可重入锁” ,就能有效解决上述死锁问题了

 可重入锁

就是让锁记录一下,是哪个线程把它锁住的,后续再加锁的时候,如果要加锁的线程已经是持有锁的线程了,那就直接加锁成功!!!

欸嘿:那在下面的代码中,如果 synchronized 是可重入锁,没有因为第二次加锁而死锁 ,但是当代码执行到 } ①,此时,锁是否应该释放呢? 进一步来讲,如果上述加锁过程有N层,释放的时机该如何判定?

不行哦~~~ ,不能释放哦,你想如果 “ } ①” “ }② ” 中间还有代码呢,是不是这里的代码就没办法受到锁的保护了,就可能会出现线程安全问题。所有不能在“ } ①”, 要在“ }② ”这释放。

所有假设有 N 层,我们应该要在最外层进行释放。

那如何知道多少层呢? ———— 这里我们就 引用计数 :

锁对象 中,不光要记录是谁拿了锁,还要记录,锁被加了几次,

每加锁一次,计数器就 +1,每解锁一次,计数器就 - 1,

出了最后一个大括号,恰好就减成 0 了,这时才真正释放锁。

 死锁拓展

      1.一个线程,针对一把锁,连续加锁两次,如果是不可重入锁,就死锁了。 (我们Java这边的synchronized 不会出现不可重入锁,但是C++ 的 std::mutex 就是不可重入锁,就会出现死锁。)

      2.两个线程 两把锁,此时无论是不是可重入锁,都会死锁

—— 2 是什么意思呢?

 就假如现在有 线程t1 和 线程t2,还有两把锁 A,B

 现在t1 获取锁A,t2 获取锁B,然后t1 尝试获取锁B,t2 尝试获取锁A,  这时候也死锁了

 我们用代码来演示一下 两把锁死锁情况:

此处的 sleep 很重要,它保证了 t1 和 t2 都分别拿到了一把锁之后,再进行后续动作

package thread;

/**
 * @Author: iiiiiihuang
 */
public class Demo18 {
    //两把锁
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                //此处的 sleep 很重要,它保证了 t1 和 t2 都分别拿到了一把锁之后,再进行后续动作
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2) {
                    System.out.println("t1 加锁成功");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker1) {
                    System.out.println("t2 加锁成功");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

我们运行看一下,啥也没有,死锁了 

 

我们可以借助 jconsole 来看看两个线程的状态:

这个Thread-0 和 Thread-1 就是刚才两个线程

我们看看Thread-0线程具体的 状态和调用栈,可以看到这个线程是 BLOCKED 状态(因为锁竞争导致的阻塞),这个要等待(竞争)的锁是 Thread-1 持有的

我们再看看Thread-1线程

BLOCKED 状态,这个要等待(竞争)的锁是 Thread-0 持有的 

大家注意,上面的死锁代码,两个 synchronized 是嵌套关系,不是并列关系,

嵌套关系说明,是在占用一把锁的前提下,获取另一把锁(则是可能出现死锁),

并列关系,则是先释放前面的锁,再获取下一把锁(不会死锁)。

N个线程,M把锁 ,此时更容易出现死锁。

这里有个经典描述 N个线程,M把锁 的死锁的模型———— 哲学家就餐问题  

哲学家就餐问题   

一个桌子上有一碗面,桌子周围 坐这 5个哲学家, 每两个哲学家 中间放一根筷子(共五根)

 

 

规则(哲学家):(1)思考人生,放下筷子,啥都不干

                             (2)吃面条, 拿起左右两边的筷子,开始吃面条

                             (3)哲学家啥时候吃面条,啥时候思考人生,是随机的

                             (4)哲学家正在吃面条的过程中,会持有左右两侧的筷子,此时相邻的哲学家                                        如果也想吃面条,就需要阻塞等待

(哲学家就 等于 线程 , 筷子就等于锁)

基于上述规则,通常情况下,整个系统,可以良好运转,但是极端情况下,就会出问题,

比如,同一时刻,五个哲学家都想吃面条,同时拿起了左手的筷子 ,此时,五个哲学家发现,他们的右手都没有筷子,于是他们就都得阻塞等待,等待过程中,他们都不会放下左手筷子,

那就谁都吃不了了,就进入死锁状态。

死锁,是属于严重的Bug,那如何解决或者避免死锁呢? 

如何解决或者避免死锁?

 死锁的的成因

首先我们要了解死锁的的成因,这里涉及四个 必要条件 (数学里的必要条件) 

  1.  互斥使用(上面讲到了,这里再说一遍):锁的基本特性,当一个线程持有一把锁之后,另一个线程也想获取到同一把锁,就要阻塞等待。
  2. 不可抢占锁的基本特性,当锁已经被线程1拿到之后,线程2 只能等线程1 主动释放,不能强行抢过来。
  3. 请求保持代码结构,一个线程尝试获取多把锁,(先拿到锁1 之后,再尝试获取锁2,获取的时候,锁1 不会释放(吃着碗里的,看着锅里的))。
  4. 循环等待/环路等待:等待的依赖关系,形成环了,就像上面介绍的哲学家就餐极端情况

要想出现死锁,得把上面的4条全都占了

所有解决死锁,核心就是破坏上诉必要条件,只要破坏一个,死锁就形成不了。

 解决死锁(重点⁜⁜)

—— 3. 请求保持

1和2,无法破坏,对于 3 ,只需要调整代码结构,避免编写 “锁嵌套” 逻辑。

比如说我们把上面的代码变一下 ,把嵌套变为并列关系。

执行:

加锁成功了 。

 但是这个不一定好使,因为有时候,可能就需要进行“锁嵌套”的操作(获取多个锁),所有我们看看 4.

—— 4. 循环等待/环路等待

对于4来说,可以约定加锁的顺序,就可以避免循环等待。 

比如,针对锁,进行编号,约定 加多把锁的时候,先加编号小的锁,后加编号大的锁。(所有的线程都要遵守这个规则)

我们还拿哲学家就餐问题来看 :

1.先给筷子(锁)编号

2.约定,每个滑稽都是先拿起编号小的筷子,后拿起编号大的筷子。

有一个哲学家,没有拿 5 号筷子 ,而是拿到 1 号筷子,(因为这个哲学家两边是 1 和 5,1 < 5, 所以他拿1 筷子,)但是1 被占了,那这个哲学家就得阻塞等待一下,这就给拿 4 号筷子的哲学家一个机会,拿起 5 号筷子,这样他就可以吃面了,他吃完了,就可以放下 4,5号筷子,那拿 3号筷子的人就可以拿起 4号了,以此类推 。此时循环等待就破除了,这不就不死锁。

代码演示 (从锁编号小的开始)

加锁成功了 

 上面加锁方法是解决 原因 3:原子问题,那 4 和 5 该如果解决,这就需要volatile 关键字

 解决方法2 —— volatile 关键字

1.保证内存可见性

2.禁止指令重排序

 内存可见性

 计算机运行的程序/代码,经常要访问一些数据,这些数据,往往还存储再 内存中 ,比如我们定义一个变量,变量就是在内存中的,

当CPU使用这个变量时,就会把这个内存中的数据,先读出来,放到CPU的寄存器中,之后再参与运算(这个就是上面的load操作)。

 那此时问题就来了,CPU读取内存的操作,其实非常慢(相对来说 读取速度:寄存器 > 内存 > 硬盘)

CPU在进行大部分操作时都很快,但是操作到读/写 内存时,速度就慢下来了。

这时候,编译器为了解决上述问题,提高效率,就可能对代码做出优化,把一些本来要读内存的操作,优化成读取 寄存器,减少读内存的次数,也就可以提高整体程序的效率了。(⁜⁜)

 这个就是内存可见性的基本情况。

接下来我们用代码演示一下, 内存可见性引发的线程安全问题:

我们现在运行一下程序,预期结果:用户输入 非0值后,t1线程 退出 

但当我们输入 1 时(非0),t1线程并没有结束退出 ,我们通过jconsole 也能看到,t1线程正在执行(RUNNABLE)

 这与预期的不一样,是个bug,且这个bug 是由 多线程引起的,这也是线程安全问题。

我们发现这个问题和之前的count++ 那里还不一样,之前是两个线程,同时修改同一个变量(count) 出了问题,现在是一个线程读(t1),一个线程修改(t2),也可能出现问题。

这样的问题就是由于 内存可见性 引起的

我们来解释一下: 

但是 这个循环里面啥都没有,导致循环速度飞快,短时间内,就会进行大量的循环,也就是进行大量的load 和 cmp 操作,

此时,编译器就发现了,虽然进行了这么多次 load,但是load 出来的结果都一样,这个isQuit就一直没变过,(load操作有非常费时间,一次load相当于上万次 cmp 了)

所有,我们编译器就决定,只是第一次循环的时候,才读了内存,后续都不会再读内存了,而是直接从寄存器中,取出isQuit 的值,

这就是编译器优化。编译器是希望能够提高程序的效率,但是提高效率的前提是保证逻辑不变,此时由于修改isQuit 的代码是另一个线程的操作,编译器没有正确的判定,上述可知,编译器以为没人修改isQuit, 就做出了这样的优化,就引起了bug 

所以,之后t2修改了isQuit ,但是t1 感知不到内存的变化,

这个问题我们就称为“内存可见性问题”

volatile就是解决方案 

volatile

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

 上述的问题,我们只需要给isQuit 加上volatile 修饰,编译器就会禁止上述优化了

运行成功 

但是如果你在循环里加一个 sleep,不加volatile,t1线程也可以顺利退出,因为加了sleep的循环速度慢了,次数就少了,load操作开销就不大了,因此,优化就没必要进行了,不触发load 的优化,也就没有 触发 内存可见性问题。 

但是吧,到底啥时候代码优化了,啥时候又没优化,又说不清,所有使用volatile 是更好的选择。 

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

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

相关文章

算法与设计分析--实验一

蛮力算法的设计与分析&#xff08;暴力&#xff09; 这次是某不知名学院开学课程的第一次实验&#xff0c;一共5道题&#xff0c;来自力扣 第一题.216组合总和*力扣题目链接 第一道题是经典的树型回溯 class Solution { public:vector<vector<int>> combinatio…

红米Note12Turbo解锁BL刷入PixelExperience原生ROM系统详细教程

红米Note12Turbo的兄弟是国外POCO F5 机型&#xff0c;并且该机性价比非常高&#xff0c;国内外销量也还可以&#xff0c;自然不缺第三方ROM适配。目前大家心心念念的原生PixelExperience已成功发布&#xff0c;并且相对来说&#xff0c;适配程度较高&#xff0c;已经达到日用的…

sqlserver union和union all 的区别

1.首先在数据库编辑1-40数字&#xff1b; 2.查询Num<30的数据&#xff0c;查询Num>20 and Num<40的数据&#xff0c;使用union all合并&#xff1b; 发现30-20的数字重复了&#xff0c;可见union all 不去重&#xff1b; 3.查询Num<30的数据&#xff0c;查询Num…

嵌入式Linux驱动开发(同步与互斥专题)(一)

一、内联汇编 1.1、语法 内联汇编实现加法 1.2、同步互斥失败的例子 进程A在读出valid时发现它是1&#xff0c;减1后为0&#xff0c;这时if不成立&#xff1b;但是修改后的值尚未写回内存&#xff1b;假设这时被程序B抢占&#xff0c;程序B读出valid仍为1&#xff0c;减1后为…

Callable、Future和FutureTask

一、Callable 与 Runnable 先说一下java.lang.Runnable吧&#xff0c;它是一个接口&#xff0c;在它里面只声明了一个run()方法&#xff1a; public interface Runnable {public abstract void run(); }由于run()方法返回值为void类型&#xff0c;所以在执行完任务之后无法返…

云数据库知识学习——概述

一、云计算是云数据库兴起的基础 云计算是分布式计算、并行计算、效用计算、网络存储、虚拟化、负载均衡等计算机和网络技术发展融合的产物。云计算是由一系列可以动态升级和被虚拟化的资源组成的&#xff0c;用户无需掌握云计算的技术&#xff0c;只要通过网络就可以访问这些资…

关于近期小程序测试的常见漏洞演示

本章节将为大家介绍一下小程序常见的漏洞的展示案例&#xff0c;包括支付业务逻辑漏洞、任意用户登录漏洞、水平越权漏洞等高危漏洞。 以上小程序测试均获取授权&#xff0c;并且客户均已得到修复(仅供学习&#xff0c;请勿恶意攻击)​ 关于微信小程序如何拦截数据包&#xff…

Nat. Communications Biology2022 | PepNN+: 用于识别多肽结合位点的深度关注模型

论文标题&#xff1a;PepNN: a deep attention model for the identification of peptide binding sites 论文链接&#xff1a;PepNN: a deep attention model for the identification of peptide binding sites | Communications Biology 代码地址&#xff1a;oabdin / PepN…

csp非零段划分

202109-2 非零段划分 计算机软件能力认证考试系统 code&#xff1a; #include<bits/stdc.h> using namespace std; const int N5e59;int a[N];vector<int> v[N];//v[i]存放所有元素值为i的元素的下标 int main() {ios::sync_with_stdio(false);cin.tie(0),cout.…

20230908_python练习_服务端与客户端数据交互

用户可以通过简单操作进行服务端数据交互&#xff0c;通过简单的sql语句直接获取EXCEL表&#xff0c;可以用来作为交互的基础。主要涉及三部分&#xff1a; 1:数据库存储表结构 --日志记录表结构 create table shzc.yytowz_service_title (leixing varchar2(18),ziduan1 v…

C#__多线程之任务和连续任务

/// <summary> /// /// 任务&#xff1a;System.Threading.Tasks&#xff08;异步编程的一种实现方式&#xff09; /// 表应完成某个单元工作。这个工作可以在单独的线程中运行&#xff0c;也可以以同步方式启动一个任务。 /// /// 连续任务&#…

【笔试强训选择题】Day36.习题(错题)解析

作者简介&#xff1a;大家好&#xff0c;我是未央&#xff1b; 博客首页&#xff1a;未央.303 系列专栏&#xff1a;笔试强训选择题 每日一句&#xff1a;人的一生&#xff0c;可以有所作为的时机只有一次&#xff0c;那就是现在&#xff01;&#xff01; 文章目录 前言一、Day…

蚂蚁发布金融大模型:两大应用产品支小宝2.0、支小助将在完成备案后上线

9月8日&#xff0c;在上海举办的外滩大会上&#xff0c;蚂蚁集团正式发布金融大模型。据了解&#xff0c;蚂蚁金融大模型基于蚂蚁自研基础大模型&#xff0c;针对金融产业深度定制&#xff0c;底层算力集群达到万卡规模。该大模型聚焦真实的金融场景需求&#xff0c;在“认知、…

六大排序算法(Java版):从插入排序到快速排序(含图解)

目录 插入排序 (Insertion Sort) 直接插入排序的特性总结&#xff1a; 选择排序 (Selection Sort) 直接选择排序的特性总结 冒泡排序 (Bubble Sort) 冒泡排序的特性总结 堆排序&#xff08;Heap Sort&#xff09; 堆排序的特性总结 希尔排序 (Shell Sort) 希尔排序的…

【Redis】3、Redis主从复制、哨兵、集群

Redis主从复制 主从复制&#xff0c;是指将一台Redis服务器的数据&#xff0c;复制到其他的Redis服务器。前者称为主节点(Master)&#xff0c;后者称为从节点(Slave)&#xff1b;数据的复制是单向的&#xff0c;只能由主节点到从节点。 默认情况下&#xff0c;每台Redis服务器…

SpringMvc 之crud增删改查应用

目录 1.创建项目 2.配置文件 2.1pom.xml文件 2.2 web.xml文件 2.3 spring-context.xml 2.4 spring-mvc.xml 2.5 spring-MyBatis.xml 2.6 jdbc.properties 数据库 2.7 generatorConfig.xml 2.8 日志文件log4j2 3.后台代码 3.1 pageBean.java 3.2切面类 3.3 biz层…

米尔电子|第十六届STM32全国研讨会即将走进11个城市

2023年9月12日至10月27日&#xff0c;以“STM32&#xff0c;不止于芯”为主题的第十六届STM32全国巡回研讨会将走进11个城市。本届研讨会为全天会议&#xff0c;我们将围绕STM32最新产品开展技术演讲和方案演示。 本次STM32全国研讨会&#xff0c;米尔电子将现场展出STM32相关…

Unity——导航系统补充说明

一、导航系统补充说明 1、导航与动画 我们可以通过设置动画状态机的变量&#xff0c;让动画匹配由玩家直接控制的角色的移动。那么自动导航的角色如何与动画系统结合呢&#xff1f; 有两个常用的属性可以获得导航代理当前的状态&#xff1a; 一是agent.velocity&#xff0c;…

GO语言网络编程(并发编程)并发介绍,Goroutine

GO语言网络编程&#xff08;并发编程&#xff09;并发介绍&#xff0c;Goroutine 1、并发介绍 进程和线程 A. 进程是程序在操作系统中的一次执行过程&#xff0c;系统进行资源分配和调度的一个独立单位。 B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更…

Java“牵手”天猫商品详情数据,天猫商品详情API接口,天猫API接口申请指南

天猫平台商品详情接口是开放平台提供的一种API接口&#xff0c;通过调用API接口&#xff0c;开发者可以获取天猫商品的标题、价格、库存、月销量、总销量、库存、详情描述、图片等详细信息 。 获取商品详情接口API是一种用于获取电商平台上商品详情数据的接口&#xff0c;通过…