ConcurrentHashMap(应对并发问题的工具类)

并发工具类

在JDK的并发包里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。

5.1 ConcurrentHashMap

5.1.1 概述以及基本使用

在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。

基于以上两个原因我们可以使用JDK1.5以后所提供的ConcurrentHashMap。

案例1:演示HashMap线程不安全

实现步骤

  1. 创建一个HashMap集合对象
  2. 创建两个线程对象,第一个线程对象向集合中添加元素(1-24),第二个线程对象向集合中添加元素(25-50);
  3. 主线程休眠1秒,以便让其他两个线程将数据填装完毕
  4. 从集合中找出键和值不相同的数据

测试类

public class HashMapDemo01 {public static void main(String[] args) {// 创建一个HashMap集合对象HashMap<String , String> hashMap = new HashMap<String , String>() ;// 创建两个线程对象,我们本次使用匿名内部类的方式去常见线程对象Thread t1 = new Thread() {@Overridepublic void run() {// 第一个线程对象向集合中添加元素(1-24)for(int x = 1 ; x < 25 ; x++) {hashMap.put(String.valueOf(x) , String.valueOf(x)) ;}}};// 线程t2Thread t2 = new Thread() {@Overridepublic void run() {// 第二个线程对象向集合中添加元素(25-50)for(int x = 25 ; x < 51 ; x++) {hashMap.put(String.valueOf(x) , String.valueOf(x)) ;}}};// 启动线程t1.start();t2.start();System.out.println("----------------------------------------------------------");try {// 主线程休眠2s,以便让其他两个线程将数据填装完毕TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}// 从集合中找出键和值不相同的数据for(int x = 1 ; x < 51 ; x++) {// HashMap中的键就是当前循环变量的x这个数据的字符串表现形式 , 根据键找到值,然后在进行判断if( !String.valueOf(x).equals( hashMap.get(String.valueOf(x)) ) ) {System.out.println(String.valueOf(x) + ":" + hashMap.get(String.valueOf(x)));}}}}

控制台输出结果

----------------------------------------------------------
5:null

通过控制台的输出结果,我们可以看到在多线程操作HashMap的时候,可能会出现线程安全问题。

注1:需要多次运行才可以看到具体的效果; 可以使用循环将代码进行改造,以便让问题方便的暴露出来!

案例2:演示Hashtable线程安全

测试类

public class HashtableDemo01 {public static void main(String[] args) {// 创建一个Hashtable集合对象Hashtable<String , String> hashtable = new Hashtable<String , String>() ;// 创建两个线程对象,我们本次使用匿名内部类的方式去常见线程对象Thread t1 = new Thread() {@Overridepublic void run() {// 第一个线程对象向集合中添加元素(1-24)for(int x = 1 ; x < 25 ; x++) {hashtable.put(String.valueOf(x) , String.valueOf(x)) ;}}};// 线程t2Thread t2 = new Thread() {@Overridepublic void run() {// 第二个线程对象向集合中添加元素(25-50)for(int x = 25 ; x < 51 ; x++) {hashtable.put(String.valueOf(x) , String.valueOf(x)) ;}}};// 启动线程t1.start();t2.start();System.out.println("----------------------------------------------------------");try {// 主线程休眠2s,以便让其他两个线程将数据填装完毕TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}// 从集合中找出键和值不相同的数据for(int x = 1 ; x < 51 ; x++) {// Hashtable中的键就是当前循环变量的x这个数据的字符串表现形式 , 根据键找到值,然后在进行判断if( !String.valueOf(x).equals( hashtable.get(String.valueOf(x)) ) ) {System.out.println(String.valueOf(x) + ":" + hashtable.get(String.valueOf(x)));}}}}

不论该程序运行多少次,都不会产生数据问题。因此也就证明Hashtable是线程安全的。

Hashtable保证线程安全的原理

查看Hashtable的源码

public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {// Entry数组,一个Entry就相当于一个元素private transient Entry<?,?>[] table;// Entry类的定义private static class Entry<K,V> implements Map.Entry<K,V> {final int hash;		// 当前key的hash码值final K key;		// 键V value;			// 值Entry<K,V> next;	// 下一个节点}// 存储数据public synchronized V put(K key, V value){...}// 获取数据public synchronized V get(Object key){...}// 获取长度public synchronized int size(){...}...}

对应的结构如下图所示

在这里插入图片描述

Hashtable保证线程安全性的是使用方法全局锁进行实现的。在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable

的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

案例3:演示ConcurrentHashMap线程安全

测试类

public class ConcurrentHashMapDemo01 {public static void main(String[] args) {// 创建一个ConcurrentHashMap集合对象ConcurrentHashMap<String , String> concurrentHashMap = new ConcurrentHashMap<String , String>() ;// 创建两个线程对象,我们本次使用匿名内部类的方式去常见线程对象Thread t1 = new Thread() {@Overridepublic void run() {// 第一个线程对象向集合中添加元素(1-24)for(int x = 1 ; x < 25 ; x++) {concurrentHashMap.put(String.valueOf(x) , String.valueOf(x)) ;}}};// 线程t2Thread t2 = new Thread() {@Overridepublic void run() {// 第二个线程对象向集合中添加元素(25-50)for(int x = 25 ; x < 51 ; x++) {concurrentHashMap.put(String.valueOf(x) , String.valueOf(x)) ;}}};// 启动线程t1.start();t2.start();System.out.println("----------------------------------------------------------");try {// 主线程休眠2s,以便让其他两个线程将数据填装完毕TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}// 从集合中找出键和值不相同的数据for(int x = 1 ; x < 51 ; x++) {// concurrentHashMap中的键就是当前循环变量的x这个数据的字符串表现形式 , 根据键找到值,然后在进行判断if( !String.valueOf(x).equals( concurrentHashMap.get(String.valueOf(x)) ) ) {System.out.println(String.valueOf(x) + ":" + concurrentHashMap.get(String.valueOf(x)));}}}}

不论该程序运行多少次,都不会产生数据问题。因此也就证明ConcurrentHashMap是线程安全的。

5.1.2 源码分析

由于ConcurrentHashMap在jdk1.7和jdk1.8的时候实现原理不太相同,因此需要分别来讲解一下两个不同版本的实现原理。

1) jdk1.7版本

ConcurrentHashMap中的重要成员变量

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable {/*** Segment翻译中文为"段" , 段数组对象*/final Segment<K,V>[] segments;// Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色,将一个大的table分割成多个小的table进行加锁。static final class Segment<K,V> extends ReentrantLock implements Serializable {transient volatile int count;    			// Segment中元素的数量,由volatile修饰,支持内存可见性;transient int modCount;			 			// 对table的大小造成影响的操作的数量(比如put或者remove操作);transient int threshold;		 			// 扩容阈值;transient volatile HashEntry<K,V>[] table;  // 链表数组,数组中的每一个元素代表了一个链表的头部;final float loadFactor;			 			// 负载因子 }// Segment中的元素是以HashEntry的形式存放在数组中的,其结构与普通HashMap的HashEntry基本一致,不同的是Segment的HashEntry,其value由		     // volatile修饰,以支持内存可见性,即写操作对其他读线程即时可见。static final class HashEntry<K,V> {final int hash;					// 当前节点key对应的哈希码值final K key;					// 存储键volatile V value;				// 存储值volatile HashEntry<K,V> next;	// 下一个节点}}

对应的结构如下图所示

在这里插入图片描述

简单来讲,就是ConcurrentHashMap比HashMap多了一次hash过程,第1次hash定位到Segment,第2次hash定位到HashEntry,然后链表搜索找到指定节点。在进行写操作时,只需锁住写

元素所在的Segment即可(这种锁被称为分段锁),其他Segment无需加锁,从而产生锁竞争的概率大大减小,提高了并发读写的效率。该种实现方式的缺点是hash过程比普通的HashMap要长

(因为需要进行两次hash操作)。

ConcurrentHashMap的put方法源码分析

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable { public V put(K key, V value) {// 定义一个Segment对象Segment<K,V> s;// 如果value的值为空,那么抛出异常if (value == null) throw new NullPointerException();// hash函数获取key的hashCode,然后做了一些处理int hash = hash(key);// 通过key的hashCode定位segmentint j = (hash >>> segmentShift) & segmentMask;// 对定位的Segment进行判断,如果Segment为空,调用ensureSegment进行初始化操作(第一次hash定位)if ((s = (Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null) s = ensureSegment(j);// 调用Segment对象的put方法添加元素return s.put(key, hash, value, false);}// Segment是一种可ReentrantLock,在ConcurrentHashMap里扮演锁的角色,将一个大的table分割成多个小的table进行加锁。static final class Segment<K,V> extends ReentrantLock implements Serializable {// 添加元素final V put(K key, int hash, V value, boolean onlyIfAbsent) {// 尝试对该段进行加锁,如果加锁失败,则调用scanAndLockForPut方法;在该方法中就要进行再次尝试或者进行自旋等待HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);V oldValue;try {// 获取HashEntry数组对象HashEntry<K,V>[] tab = table;// 根据key的hashCode值计算索引(第二次hash定位)int index = (tab.length - 1) & hash;HashEntry<K,V> first = entryAt(tab, index);for (HashEntry<K,V> e = first;;) // 若不为nullif (e != null) {K k;// 判读当前节点的key是否和链表头节点的key相同(依赖于hashCode方法和equals方法) // 如果相同,值进行更新if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {oldValue = e.value;if (!onlyIfAbsent) {e.value = value;++modCount;}break;}e = e.next;} else {  // 若头结点为null// 将新节点添加到链表中if (node != null) node.setNext(first);elsenode = new HashEntry<K,V>(hash, key, value, first);int c = count + 1;// 如果超过阈值,则进行rehash操作if (c > threshold && tab.length < MAXIMUM_CAPACITY)rehash(node);elsesetEntryAt(tab, index, node);++modCount;count = c;oldValue = null;break;}}} finally {unlock();}return oldValue;} 	}}

注:源代码进行简单讲解即可(核心:进行了两次哈希定位以及加锁过程)

2) jdk1.8版本

在JDK1.8中为了进一步优化ConcurrentHashMap的性能,去掉了Segment分段锁的设计。在数据结构方面,则是跟HashMap一样,使用一个哈希表table数组。(数组 + 链表 + 红黑树)

而线程安全方面是结合CAS机制 + 局部锁实现的,减低锁的粒度,提高性能。同时在HashMap的基础上,对哈希表table数组和链表节点的value,next指针等使用volatile来修饰,从而

实现线程可见性。

ConcurrentHashMap中的重要成员变量

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {// Node数组transient volatile Node<K,V>[] table;// Node类的定义static class Node<K,V> implements Map.Entry<K,V> { final int hash;				// 当前key的hashCode值final K key;				// 键volatile V val;				// 值volatile Node<K,V> next;	// 下一个节点}// TreeNode类的定义static final class TreeNode<K,V> extends Node<K,V> {TreeNode<K,V> parent;  // 父节点TreeNode<K,V> left;	   // 左子节点TreeNode<K,V> right;   // 右子节点TreeNode<K,V> prev;    // needed to unlink next upon deletionboolean red;		   // 节点的颜色状态}}

对应的结构如下图

在这里插入图片描述

ConcurrentHashMap的put方法源码分析

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {// 添加元素public V put(K key, V value) {return putVal(key, value, false);}// putVal方法定义final V putVal(K key, V value, boolean onlyIfAbsent) {// key为null直接抛出异常if (key == null || value == null) throw new NullPointerException();// 计算key所对应的hashCode值int hash = spread(key.hashCode());int binCount = 0;for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;// 哈希表如果不存在,那么此时初始化哈希表if (tab == null || (n = tab.length) == 0)tab = initTable();// 通过hash值计算key在table表中的索引,将其值赋值给变量i,然后根据索引找到对应的Node,如果Node为null,做出处理else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// 新增链表头结点,cas方式添加到哈希表tableif (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break;                   }else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);else {V oldVal = null;// f为链表头结点,使用synchronized加锁synchronized (f) {if (tabAt(tab, i) == f) {if (fh >= 0) {binCount = 1;for (Node<K,V> e = f;; ++binCount) {K ek;// 节点已经存在,更新value即可if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {oldVal = e.val;if (!onlyIfAbsent)e.val = value;break;}// 该key对应的节点不存在,则新增节点并添加到该链表的末尾Node<K,V> pred = e;if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key, value, null);break;}}} else if (f instanceof TreeBin) { // 红黑树节点,则往该红黑树更新或添加该节点即可Node<K,V> p;binCount = 2;if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}// 判断是否需要将链表转为红黑树if (binCount != 0) {if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}addCount(1L, binCount);return null;}// CAS算法的核心类private static final sun.misc.Unsafe U;static {try {U = sun.misc.Unsafe.getUnsafe();...} catch (Exception e) {throw new Error(e);}}// 原子获取链表节点static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);}// CAS更新或新增链表节点static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);}}

简单总结:

  1. 如果当前需要put的key对应的链表在哈希表table中还不存在,即还没添加过该key的hash值对应的链表,则调用casTabAt方法,基于CAS机制来实现添加该链表头结点到哈希表

    table中,避免该线程在添加该链表头结的时候,其他线程也在添加的并发问题;如果CAS失败,则进行自旋,通过继续第2步的操作;

  2. 如果需要添加的链表已经存在哈希表table中,则通过tabAt方法,基于volatile机制,获取当前最新的链表头结点f,由于f指向的是ConcurrentHashMap的哈希表table的某条

    链表的头结点,故虽然f是临时变量,由于是引用共享的该链表头结点,所以可以使用synchronized关键字来同步多个线程对该链表的访问。在synchronized(f)同步块里面则是与

    HashMap一样遍历该链表,如果该key对应的链表节点已经存在,则更新,否则在链表的末尾新增该key对应的链表节点。

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

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

相关文章

可一件转化的视频生成模型:快手官方大模型“可灵”重磅来袭!

可一件转化的视频生成模型“可灵”重磅来袭&#xff01; 前言 戴墨镜的蒙娜丽莎 达芬奇的画作《蒙娜丽莎的微笑》相信大家是在熟悉不过了&#xff0c;可《戴墨镜的蒙娜丽莎》大家是不是第一次见&#xff1f;而且这还不是以照片的形式&#xff0c;而是以视频的形式展示给大家。 …

Spring AOP实战--之优雅的统一打印web请求的出参和入参

背景介绍 由于实际项目内网开发&#xff0c;项目保密&#xff0c;因此本文以笔者自己搭建的demo做演示&#xff0c;方便大家理解。 在项目开发过程中&#xff0c;团队成员为了方便调试&#xff0c;经常会在方法的出口和入口处加上log输出&#xff0c;由于每个人的log需求和输…

奔驰EQS SUV升级原厂主动式氛围灯效果展示

以下是一篇关于奔驰 EQs 升级原厂主动氛围灯案例的宣传文案&#xff1a; 在汽车科技不断演进的今天&#xff0c;我们自豪地为您呈现奔驰 EQs 升级原厂主动氛围灯的精彩案例。 奔驰 EQs&#xff0c;作为豪华电动汽车的典范&#xff0c;其卓越品质与高端性能有目共睹。而此次升…

CVPR 2024盛况空前,上海科技大学夺得最佳学生论文奖,惊艳全场

CVPR 2024盛况空前&#xff01;上海科技大学夺得最佳学生论文奖&#xff0c;惊艳全场&#xff01; 会议之眼 快讯 2024 年 CVPR &#xff08;Computer Vision and Pattern Recogntion Conference) 即国际计算机视觉与模式识别会议&#xff0c;于6月17日至21日正在美国西雅图召…

手把手教你java CPU飙升300%如何优化

背景 今天有个项目运行一段时间后&#xff0c;cpu老是不堪负载。 排查 top 命令 TOP 命令 top t 按cpu 排序 top m 按内存使用率排序 从上面看很快看出是 pid 4338 这个进程资源消耗很高。 top -Hp pid top -Hp 4338 找到对应线程消耗的资源shftp cpu占用进行排序&#xf…

【Java】已解决java.net.ProtocolException异常

文章目录 一、分析问题背景二、可能出错的原因三、错误代码示例四、正确代码示例五、注意事项 已解决java.net.ProtocolException异常 在Java的网络编程中&#xff0c;java.net.ProtocolException异常通常表示在网络通信过程中&#xff0c;客户端或服务器违反了某种协议规则。…

计算机组成原理 | 计算机系统概述

CPI:(Clockcycle Per Instruction)&#xff0c;指每条指令的时钟周期数。 时钟周期&#xff1a;对CPU来说&#xff0c;在一个时钟周期内&#xff0c;CPU仅完成一个最基本的动作。时钟脉冲是计算机的基本工作脉冲&#xff0c;控制着计算机的工作节奏。时钟周期 是一个时钟脉冲所…

除了百度,还有哪些搜索引擎工具可以使用

搜索引擎成是我们获取知识和信息不可或缺的工具。百度作为国内最大的搜索引擎&#xff0c;全球最大的中文搜索引擎&#xff0c;是许多人的首选。那么除了百度&#xff0c;还有哪些搜索引擎可以使用呢&#xff1f;小编就来和大家分享国内可以使用的其他搜索工具。 1. AI搜索 AI…

梯度提升决策树(GBDT)的训练过程

以下通过案例&#xff08;根据行为习惯预测年龄&#xff09;帮助我们深入理解梯度提升决策树&#xff08;GBDT&#xff09;的训练过程 假设训练集有4个人&#xff08;A、B、C、D&#xff09;&#xff0c;他们的年龄分别是14、16、24、26。其中A、B分别是高一和高三学生&#x…

大模型时代,新手和程序员如何转型入局AI行业?

在近期的全国两会上&#xff0c;“人工智能”再次被提及&#xff0c;并成为国家战略的焦点。这一举措预示着在接下来的十年到十五年里&#xff0c;人工智能将获得巨大的发展红利。技术革命正在从“互联网”向“人工智能”逐步迈进&#xff0c;我将迎来新一轮技术革新和人才需求…

ASP.NET Core 6.0 启动方式

启动方式 Visualstudio 2022启动 IIS Express IIS Express 是一个专为开发人员优化的轻型独立版本的 IIS。 借助 IIS Express,可以轻松地使用最新版本的 IIS 开发和测试网站。 控制台版面 直接在浏览器输入监听的地址,监听的是 http://localhost:5137 脚本启动 dotnet run…

C++11 右值引用和移动语义

目录 1.左值引用和右值引用 2.右值引用使用场景&#xff08;移动语义&#xff09;和意义 3.右值引用引用左值及其一些更深入的使用场景分析 4.完美转发 1.左值引用和右值引用 传统的C语法中就有引用的语法&#xff0c;而C11中新增了的右值引用语法特性&#xff0c;所以从现…

Verilog:【8】基于FPGA实现SD NAND FLASH的SPI协议读写

在此介绍的是使用FPGA实现SD NAND FLASH的读写操作&#xff0c;以雷龙发展提供的CS创世SD NAND FLASH样品为例&#xff0c;分别讲解电路连接、读写时序与仿真和实验结果。 目录 1 视频讲解 2 SD NAND FLASH背景介绍 3 样品申请 4 电路结构与接口协议 4.1 SD NAND 4.2 SD NAND测…

机器学习算法的电影推荐系统以及票房预测系统

一、实验概述 1. 实验目标 本项目希望基于电影数据集&#xff0c;依据电影的简介、关键词、预算、票房、用户评分等特征来对电影进行分析&#xff0c;并完成以下任务&#xff1a; 对电影特征的可视化分析对电影票房的预测多功能个性化的电影推荐算法 2. 数据集 针对票房预…

AIGC-CVPR2024best paper-Rich Human Feedback for Text-to-Image Generation-论文精读

Rich Human Feedback for Text-to-Image Generation斩获CVPR2024最佳论文&#xff01;受大模型中的RLHF技术启发&#xff0c;团队用人类反馈来改进Stable Diffusion等文生图模型。这项研究来自UCSD、谷歌等。 在本文中&#xff0c;作者通过标记不可信或与文本不对齐的图像区域&…

vulnhub靶场之FunBox-11

一.环境搭建 1.靶场描述 As always, its a very easy box for beginners. Add to your /etc/hosts: funbox11 This works better with VirtualBox rather than VMware. 2.靶场下载 https://www.vulnhub.com/entry/funbox-scriptkiddie,725/ 3.靶场启动 二.信息收集 1.寻找靶…

通过腾讯云TDSQL TCPTCE(MySQL版)认证考试秘籍宝典

腾讯云TDSQL(MySQL版)交付运维高级工程师TCCP证书展示 腾讯云TDSQL(MySQL版)交付运维专家TCCE考试成绩、证书展示 认证类型与级别 TCCA:入门级(初级) TCCP:高级(中级) TCCE:专家级(高级) 考试形式 考试是在线考试&#xff0c;考生需要在腾讯云大学官网上完成。 腾讯云TDSQ…

LabVIEW项目中的常见电机及其特点分析

在LabVIEW项目中&#xff0c;电机的选择对系统的性能和应用效果至关重要。常见电机类型包括直流电机&#xff08;DC Motor&#xff09;、步进电机&#xff08;Stepper Motor&#xff09;、交流感应电机&#xff08;AC Induction Motor&#xff09;和无刷直流电机&#xff08;BL…

mongosh常用命令详解及如何开启MongoDB身份验证

目录 Mongosh常用命令介绍 连接到MongoDB实例 基本命令 查看当前数据库 切换数据库 查看所有数据库 查看当前数据库中的集合 CRUD操作 插入文档 查询文档 更新文档 删除文档 替换文档 索引操作 创建索引 查看索引 删除索引 聚合操作 数据库管理 创建用户 …

手把手教你软著申请(带视频+包括所有模板附赠软著申请软件)

基于前面的这个软件&#xff0c;这一次我沉淀两日重新归来&#xff01; 小唐读取软件全新升级&#xff01; 现在我们开始把我们软著申请流程重新走一遍&#xff01; 要不&#xff1f;你也来申请一张软著&#xff1f; 1.中国版权保护中心注册 1.1注册 大家在这个网址处写好自…