详解设计模式:单例的进化之路

概念

单例模式(Singleton Pattern)是设计模式中一个重要的模式之一,是确保一个类在任何情况下都绝对只有一个实例。单例模式一般会屏蔽构造器,单例对象提供一个全局访问点,属于创建型模式。

根据初始化时间的不同,可以将单例模式分为两类:

  • 饿汉式单例
  • 懒汉式单例

当然,除了上面的两个分类之外,处于对性能、安全等方面的考量,单例模式还演化出了各种实现版本,每一种版本的演进,都是单例的一次**进化与升级,**下面就来看看单例模式的进化之路上都经历了哪些挑战与对抗。

饿汉式单例

饿汉式单例,特指在 类加载的时候就立即初始化并创建单例对象的一种单例模式写法。由于是在线程还没有出现之前就被实例化了,所以这种模式下的单例是线程绝对安全的,不存在访问安全的问题。

根据具体的实现方式划分,饿汉式单例可以通过 私有化构造器以及 使用静态代码块的方式具体实现。

  • 私有化构造器写法:HungrySingleton.java
/*** @author: 八尺妖剑* @date: 2023/1/31 9:32* @email: ilikexff@gmail.com* @blog: https://www.waer.ltd* @Description: 饿汉式单例-构造器私有化写法*/
public class HungrySingleton {private static final HungrySingleton hungrSingleton  = new HungrySingleton();private HungrySingleton() {};public static HungrySingleton getInstance() {return hungrSingleton;}
}

上面的代码中,我们将构造器进行了私有化之后,无法再通过new来创建对象,这种实现下,只能通过提供的getInstance()方法来获得单例对象。

  • 静态代码块写法:HungryStaticSingleton.java
package 设计模式.单例模式.饿汉式单例;/*** @author: 八尺妖剑* @date: 2022/4/23 8:36* @description: 饿汉式单例静态块写法* @blog:www.waer.ltd*/
@SuppressWarnings({"all"})
public class HungryStaticSingleton {private static final HungryStaticSingleton hungryStaticSingleton;static {hungryStaticSingleton = new HungryStaticSingleton();}private HungryStaticSingleton(){}public static HungryStaticSingleton getInstance(){return hungryStaticSingleton;}
}
  • 测试类
package ltd.waer.javabaseforio.PatternDesign;@SuppressWarnings("all")
/*** @author: 八尺妖剑* @date: 2023/1/31 9:42* @email: ilikexff@gmail.com* @blog: https://www.waer.ltd* @Description: 饿汉式单例测试类*/
public class HungrySingletonTest {public static void main(String[] args) {//私有构造器写法HungrySingleton hungrySingleton1 = HungrySingleton.getInstance();HungrySingleton hungrySingleton2 = HungrySingleton.getInstance();System.out.println(hungrySingleton1 == hungrySingleton2);//静态块初始化写法HungryStaticSingleton singleton3 = HungryStaticSingleton.getInstance();HungryStaticSingleton singleton4 = HungryStaticSingleton.getInstance();System.out.println(singleton3==singleton4);}
}

测试结果:true。说明两种方式实现的单例都是有效的,因为不论我们调用多少次 getInstance 方法最后返回的就是同一个对象

优缺点:

创建的对象没有添加任何锁,执行效率高。

由于是在类加载的时候就初始化了,所以不管我们使用与否,它都将占有一定的内存空间,这种情况下,通过项目中存在了大量的单例,那么所占用的内存量就很可观了,着实浪费。

懒汉式单例

那么针对上述饿汉式单例存在的空间占用问题,有没有合适的替换或者解决方案呢?那么有请懒汉出场。
见名知意, 懒汉式单例饿汉式单例的理念刚好相反。它不会在 类加载的时候就初始化,而是等到用到了才会初始化,就这点来说,确实很 懒汉,不饿不吃饭(似乎有点道理??我不饿的时候也不想吃饭)。

到这里,单例模式就开始自己的进化之路了,下面列一下进化路线

进化主线:

  • 普通非线程安全单例
    • sync线程安全单例
      • 双重检查锁单例
        • 内部类单例
          • 枚举式单例

打野副本:

  • 内部类单例
    • 注册式单例
    • 单线程安全单例
      • 枚举式单例

1. 普通非线程安全单例

package ltd.waer.javabaseforio.PatternDesign.LazySingleton;
@SuppressWarnings("all")
/*** @author: 八尺妖剑* @date: 2023/1/31 9:47* @email: ilikexff@gmail.com* @blog: https://www.waer.ltd* @Description: 版本一:非线程安全*/
public class LazySingleton {private static LazySingleton lazySingleton = null;private LazySingleton() {};public static LazySingleton getInstance() {if (null == lazySingleton) {lazySingleton = new LazySingleton();}return lazySingleton;}public static void main(String[] args) {LazySingleton instance1 = LazySingleton.getInstance();LazySingleton instance2 = LazySingleton.getInstance();System.out.println(instance1 == instance2);}
}

上面是单例的最简单写法,也是最初的一种版本,在开始时将实例赋值为null,并没有进行初始化,而是在调用getInstance方法的时候才会初始化,虽然实现简单,但也存在线程安全问题,多线程环境下有一定几率会返回多个单例对象,这显然违背了单例的原则,进一步的解决办法就是下面这种实现。使用synchronizeed关键字保证线程安全。

2. sync线程安全单例

package ltd.waer.javabaseforio.PatternDesign.LazySingleton;
@SuppressWarnings("all")
/*** @author: 八尺妖剑* @date: 2023/1/31 9:51* @email: ilikexff@gmail.com* @blog: https://www.waer.ltd* @Description: 线程安全的懒汉式单例-synchronized*/
public class LazySyncSingleton {private static LazySyncSingleton lazySyncSingleton = null;private LazySyncSingleton() {};public synchronized LazySyncSingleton getInstance () {if (null == lazySyncSingleton) {lazySyncSingleton = new LazySyncSingleton();}return lazySyncSingleton;}
}

上面的实现也非常简单,在前面一种写法的基础山加了一个synchronized关键字即可,这样确实解决了线程安全的问题,但也引出了一个新的问题,假如单例对象的创建非常复杂耗时的情况下,一旦并发量上来了,CPU压力上升,那么可能会导致大批量线程出现阻塞的情况,从而导致程序的允许性能大幅下降,解决方法是**双重检查锁(double-checked locking)**单例写法,如下:

3. 双重检查锁单例

package ltd.waer.javabaseforio.PatternDesign.LazySingleton;
@SuppressWarnings("all")
/*** @author: 八尺妖剑* @date: 2023/1/31 9:59* @email: ilikexff@gmail.com* @blog: https://www.waer.ltd* @Description: 解决写法2的问题,双重检查锁写法*/
public class LazyDoubleCheckSingleton {private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;private LazyDoubleCheckSingleton () {};public static LazyDoubleCheckSingleton getInstance() {if ( null == lazyDoubleCheckSingleton) {synchronized (LazyDoubleCheckSingleton.class) {if (null == lazyDoubleCheckSingleton) {lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();}}}return lazyDoubleCheckSingleton;}
}

这种写法中,较于上面的写法做了两个地方的改变。

  • lazyDoubleCheckSingleton属性加上了volatile关键字,原因就是为了解决多线程下可见性问题,因为我们的getInstance方法在判断lazyDoubleCheckSingleton是否为null时并没有加锁,所以假如线程1初始化了对象,另外线程2是无法感知的,而加上了volatile之后便可以解决这个问题。

  • synchronized关键字移到了方法内部,尽可能缩小加锁的代码块,提升效率

迭代了这几个版本,到这里是否就已经完美了呢?其实不是,这种写法依旧存在问题,那就是指令重排问题。

上面new对象只有一行代码,然而这行代码在JVM底层却分成了3步:

  1. 分配内存来创建对象,即new操作。
  2. 创建一个对象lazyDoubleCheckSingleton此时lazyDoubleCheckSingleton==nul
  3. new出来的对象赋给lazyDoubleCheckSingleton

但实际运行的时候为了提升效率,这3步并不会按照实际顺序来运行。

假如线程t1进入同步代码块正在创建对象,而此时执行了后面2步,也即是此时lazyDoubleCheckSingleton依已经不为null了,但是对象却没有创建结束,这时候又来了一个线程t2进入getInstance方法,这时候if条件不再成立,线程t2会直接返回一个残缺不全的对象,自然会出现报错。

为了解决这个问题,下面引出了第四个单例版本,即

4. 内部类单例

package ltd.waer.javabaseforio.PatternDesign.LazySingleton;
import java.io.Serializable;
import java.lang.reflect.Constructor;
@SuppressWarnings("all")
/*** @author: 八尺妖剑* @date: 2023/1/31 10:48* @email: ilikexff@gmail.com* @blog: https://www.waer.ltd* @Description: 内部类懒汉式单例-解决指令重排问题*/
public class LazyInnerClassSingleton  implements Serializable {private LazyInnerClassSingleton () {};public static final LazyInnerClassSingleton getInstance() {return InnerLazy.LAZY;}private static class InnerLazy {private static final LazyInnerClassSingleton LAZY =new LazyInnerClassSingleton();}
}

这种写法巧妙的利用了内部类会等到外部调用时才会被初始化的特性,用饿汉式单例的思想实现了懒汉式单例。

这种写法看起来已经是高效完美,但其实存在安全隐患,比如可以通过反射的方式破坏这种写法,测试代码如下:

public static void main(String[] args) throws Exception {Class<?> clazz = LazyInnerClassSingleton.class;Constructor constructor = clazz.getDeclaredConstructor();constructor.setAccessible(true);Object o1 = constructor.newInstance();Object o2 = LazyInnerClassSingleton.getInstance();System.out.println(o1 == o2); //false
}

可以看到,虽然构造方法被私有化了,但是我们仍然可以利用反射来破坏单例。为了防止反射破坏单例,我们将上面的写法再改造一下。

5. 改进版的内部类单例

public class LazyInnerClassSingleton {private LazyInnerClassSingleton(){//防止反射破坏单例if(null != InnerLazy.LAZY){throw new RuntimeException("不允许通过反射类构造单例对象");}}public static final LazyInnerClassSingleton getInstance(){return InnerLazy.LAZY;}private static class InnerLazy{private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();}
}

尽管如此,但假如我们的单例对象实现了 Serializable 接口,那么内部类的写法就还是能通过序列化来破坏

6. 实现了Serializable接口的内部类单例

package singleton.lazy;import java.io.Serializable;public class LazyInnerClassSingleton implements Serializable {private LazyInnerClassSingleton(){//防止反射破坏单例if(null != InnerLazy.LAZY){throw new RuntimeException("不允许通过反射类构造单例对象");}}public static final LazyInnerClassSingleton getInstance(){return InnerLazy.LAZY;}private static class InnerLazy {private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();}
}

由于实现了序列化的接口,所以内部类的写法依然可以通过序列化来进行破坏,比如使用下面这段测试代码。

package singleton.lazy;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;public class TestLazyInnerClassSingleton2 {public static void main(String[] args) {//序列化攻击内部类式单例LazyInnerClassSingleton s1 = null;LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();FileOutputStream fos = null;try {fos = new FileOutputStream("LazyInnerClassSingleton.text");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(s2);oos.flush();oos.close();FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.text");ObjectInputStream ois = new ObjectInputStream(fis);s1 = (LazyInnerClassSingleton)ois.readObject();ois.close();System.out.println(s1 == s2);//输出:false}catch (Exception e){e.printStackTrace();}}
}

上面示例中 s1 是通过我们自己提供的全局入口创建的对象,而 s2 是通过序列化的方式创建的对象,不相等说明这是两个对象,也就是说序列化破坏了单例模式。

解决办法就是在 LazyInnerClassSingleton 类中加一个 readResolve 方法,防止序列化破坏单例。

7. 再改进版的内部类单例

package singleton.lazy;import java.io.Serializable;public class LazyInnerClassSingleton implements Serializable {private LazyInnerClassSingleton(){//防止反射破坏单例if(null != InnerLazy.LAZY){throw new RuntimeException("不允许通过反射类构造单例对象");}}public static final LazyInnerClassSingleton getInstance(){return InnerLazy.LAZY;}private static class InnerLazy {private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();}//防止通过序列化破坏单例private Object readResolve(){return InnerLazy.LAZY;}
}

这次返回了 true,也就是序列化没有破坏单例了。原因是因为 JDK 源码中在序列化的时候会检验一个类中是否存在一个 readResolve 方法,如果存在,则会放弃通过序列化产生的对象,而返回原本的对象。

这种方式虽然保证了单例,但是在校验是否存在 readResolve 方法前还是会产生一个对象,只不过这个对象会在发现类中存在 readResolve 方法后丢掉,然后返回原本的单例对象。这种写法只是保证了结果的唯一,但是过程中依然会被实例化多次,假如创建对象的频率增大,就意味着内存分配的开销也随之增大。

上面介绍了这么多种写法,看起来每种写法似乎都存在问题,难道就没有一种最优雅、安全、高效的方法吗?这就是我们最后要介绍的枚举式单例,不过在介绍枚举式单例之前,我们先刷一下副本,看看其它写法。

8. 注册式单例

将每一个实例都保存起来,然后在需要使用的时候直接通过唯一的标识获取实例,这便是注册式单例。

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;public class ContainerSingleton {private ContainerSingleton(){}private static Map<String,Object> ioc = new ConcurrentHashMap<>();public static Object getBean(String className){synchronized (ioc){//如果容器中不存在当前对象if(!ioc.containsKey(className)){Object obj = null;try {obj = Class.forName(className).newInstance();//将className作为唯一标识存入容器ioc.put(className,obj);}catch (Exception e) {e.printStackTrace();}return obj;}}//如果容器中已经存在了单例对象,则直接返回return ioc.get(className);}
}

新建一个空对象 MyObject.java,用来测试单例。

package singleton.register;public class MyObject {
}

新建一个测试类 TestContainerSingleton.java

package singleton.register;public class TestContainerSingleton {public static void main(String[] args) {MyObject myObject1 = (MyObject) ContainerSingleton.getBean("singleton.register.MyObject");MyObject myObject2 = (MyObject) ContainerSingleton.getBean("singleton.register.MyObject");System.out.println(myObject1 == myObject2);//输出:true}
}

上面返回 true 是因为我们加了 synchronized 关键字,实际上 Spring 框架中用的就是容器式单例,默认是线程不安全的。

9. 单线程安全单例

基于ThreadLocal实现,该单例不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,在单线程环境下线程天生安全。

import java.util.concurrent.ThreadFactory;
public class ThreadLocalSingleton {private ThreadLocalSingleton(){}private static final ThreadLocal<ThreadLocalSingleton> singleton = new ThreadLocal<ThreadLocalSingleton>(){@Overrideprotected ThreadLocalSingleton initialValue() {return new ThreadLocalSingleton();}};public static ThreadLocalSingleton getInstance(){return singleton.get();}
}

测试类:

public class TestThreadLocalSingleton {public static void main(String[] args) {System.out.println(ThreadLocalSingleton.getInstance());//主线程输出System.out.println(ThreadLocalSingleton.getInstance());//主线程输出Thread t1 = new Thread(()->{ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance();System.out.println(Thread.currentThread().getName() + ":" + singleton);});t1.start();}
}

请在此添加图片描述

从上图可以看到,main 线程输出的和 t1 线程输出的并不是同一个对象,故而 ThreadLocal 式示例仅对单线程是安全的。

10. 枚举式单例

枚举式单例充分利用了枚举类的特性来创建单例对象,目前来说这是最优雅的一种写法。

照例我们新建一个空的对象 MyObject.java 来测试单例。

package singleton.meiju;public class MyObject {
}
public class EnumSingleton {INSTANCE;private MyObject myObject;EnumSingleton(){this.myObject = new MyObject();}public Object getData() {return myObject;}public static EnumSingleton getInstance(){return INSTANCE;}
}
  • 新建测试类 TestEnumSingleton.java 进行测试。
package singleton.meiju;public class TestEnumSingleton {public static void main(String[] args) throws Exception{EnumSingleton enumSingleton = EnumSingleton.getInstance();System.out.println(enumSingleton.getData() == enumSingleton.getData());//输出:true}
}

输出结果为 true,枚举式单例写法能有效的防止通过反射以及序列化手段的破坏,确实为目前最佳的单例实践之选。

小结

尽管实现单例模式的具体思想和方法多种多样,也各有千秋和不足,但在实际的使用中,并不是最优的就是最合适的,在使用单例模式时,应该结合具体的项目需求以及场景来选择合适的实现方式。比如小项目追求线程安全又拥有足够空间的情况下使用饿汉式单例又何尝不可?

致谢&引用:

  • Java Singletons Using Enum
  • Advantages and Disadvantages of using Enum as Singleton in Java
  • 百度百科

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

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

相关文章

文件操作讲解

目录 一.为什么使用文件 二.什么是文件 2.1程序文件 2.2数据文件 2.3文件名 三.文本文件和二进制文件 fwrite函数 fclose函数 四.文件的打开和关闭 4.1流和标准流 4.2文件指针 4.3文件的打开和关闭 五.文件的顺序读写 5.1文件的顺序读写函数 5.1.1fgetc函数…

【软件工程】概要设计

1. 导言 1.1 目的 该文档的目的是描述学生成绩管理系统的概要设计&#xff0c;其主要内容包括&#xff1a; 系统功能简介 系统结构简介 系统接口设计 数据设计 模块设计 界面设计 本文的预期读者是&#xff1a; 项目开发人员 项目管理人员 项目评测人员&#xff08;…

VS2022使用属性表快速设置OpenCV工程属性

1.创建C++控制台应用 2.配置工程 3.打开工程后,为工程添加属性表 打开属性管理器窗口,选择Debug|x64 然后右击选择添加新的项目属性表 并命名为opencv490_debug_x64 点击添加 Debug版本属性表添加成功 使用相同方法添加Release版本属性表

Windows通过git配置github代码仓库全流程

git git是代码的版本控制工具 git安装和github注册 这个默认弄过了 通过git和github之间的SSH配置 在github上面新建仓库&#xff0c;做好配置 git绑定GitHub账号 先cd到上传的文件所在的目录 git config --global user.name "你的github用户名"git config -…

网络原理 - HTTP / HTTPS(3)——http响应

目录 一、认识 “状态码”&#xff08;status code&#xff09; 常见的状态码 &#xff08;1&#xff09;200 OK &#xff08;2&#xff09;404 Not Found &#xff08;3&#xff09;403 ForBidden &#xff08;4&#xff09;405 Method Not Allowed &#xff08;5&…

基于java实现的弹幕视频网站

开发语言&#xff1a;Java 框架&#xff1a;ssm 技术&#xff1a;JSP JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclip…

基于STC12C5A60S2系列1T 8051单片机的带字库液晶显示器LCD12864数据传输并行模式显示自定义字符应用

基于STC12C5A60S2系列1T 8051单片机的带字库液晶显示器LCD12864显示自定义字符应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍液晶显示器LCD12864简单介绍一、LCD…

使用ffmpeg将视频解码为帧时,图像质量很差

当使用ffmpeg库自带的ffmpeg.exe对对视频进行解帧或合并时&#xff0c;结果质量很差。导致这种原因的是在使用ffmpeg.exe指令进行解帧或合并时使用的是默认的视频码率&#xff1a;200kb/s。 如解帧指令&#xff1a; ffmpeg.exe -i 600600pixels.avi -r 2 -f image2 img/%03d.…

深度学习arm cache系列--一篇就够了

快速链接: 【精选】ARMv8/ARMv9架构入门到精通-[目录] &#x1f448;&#x1f448;&#x1f448; 1. cache的基本概念介绍 1.1、为什么要用cache? ARM 架构刚开始开发时&#xff0c;处理器的时钟速度和内存的访问速度大致相似。今天的处理器内核要复杂得多&#xff0c;并且时…

【基于HTML5的网页设计及应用】——-正则表达式.

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

ctf_show笔记篇(web入门---SSRF)

ssrf简介 ssrf产生原理&#xff1a; 服务端存在网络请求功能/函数&#xff0c;例如&#xff1a;file_get_contens()这一类类似于curl这种函数传入的参数用户是可控的没有对用户输入做过滤导致的ssrf漏洞 ssrf利用: 用于探测内网服务以及端口探针存活主机以及开放服务探针是否存…

C语言交换二进制位的奇数偶数位

基本思路 我们要先把想要交换的数的二进制位给写出来假如交换13的二进制位&#xff0c;13的二进制位是 0000 0000 0000 0000 0000 0000 0000 1101然后写出偶数位的二进制数&#xff08;偶数位是1的&#xff09; 1010 1010 1010 1010 1010 1010 1010 1010然后写出奇数位的二进…

uniapp切换中英文

一、安装 npm install uni-i18n --save 二、创建中英文切换的文件 1.英文en.js文件 2.中文zh_CN.js文件 三、 main.js中引用 // Vue i18n 国际化 import VueI18n from /common/vue-i18n.min.js; Vue.use(VueI18n);// i18n 部分的配置&#xff0c;引入语言包&#xff0c;注意路…

Linux :进程的程序替换

目录 一、什么是程序替换 1.1程序替换的原理 1.2更改为多进程版本 二、各种exe接口 2.2execlp ​编辑 2.2execv 2.3execle、execve、execvpe 一、什么是程序替换 1.1程序替换的原理 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往…

0基础安装配置Linux-ubuntu环境

Vmtools的安装参见 0基础教你安装VM 17PRO-直接就是专业许可证版_vm17许可证-CSDN博客 在vmtools中安装ubuntu 等待安装 这时候发现没有继续按钮&#xff0c;我们关闭这个界面&#xff0c;进入系统中&#xff0c;先更改分辨率 点击这个三角&#xff0c;因为还么有安装成功&am…

【Canavs与艺术】绘制蓝白绶带大卫之星勋章

【图例】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>用Canvas绘制蓝白绶带大卫之星勋章</title><style type&quo…

从 MongoDB 到 PostgreSQL 的大迁移

Infisical&#xff0c;一家做密钥管理的开源商业公司&#xff0c;主要对标的是 HashiCorp Vault Infisical 在过去一年里迅速发展&#xff0c;平台现在每天处理超过 5000 万个密钥&#xff0c;将应用程序配置和私密数据发送给需要的团队、CI/CD 流水线以及服务器 / 应用程序。 …

基于JSP的农产品供销服务系统

背景 互联网的迅猛扩张彻底革新了全球各类组织的运营模式。自20世纪90年代起&#xff0c;中国的政府机关和各类企业便开始探索利用网络系统来处理管理事务。然而&#xff0c;早期的网络覆盖范围有限、用户接受度不高、互联网相关法律法规不完善以及技术开发不够成熟等因素&…

Python如何解决“滑动拼图”验证码(8)

前言 本文是该专栏的第67篇,后面会持续分享python爬虫干货知识,记得关注。 做过爬虫项目的同学,或多或少都会接触到一些需要解决验证码才能正常获取数据的平台。 在本专栏之前的文章中,笔者有详细介绍通过python来解决多种“验证码”(点选验证,图文验证,滑块验证,滑块…

汽车EDI:如何与奔驰建立EDI连接?

梅赛德斯-奔驰是世界闻名的豪华汽车品牌&#xff0c;无论是技术实力还是历史底蕴都在全球汽车主机厂中居于领先位置。奔驰拥有多种车型&#xff0c;多元化的产品布局不仅满足了不同用户画像的需求&#xff0c;也对其供应链体系有着极大的考验。 本文将为大家介绍梅赛德斯-奔驰乘…