java多线程之线程安全(重点,难点)

由于操作系统中,线程的调度是抢占式执行的,或者说是随机的,这就造成线程调度执行时,线程的执行顺序是不确定的,虽然有一些代码在这种执行顺序不同的情况下也不会运行出错,但是还有一部分代码会因为执行顺序发生改变而受到影响,这就会造成程序出现Bug,对于多线程并发时会使程序出现bug的代码称作线程不安全的代码.
本质原因: 线程在系统中的调度是无序的/随机的(抢占式执行)
1. 线程不安全的原因:

序号线程不安全的原因
1抢占式执行(罪魁祸首)
2多个线程同时修改同一个变量
3修改操作不是原子的
4内存可见性
5指令重排序

多线程不安全的原因主要分为一下三种:

1.原子性

    多行指令,如果指令前后有依赖关系,不能插入其他影响自身线程执行结果的指令

2.可见性

    系统调用CPU执行线程内,一个线程对共享变量的修改,另一个线程能够立刻看到

3.有序性

    程序执行的顺序按照代码的先后顺序执行(处理器可能会对指令进行重排序)

1.1 抢占式执行

我们通过下面的代码来进行讲解:

class Demo{private static int count;//public static void countAdd(){count++;}//返回countpublic static int getCount() {return count;}
}public class ThreadDemo11 {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {//执行50000次count++Demo.countAdd();}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {//执行50000次count++Demo.countAdd();}});//执行t1线程t1.start();//执行t2线程t2.start();//等待t1,t2线程执行完t1.join();t2.join();//打印此时的count值System.out.println(Demo.getCount());}

此时我们可以看到,线程 t1 和线程 t2 分别对count进行50000次的自增,那么我们最后打印的值应该是100000吧~

此时我们运行看一下结果:

a40659551c1f7fb1cef290d07e1081e9.png

759925384f22356081f513f6aa595b0c.png

036148cf2b5217c77b7df5a0e37ad5e7.png

大家会发现,每次的运行结果都不一样,并且没有一次的值是正确的,到底是为什么呢?

小鱼给大家画图解释下:

4905916b4d404bbd81477b15bce8d4ca.png

    load: 从内存中将值读取到cpu寄存器中

    add: 将cpu寄存器的值进行+1

    save: 将寄存器的值读取到内存中

此时这里的结果错误,就是因为count++不是原子的而造成.

为什么这么说呢? 为了方便大家理解,我们用两个cpu内核来举例.内两个圆圆的东西就是 t1 的工作内存和 t2 的工作内存.

工作内存包含:cpu寄存器和缓存…

21061e6fdcf2751c1713402b63ba1cdc.gif

此时我们可以观看到,我们是在执行完 t1 线程的count++之后再去执行 t2 的count++的.此时我们的count是2,是正确的~~

但是呢,由于线程的随即调度,我们可能会存在这种情况:

8b4ffa9f90f07e4cf7127a8ea3c018e2.gif

由于线程是抢占式执行的,所以可能会存在,当 t1 线程刚进行 load 之后, t2 就也进行了 load,就是上图中左侧线程执行的顺序.当然,类似于这种插队式的组合方法多的数不清,上图运行的结果是count=1,但是我们的count已经自增两次了啊,应该是2的,此时出现的错误,就是线程安全的问题.

下面是一部分可能出现的随机排列情况:

07403cc678269ac68da54ae7b032e68e.png

1.2 多个线程修改同一个变量

上述的多线程安全问题就是因为多个线程对同一变量进行修改造成的.

大家看下面的表格:

原因安全性
一个线程修改一个变量安全
多个线程修改一个变量不安全
多个线程读取多个变量安全
多个线程修改多个不同变量安全

1.3 修改操作不是原子的

原子性: 不可分割的最小单位.

b0ff7c58e23a095db7523992b5e22359.png

通过上述过程我们知道,正因为有些操作不是原子的,导致两个或多个线程的指令排序存在更多的变数,自然就引发线程不安全的问题.

关于内存可见性,和指令重排序在后面讲到…

那我们如何解决上述的线程安全问题呢?

我们通过count++ 举例,如何让count++ 变成原子的呢?

我们可以通过加锁的方式将count++ 变成原子的.
锁(synchronized)

锁的核心操作分为两个:

(1) 加锁 : 当我们进入这个房间之后,别人就无法进入这个房间.

(2) 解锁 : 只有我们打开门,释放锁之后,别人才可以进入这个房间.

如果我进入这个房间之后,还有别人想要进入,就需要等我释放锁,走出这个房间才可以进入,别人在门口等待我的过程称为"阻塞".

当然,由于线程是抢占式执行的,所以如果我想进入这个房间就需要和那些都想进入这个房间的人进行争抢,当我进入这个房间之后,此时一群人只能在外面等着我,不能干别的事情,当我出了这个房间之后,他们又会开始新一轮的争抢,如果我还有进入房间的需要,我也会再次和他们一起争抢.

锁(synchronized) 这个关键字还有一个参数需要传进去,这个参数的类型需要是object或者它的子类…
1.一个锁对应一个锁对象.

关于这个锁对象的用途,这里通过生活中的例子讲解…

我们假设有两个老师,A,B他们都是教英语的,学校规定,这个学期英语课只有50节,工资按照你们每个人上课的次数决定,由于上课的教室只有一个,所以A,B老师先到先得.因为老师上课的时候不能被打扰,所以规定,这个教室一次只能进入一个老师,假设A老师在讲课,那么B老师如果想讲课的话只能等着,且不能做别的事情,只能等A老师讲完课并且出门之后,再次和A老师竞争上课的资格.

上面涉及到的知识点有:

(1) A,B老师先到先得(抢占式执行)(也可以称为锁竞争)

(2) 学校规定,一次只能进去一个老师(加锁)

(3)如果A老师讲课,B老师如果想讲课的话只能等着(阻塞)

(4) 只能等A老师讲完课并且出门之后(执行完锁内的代码块,并释放锁)

也可以用代码举例:

我们这里假设两个老师讲的课程一模一样,这50节课的内容从来没变.

class Teacher{static Object object = new Object();public static void AttendClass(){//此时两个进程都调用这个方法,为了防止老师讲课被打扰//对这个讲课进行加锁//此时这个锁的参数,也可以称为锁对象//我们可以将锁对象理解为教室,synchronized (object){//课程System.out.println("ABCDEFG....");ThreadDemo14.size--;}}
}
public class ThreadDemo14 {static int size = 50;public static void main(String[] args) {Thread A = new Thread(()->{while (size >= 0)Teacher.AttendClass();}) ;Thread B = new Thread(()->{while (size >= 0)Teacher.AttendClass();}) ;A.start();B.start();}
}

上述讲的例子是只在教室里面讲英语(只有一个锁,且只有一个锁对象).下面这个例子是不仅仅是讲英语…
2.多个锁对应一个锁对象.

依旧是先语言表达一下:

A,B两个老师,A英语老师,B语文老师,但是呢,只有一个教室可以用来上课,此时A,B老师就会为了这个教室的使用权而争抢,没有抢到的老师只能等着,依旧是50节课,谁上的课多谁工资高!

代码举例:

class Teacher{static Object object = new Object();public static void EnglishClass(){//注意这里synchronized (object){System.out.println("ABCDEFG...."+ThreadDemo14.size);ThreadDemo14.size--;}}public static void ChineseClass(){//注意这里synchronized (object){System.out.println("鹅鹅鹅,曲项向天歌...."+ThreadDemo14.size);ThreadDemo14.size--;}}
}
public class ThreadDemo14 {static int size = 50;public static void main(String[] args) {Thread A = new Thread(()->{while (size >= 0)Teacher.EnglishClass();}) ;Thread B = new Thread(()->{while (size >= 0)Teacher.ChineseClass();}) ;A.start();B.start();}
}

上述代码和之前的代码的区别就是,我上了两个锁,但是锁对象一样(多个锁,但是只有一个锁对象),这是什么意思呢? 意思就是,虽然我们讲的是不同的课程(锁的代码块不一样),但是这个锁对象一样,意味着我们需要在同一个教室讲课,一次只能有一个老师讲课.

更通俗点理解就是:不管你多少个老师,不管你教什么,只要是同一个教室(锁对象一样)那么,你就只能等这个教室空出来才能用…在此期间
2.多个锁对应多个锁对象.

但是呀,如果多个老师争抢一个教室,那么一定会引发不满的,所以个,学校就多建造了几个教室,那么这些老师就可以在不同的教室上课了…

class Teacher{
//教室1static Object classroom1 = new Object();//教室2static Object classroom2 = new Object();public static void EnglishClass(){//此时锁对象不同,意味着在不同的教室synchronized (classroom1){System.out.println("ABCDEFG...."+ThreadDemo14.size);ThreadDemo14.size--;}}public static void ChineseClass(){//此时锁对象不同,意味着在不同的教室synchronized (class2room){System.out.println("鹅鹅鹅,曲项向天歌...."+ThreadDemo14.size);ThreadDemo14.size--;}}
}

此时我们大概了解锁对象的意思了,科学一点的解释如下:

总结:
(1) 多个代码块使用了同一个同步监视器 ( 锁 ) , 锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中任何一个代码块.

(2) 多个代码块使用了同一个同步监视器 ( 锁 ) , 锁住一个代码块的同时,也锁住了所有该锁的所有代码块,但是没有锁柱使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块.
4. 找出代码错误

你也为锁这块讲完了吗?

天真! 上面还有一处错误! 不知道同学发现了没有…

我们来运行下两个英语老师的那个例子:

97d35bd32e98f622e26c8c849b653080.png

咦,怎么打印出来了-1,这是为什么呢?

有一种情况我们没有考虑到,就是…

fcc59437dcf8b707880ddb23c58cd2c6.png

33a84772b839081a76c2c1b404031bca.png

5dbe74044d045ae5b1eec371aceb3461.png

那么怎么解决呢?

f65b23e027ea8b433dc7b2589a792d7b.png

此时的运行结果:

164a8d34d8af07d6664c830c79e227f1.png

可能这个方法不是最优的,因为每次进入锁又需要进行一次if(),这些都是时间上的开销!!!
5. 锁的另一种用法

我们可以用synchronized来修饰方法,如果是用锁来修饰方法的话,我们不需要给这个锁设置参数,如果这个方法是静态的,那么这个锁的对象就是类名(该方法所处的类).class,如果是非静态的方法,那么谁调用这个方法,谁就是锁对象,也就是this.

代码如下:

非静态方法:

public  void func1(){synchronized (this){}}//两者等价synchronized public void func2(){}

静态方法:

class A1{public static void func1(){synchronized (A.class){}}//两者等价synchronized public void func2(){}}

1.4 内存可见性

什么是内存可见性呢? 大家看下面的代码,猜猜运行结果!

public class ThreadDemo15 {static boolean flog = false;public static void main(String[] args) {Thread t1 = new Thread(()->{while (!flog){//什么都不打印}//循环执行结束System.out.println("循环执行结束");});t1.start();try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}flog = true;System.out.println("flog改为true");}
}

两个选项:

A. 打印完flog之后就程序结束
B. 打印完flog之后程序没有结束

现在请看运行结果…

c3ed1860ba1c3dc066571b81193a487a.png

正确答案是B,为什么呢?

小鱼为大家解答.

我们通过一个例子来解释:

一天呢,玉帝派给孙悟空一个任务,要求他一直看着唐僧,看他会不会怀孕…
这孙悟空一听,男的会怀孕? 那不可能啊…

于是玉帝每次问孙悟空,孙悟空看都不看唐僧一眼就说没怀孕,不知过了多久,唐僧因为喝了女儿国的水,怀孕了!!! 就在唐僧怀孕之后,玉帝问孙悟空,你师傅怀孕了嘛?孙悟空依旧说不屑的回答:“没有”,殊不知唐僧孩子都快生出来了…

我们刚才的程序为什么会在我修改flog之后也一直在继续执行呢?

是因为,while(flog != false) 需要两步,从内存中读取flog的值到自己的寄存器,再将寄存器的值和false比较,由于呢~读内存(load)的操作很是麻烦,所以编译器就自作主张,想要优化这个代码,于是就不再去内存中读取了,直接将自己寄存器的值和false进行比较,但是这一不读取就出现了意外,代码内心: 你小子看我一眼啊,我都变了,我都变成true了,你丫还在循环!!!

上面出现线程安全的主要原因就是编译器优化,因为读内存的操作比读寄存器要慢几千倍,所以编译器为了运行效率,擅自做了决定.

所谓内存可见性就是在多线程的情况下,编译器对于代码优化,产生了误判,从而引起的一系列Bug,进而导致咱们的代码bug了.
解决内存可见性引发的线程安全问题(volatile)

我们对比这个代码:

public class ThreadDemo15 {static boolean flog = false;public static void main(String[] args) {Thread t1 = new Thread(()->{while (!flog){//什么都不打印//加入了一个时间限制try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}//循环执行结束System.out.println("循环执行结束");});t1.start();try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}flog = true;System.out.println("flog改为true");}
}

运行结果:

3433a7f8c06f8f66330bb6af277e824f.png

这个代码有sleep,加上sleep循环执行速度就变得很慢,当循环次数下降了,此时load不再是负担,编译器就没必要优化了.

但是我如果我就想让循环空转,并且还不能出错,该怎么处理?

咱么可以让编译器针对这个场景暂停优化.

如何做到呢?

有请接下来的主角: volatile

我们通过使用volatile关键字修饰变量,此时该变量就会禁止编译器优化,能够保证每次都是从内存中重新读取数据.

888be6c0d861d00afd8a5e4b6c72a38f.png

volatile是一个类型修饰符,作用是作为指令关键字,一般都是和const对应,确保本条指令不会被编译器的优化而忽略。
1.5 指令重排序

volatile还有一个用处就是禁止指令重排序.

指令重排序也是编译器优化的策略,调整了代码的执行顺序,让程序更高效.

前提: 保证代码逻辑不变,并且调整之后的结果要和之前是一样的.

关于指令重排序,小鱼给大家举个例子吧…

妈妈今天让小鱼去菜市场买菜,把买菜的清单列给了小鱼.

1b9496023a239f5422e052d062162380.png

小鱼发现,如果按照清单上的顺序购买,比较浪费时间.

3af35b687cd6073da609d34328144dd8.png

小鱼于是呢,就想换个路线…

73646c0a80e9528e3cf01040dd2bc136.png

当小鱼买完西红柿之后呢,妈妈给小鱼打电话,问小鱼买完西红柿了嘛,小鱼说买完了,妈妈说,那就抓紧回家吧~~

此时小鱼就到了家里,妈妈看到小鱼手里的西红柿陷入了沉思…

怎么只有西红柿,别的菜呢? 然后小鱼就挨打了,因为妈妈以为他是按照清单上的顺序去买的,当看到小鱼买完西红柿之后以他都买好了,就让他回来了,结果今天只能吃凉拌西红柿了…

我们也可以用代码来解释:

class Student {//成员变量static Student s;//成员方法public static Student getS() {return s;}//public static void main(String[] args) {Thread t1 = new Thread(()->{s = new Student();});Thread t2 = new Thread(()->{if(s != null) {s.getClass();}});t1.start();t2.start();}
}
s = new Studnet();

这和new的过程可以大体分为三部分.

1.申请内存空间
2.调用构造方法
3.把对象的引用赋值给 s

如果是在单线程的环境下,1,2,3的指令可以发生重排序,1先执行,2和3谁先谁后都可以.

在单线程情况下,这种优化并不会出现什么问题,但是在多线程情况下就不好说了…

872ab01c5dd24745d6065cc1001fe1ec.png

此时呢,为了避免指令重排序产生的线程安全问题,我们需要将

volatile static Student s;//进行volatile修饰

1dd66d769d7b2886b82828e750f038d7.png扫码关注7b391a7d5d4ce66993d0943a16bfa9a2.png

36945057c19b35c970351acd6e03c0e4.jpeg

 
 

1、微信闪退Bug罪魁祸首竟是二维码引擎,附源代码分析2、抖音服务器带宽有多大,才能供上亿人同时刷?3、Java网络编程之UDP和TCP套接字4、一天吃透计算机网络八股文5、IDE装上ChatGPT,彻底炸裂!6、求求你们了,别再重复造轮子了,一个 Spring 注解轻松搞定循环重试功能!6、全球最大ChatGPT开源平替来了!支持35种语言,写代码、讲笑话全拿

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【GPT-3】text-davinci -003 模型在Python中的应用,单次对话-连续对话-GUI界面

Navigator 一、Python1.1 最简单的单次问答1.2 连续对话1.3 GUI界面 首先简单说一下&#xff1a;调用openAI的模型&#xff0c;需要申请API key&#xff0c;用于鉴权和计费嘛。你注册之后有18美元体验金&#xff0c;3个月有效期。 更多信息可以看我上一篇文章&#xff0c;或者…

竞彩足球比分的概率chatGpt用泊松分布模型推算方法准确的计算分析出来

引言&#xff1a; 足球是世界上最受欢迎的体育运动之一&#xff0c;而竞彩足球成为了足球迷们热衷参与的一种形式。在竞彩足球中&#xff0c;预测比分是一项具有挑战性的任务。然而&#xff0c;通过人工分析比赛数据来预测足球比分需要耗费大量时间和精力。近年来&#xff0c;随…

软件对人们生活的改变的好处和坏处

软件工程对人们生活的改变 在我们的生活中&#xff0c;我们随处可见的都是信息化&#xff0c;在我国各行各业都离不开计算机软件的支持&#xff0c;软件为企业的管理带来了极大的帮助&#xff0c;提高了企业的收益&#xff0c;减少了人力、物力、财力的消耗&#xff0c;软件也为…

latex格式的英文文章中引用的中文参考文献在BIBTEX中的格式是如何的

查找了很多如何在bibtex中引用中文的参考文献&#xff0c;找到的总是bibtex如何在latex中的添加或者是显示&#xff0c;但是到底在bib文件中是如何写的呢&#xff1f; 中文文献在英文文献中引用首先就需要把它翻译成英文&#xff0c;而中文文献里都有英文标题&#xff0c;姓名…

参考文献去哪里查找,参考文献标准格式是什么

1、参考文献类型&#xff1a; 普通图书[M]、期刊文章[J]、报纸文章[N]、论文集[C]、学位论 文[D]、报告[R]、标准[s]、专利[P]、数据库[DB]、计算机程序[CP]、电 子公告[EB]、联机网络[OL]、网上期刊[J&#xff0f;OL]、网上电子公告[EB&#xff0f;OL]、其他未 说明文献[z]。…

Latex投稿Elsevier后被要求修改格式(五)如何合并多个参考文献

我才意外发现&#xff0c;之前写的latex代码一直未涉及到合并多个参考文献的问题&#xff0c;果然实践出真知&#xff0c;不断学习不断更新。 由于引用文献过多导致文章显得十分冗长与累赘&#xff0c;因此试图将参考文献合并&#xff0c;寻找相关代码普遍建议使用cite宏包&am…

Latex同时引用多篇文献修改字体/表格/图片/参考文献颜色

1、引用多篇参考文献 在yourpaper.tex上导入包cite&#xff0c;上面加上一行&#xff1a;\usepackage{cite} 然后在yourReference.bib文件加上参考文献 在yourpaper.tex上同时引用&#xff0c;方式如下&#xff1a; \cite{Ref2,Ref3,Ref4}2、修改字体/表格/图片颜色&#xf…

如何快速调整参考文献格式

1.查找文献并引用 可在百度学术网站输入关键字&#xff08;例如“绿波”&#xff09;查找文献&#xff0c;时间一般选择“近三年”&#xff0c;点击“批量引用”。 点击右下角的小红点&#xff0c;文献以列表显示&#xff0c;文献导出格式一般选择“GB/T 7714” &#xff0c;全…

字数超出显示...,鼠标划过显示完整内容

在微信公众号上看到这篇纯css实现多行文字截断&#xff0c;本来想在项目中练习使用一下&#xff0c;发现效果不太好&#xff0c;所以项目中还是使用了单行文本截断&#xff0c;下面是实现单行超出宽度&#xff0c;然后使用...显示的代码 .courseTitle{white-space: nowrap;text…

小程序textarea文本域字数控制---并显示已输入字数

有时候我们在写项目的时候&#xff0c;用到input或者textarea的时候&#xff0c;可能需要对输入的字数进行一个限制和显示&#xff0c;效果图如下&#xff1a; 输入文字后的效果图是这样的&#xff1a; 下面闲话少说 把这个小功能分享给大家&#xff0c;先是wxml&#xff1a;…

微信小程序 解决 view 文字 过多 溢出的问题,超过行数后隐藏显示省略号

单行 显示&#xff1a; overflow:hidden; //超出一行文字自动隐藏text-overflow:ellipsis;//文字隐藏后添加省略号white-space:nowrap; //强制不换行 显示多行文字&#xff0c;但不显示完全&#xff1a; display: -webkit-box;word-break: break-all;text-overflow: ellipsis…

Android TextView 显示字数的限制问题

Android TextView 显示字数的限制问题 —2021/06/07 今天写课设的时候碰到安卓的TextView字体超出了&#xff0c;本身的限制。csdn找了一下怎么解决这个问题。 未解决之前的 解决之后的 TextView中有个ellipsize属性,作用是当文字过长时,该控件该如何显示,解释如下: 1.androi…

Qt应用程序文字显示不全

用Qt写了一个模拟购票系统&#xff0c;在自己电脑&#xff08;Windows10,分辨率&#xff1a;1366*768&#xff09;上运行界面正常。当在别人电脑&#xff08;windows7、windows10&#xff0c;分辨率都为&#xff1a;1920*1080&#xff09;上运行时就出现了一些文字被遮挡覆盖的…

Prompt 用法大全!让 ChatGPT 更智能的六种策略(下)

上两篇介绍了 Pormpt 用法大全中前四种策略&#xff0c;本篇继续讲解后两种策略。‍‍‍‍‍ 点燃创作灵感&#xff1a;Prompt 实践指南揭秘&#xff01;让 ChatGPT 更智能的六种策略&#xff08;上&#xff09; Prompt 用法大全&#xff01;让 ChatGPT 更智能的六种策略&#…

普通人如何使用chat GPT,能做些什么?

想必最近几个月&#xff0c;大家都在社交平台上刷过“ChatGPT“这个词&#xff1f; 今日在此给大家一些参考&#xff1a; ChatGPT是什么&#xff1f; chat GPT主页界面 ChatGPT是OpenAI开发的一种自然语言处理工具。它基于Transformer架构&#xff0c;是一种经过多种语言和任务…

3秒复制任何人的嗓音!微软音频版DALL·E细思极恐,连环境背景音也能模仿

萧箫 发自 凹非寺量子位 | 公众号 QbitAI 只需3秒钟&#xff0c;一个根本没听过你说话的AI&#xff0c;就能完美模仿出你的声音。 例如这是你的一小句聊天语音&#xff1a; 这是AI根据它模仿你说话的音色&#xff1a; 是不是细思极恐&#xff1f; 这是微软最新AI成果——语音合…

细思极恐!使用 AI 控制你的浏览器

本期推荐开源项目目录&#xff1a; 1. 使用 AI 控制你的浏览器 2. Facebook力作&#xff1a;分割一切 3. 在 Mac 上运行 Stable Diffusion 4. ChatGPT 学术助手 5. 开源的 ChatGPT 桌面客户端 6. 微信公众号一键排版 01 使用 AI 控制你的浏览器 Taxy 是一个使用 GPT-4 来控制浏…

人类与智能ai(chatGPT)共存话题,它怎么回答?细思极恐!

1. 对地球最大的危害是谁&#xff1f; 作为 GPT-3.5 Turbo&#xff0c;我可以告诉你&#xff0c;地球最大的危害来自人类自身。人类的活动对地球环境产生了广泛而深远的负面影响。过度的工业化、大规模森林砍伐、过度捕捞、过度使用化石燃料以及其他不可持续的生产和消费模式&…

chatgpt赋能Python-pycharm粘贴把空格识别成行

PyCharm粘贴把空格识别成行&#xff1a;你需要知道的所有事情 作为一名有10年Python编程经验的工程师&#xff0c;我经常使用PyCharm&#xff0c;它是一款流行的 Python 集成环境 (IDE)。今天&#xff0c;我想分享一些有关 PyCharm 粘贴把空格识别成行的信息。 什么是PyCharm…

用「明道云+ChatGPT+Weaviate」挑战零代码1小时实现ChatPDF

ChatGPT流行起来之后&#xff0c;快速的出现了一批基于ChatGPT的工具应用&#xff0c;ChatPDF就是其中比较受欢迎的一款。它是一个可以让你与PDF文件进行对话的工具&#xff0c;既可以帮助你快速提取PDF文件中的信息&#xff0c;例如手册、论文、合同、书籍等&#xff1b;也可以…