提供线程内的局部变量,不同的线程之间不会互相干扰,只在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度特点,应用了弱引用
线程并发,多线程并发的场景下使用传递数据,通过ThreadLocal在同一线程下,不同组件中传递线程隔离,每个线程变量都是独立的,不会互相影响
一个线程往ThreadLocal放,另一个线程取不到,有隔离特点
static ThreadLocal tl = new ThreadLocal<>(); public static void main(String[] args) { new Thread(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } //null System.out.println(tl.get()); }).start(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } tl.set(new Person()); //com.java.threadlocal.Person@4ebf4ac7 System.out.println(tl.get()); }).start(); }
一个Thread有一个ThreadLocalMap一个ThreadLocalMap包含多个Entry对一个Entry对为一个ThreadLocal对象和value构成一个ThreadLocal可以作为多个Thread的ThreadLocalMap的key一个Thread只能通过自己的ThreadLocalMap,根据ThreadLocal获取对应的value
JDK8后,这种设计方式每个ThreadLocalMap存储的键值对少,每个Thread维护自己的ThreadLocalMap,一个ThreadLocalMap的键值对数量由ThreadLocal决定,而实际开发中,并不是很多,避免哈希冲突
JDK8之间,由ThreadLocal维护一个Map,Thread-value作为键值对,个数由线程决定
当Thread销毁之后,ThreadLocalMap也会随之销毁,减少内存使用 和sychronized区别
共同点,都能用于处理多线程并发访问变量的问题sychronized时间换空间,只提供一份变量,让不同线程排队访问,侧重点在于多个线程访问资源的同步ThreadLocal空间换时间,为每个线程都提供一个线程独享的变量,实现同时访问而不互相干扰,侧重点在于每个线程之间的数据隔离 spring事务中的应用
保证所有操作都在一个事务中,每个操作使用的连接都必须是同一个
数据层和服务层的connection是同一个
线程并发的情况下,每个线程只能操作各自的connection普通解决方案,需要将连接作为参数传入,并且要用synchronized保证线程安全
增加代码耦合度,影响性能
@Transactional最终调用DataSourceTransactionManager,利用ThreadLocal传递connectiondoBegin首先检查是否有连接对象,没有则获取一个,并且会设置给newConnectionHolder
doBegin会检查是否是新的连接,如果是将新连接通过TransactionSynchronizationManager与ThreadLocalMap绑定
设置给resources,以map类型存储,key是数据源,value为连接,说明一个线程,对应的一个数据源,对应一个连接
resources实际上就是个ThreadLocal,里面的元素类型为Map 在MyBatis的应用 关于分页PageHelper,会根据当前数据库连接,选择合适的分页方式 PageHelper.startPage(2, 1); List accounts = accountMapper.findAll(); for (Account account : accounts) { System.out.println(account); startPage public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); //当执行过orderBy的时候 Page oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByonly()) { page.setOrderBy(oldPage.getOrderBy()); } //关键 setLocalPage(page); return page; } setLocalPage,给ThreadLocalMap设置Page对象 protected static void setLocalPage(Page page) { LOCAL_PAGE.set(page); } PageInterceptor拦截器中,调用intercept,完成所有操作调用afterAll afterAll,将当前线程对应的dialect和page清理,remove掉 public void afterAll() { AbstractHelperDialect delegate = this.autoDialect.getDelegate(); if (delegate != null) { delegate.afterAll(); this.autoDialect.clearDelegate(); } clearPage(); } getDelegate实际上也是从ThreadLocal获取当前线程的AbstractHelperDialect,应用了代理模式,最终由PageHelper来增强删除 public AbstractHelperDialect getDelegate() { return this.delegate != null ? this.delegate : (AbstractHelperDialect)this.dialectThreadLocal.get(); } clearPage调用remove public static void clearPage() { LOCAL_PAGE.remove(); } Set 主要工作 设置值,如果没有ThradLocalMap就为其创建在实际设置的过程中,如果找到k相等的,就替换;如果找到k==null,就进行一次清理工作,并在清理同时,如果找到k相等的,同样替换,如果没有相等的,就放找到为null的地方 获取到当前线程,放value是放入当前线程对于的map里,map的key为当前ThreadLocal对象 public void set(T value) { //当前线程 Thread t = Thread.currentThread(); //获取map ThreadLocalMap map = getMap(t); if (map != null) //key - 当前ThreadLocal的实例对象 map.set(this, value); else //没有则创建 createMap(t, value); } getMap对应的为Thread的成员变量threadLocals,每出现一个线程,就会初始化一个ThreadLocalMap类型的threadLocals,专属于的该线程的map ThreadLocalMap getMap(Thread t) { return t.threadLocals; } createMap 初始化当前线程对应的ThreadLocalMap void createMap(Thread t, T firstValue) { //当前ThreadLocal的实例对象作为key t.threadLocals = new ThreadLocalMap(this, firstValue); } ThreadLocalMap构造 ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) { //table 为 Entry(继承了WeakReference)数组 INITIAL_CAPACITY 默认 16 //容量必须是2的整数次幂 table = new Entry[INITIAL_CAPACITY]; //线性探测法,找到一个下标 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; //设置扩容阈值,当大于它时,需要扩容 setThreshold(INITIAL_CAPACITY); } set,实际进行set的操作,在当前线程对应的map中遍历 private void set(ThreadLocal> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //遍历所有的Entry 线性探测法 for (Entry e = tab[i]; e != null; //实际上是循环遍历 i + 1 < len ? i + 1 : 0 e = tab[i = nextIndex(i, len)]) { ThreadLocal> k = e.get(); if (k == key) { //相等则替换 不相等,就找下一个,此时hash冲突了 e.value = value; return; } if (k == null) { //发现一个为null的key //1.第一次遍历做一次整体的清理,并保存第一个为null的地方,防止后续突然增加大量数据 //2.第二次遍历找跟当前key是否有相等的,有或没有都放到i的位置,原先的位置置null //3.清理所有entry指向null的下标 replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; //清理为null的元素 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } get 主要工作 获取ThreadLocal实例对象对应的值,如果没有就返回null如果当前Thread没有ThreadLocalMap为其创建,并将ThreadLocal-null加入搜索时,第一次尝试直接命中,如果找不到,尝试遍历搜索,同时清理k == null的Entry get,范型写法 public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { //根据ThreadLocal实例对象获得Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { //抑制没被使用的警告 @SuppressWarnings(“unchecked”) //强转 T result = (T)e.value; return result; } } //当前线程没有对应的map 或者 没有找到当前key对应的value 返回null return setInitialValue(); } getEntry private Entry getEntry(ThreadLocal> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; //看看能不能正好找到 if (e != null && e.get() == key) return e; else //找不到就遍历 return getEntryAfterMiss(key, i, e); } getEntryAfterMiss,遍历搜索,在遍历的同时,清除为null的Entry private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { //没找到 返回null ThreadLocal> k = e.get(); if (k == key) return e; if (k == null) //找到为null的,直接清除 expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; } setInitialValue,为其创建一个map private T setInitialValue() { //返回null T value = initialValue(); Thread t = Thread.currentThread(); //一样的,返回ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) //有map设置当前key map.set(this, value); else //否则为其创建一个 createMap(t, value); //实际上就是null return value; } initialValue,实际上仅是返回null,可以继承ThreadLocal重写此方法,自定义返回初始值 不了解的可以看这篇博客:https://www.cnblogs.com/pxza/ protected T initialValue() { return null; } remove remove,存在map,找到key删除 public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } ThreadLocalMap的remove,遍历map进行删除 private void remove(ThreadLocal> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); //清理这个Entry的同时,做一次整体清理 expungeStaleEntry(i); return; } } } Entry Entry继承WeakReference,实际上是指向ThreadLocal实例对象的虚引用 static class Entry extends WeakReference> { Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } } 为什么使用弱引用 在set/get方法中,会对k为null进行判断并清理如果使用强引用,当不需要使用当前ThreadLocal,将当前ThreadLocal的实例对象置为空即可被回收,但是在ThreadLocalMap中的Entry仍然指向当前ThreadLocal,无法被回收,会产生内存泄漏,只有ThreadMap能被回收,才能回收如果是弱引用,将ThreadLocal的生命周期和Thread解绑,只需要把当前外部使用的ThreadLocal的实例对象置为空即可,内部Entry指向的为弱引用,只要GC就会被回收,但是Entry中的value仍然存在,被Entry对象指向,无法被回收,也会产生内存泄漏 在不使用当前Entry时,需要tl.remove();,调用get/set中仍然会remove,但是存在长时间不调用get/set的情况 当线程来自于线程池,在归还线程的时候,ThreadLocalMap没有被清理掉,会影响下次使用,并导致空间越来越大 内存泄漏 真实原因跟Entry是否是弱引用没有关系,根源是使用完ThreadLocal没有及时remove,导致Map越来越大 没有手动删除Entry线程一直存在,ThreadLocalMap生命周期跟Thread一样 使用弱引用,避免ThreadLocalMap中仍指向ThreadLocal无法被回收使用完毕后,要及时remove,防止Entry指向的value不能被及时回收 扩容 setThreshold,初始化为2/3 private void setThreshold(int len) { threshold = len * 2 / 3; } rehash,先清理 private void rehash() { //清掉空的位置 expungeStaleEntries(); //如果清空后,仍然大于 3/4,扩容 if (size >= threshold - threshold / 4) resize(); } resize真正扩容,复制一份,扩容两倍 private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; //两倍 int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; for (int j = 0; j < oldLen; ++j) { //复制一份 Entry e = oldTab[j]; if (e != null) { ThreadLocal> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; } 哈希冲突 由于ThreadLocalMap本身就需要不断的进行整体遍历remove,可以结合开放地址法,解决哈希冲突 解决哈希冲突的核心 https://www.cnblogs.com/pxza/ int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 相关部分 private final int threadLocalHashCode = nextHashCode(); //Integer的原子操作,静态方法,从0开始,所以每个实例对象的哈希值都是不同的 private static AtomicInteger nextHashCode = new AtomicInteger(); //魔数,跟斐波那契数列有关,主要为了让哈希码能够均匀的分布在2的n次方数组内,不容易堆积在一起 private static final int HASH_INCREMENT = 0x61c88647; // private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } 相当于取模运算,hashcode % size & (INITIAL_CAPACITY - 1) 线性探测法 nextIndex ((i + 1 < len) ? i + 1 : 0); 一次探测下一个地址,知道有空的地址后插入,若整个空间都找不到空的地址会溢出如果当前长度为16,计算出来的i为14,此时tab[14]上有值,且key不相等,发生了hash冲突,那就+1找下一个,循环遍历 总结 写到这里也结束了,在文章最后放上一个小小的福利,以下为小编自己在学习过程中整理出的一个关于 java开发 的学习思路及方向。从事互联网开发,最主要的是要学好技术,而学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯,更加需要准确的学习方向达到有效的学习效果。 由于内容较多就只放上一个大概的大纲,需要更及详细的学习思维导图的 点击我的Gitee获取。 还有 高级java全套视频教程 java进阶架构师 视频+资料+代码+面试题! 全方面的java进阶实践技术资料,并且还有技术大牛一起讨论交流解决问题。
关于分页PageHelper,会根据当前数据库连接,选择合适的分页方式
PageHelper.startPage(2, 1); List accounts = accountMapper.findAll(); for (Account account : accounts) { System.out.println(account);
startPage
public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); //当执行过orderBy的时候 Page oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByonly()) { page.setOrderBy(oldPage.getOrderBy()); } //关键 setLocalPage(page); return page; }
setLocalPage,给ThreadLocalMap设置Page对象
protected static void setLocalPage(Page page) { LOCAL_PAGE.set(page); }
PageInterceptor拦截器中,调用intercept,完成所有操作调用afterAll
afterAll,将当前线程对应的dialect和page清理,remove掉
public void afterAll() { AbstractHelperDialect delegate = this.autoDialect.getDelegate(); if (delegate != null) { delegate.afterAll(); this.autoDialect.clearDelegate(); }
clearPage();
}
getDelegate实际上也是从ThreadLocal获取当前线程的AbstractHelperDialect,应用了代理模式,最终由PageHelper来增强删除
public AbstractHelperDialect getDelegate() { return this.delegate != null ? this.delegate : (AbstractHelperDialect)this.dialectThreadLocal.get(); }
clearPage调用remove
public static void clearPage() { LOCAL_PAGE.remove(); }
主要工作
设置值,如果没有ThradLocalMap就为其创建在实际设置的过程中,如果找到k相等的,就替换;如果找到k==null,就进行一次清理工作,并在清理同时,如果找到k相等的,同样替换,如果没有相等的,就放找到为null的地方
获取到当前线程,放value是放入当前线程对于的map里,map的key为当前ThreadLocal对象
public void set(T value) { //当前线程 Thread t = Thread.currentThread(); //获取map ThreadLocalMap map = getMap(t); if (map != null) //key - 当前ThreadLocal的实例对象 map.set(this, value); else //没有则创建 createMap(t, value); }
getMap对应的为Thread的成员变量threadLocals,每出现一个线程,就会初始化一个ThreadLocalMap类型的threadLocals,专属于的该线程的map
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
createMap 初始化当前线程对应的ThreadLocalMap
void createMap(Thread t, T firstValue) { //当前ThreadLocal的实例对象作为key t.threadLocals = new ThreadLocalMap(this, firstValue); }
ThreadLocalMap构造
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) { //table 为 Entry(继承了WeakReference)数组 INITIAL_CAPACITY 默认 16 //容量必须是2的整数次幂 table = new Entry[INITIAL_CAPACITY]; //线性探测法,找到一个下标 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; //设置扩容阈值,当大于它时,需要扩容 setThreshold(INITIAL_CAPACITY); }
set,实际进行set的操作,在当前线程对应的map中遍历
private void set(ThreadLocal> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //遍历所有的Entry 线性探测法 for (Entry e = tab[i]; e != null; //实际上是循环遍历 i + 1 < len ? i + 1 : 0 e = tab[i = nextIndex(i, len)]) { ThreadLocal> k = e.get(); if (k == key) { //相等则替换 不相等,就找下一个,此时hash冲突了 e.value = value; return; } if (k == null) { //发现一个为null的key //1.第一次遍历做一次整体的清理,并保存第一个为null的地方,防止后续突然增加大量数据 //2.第二次遍历找跟当前key是否有相等的,有或没有都放到i的位置,原先的位置置null //3.清理所有entry指向null的下标 replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; //清理为null的元素 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
获取ThreadLocal实例对象对应的值,如果没有就返回null如果当前Thread没有ThreadLocalMap为其创建,并将ThreadLocal-null加入搜索时,第一次尝试直接命中,如果找不到,尝试遍历搜索,同时清理k == null的Entry
get,范型写法
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { //根据ThreadLocal实例对象获得Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { //抑制没被使用的警告 @SuppressWarnings(“unchecked”) //强转 T result = (T)e.value; return result; } } //当前线程没有对应的map 或者 没有找到当前key对应的value 返回null return setInitialValue(); }
getEntry
private Entry getEntry(ThreadLocal> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; //看看能不能正好找到 if (e != null && e.get() == key) return e; else //找不到就遍历 return getEntryAfterMiss(key, i, e); }
getEntryAfterMiss,遍历搜索,在遍历的同时,清除为null的Entry
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { //没找到 返回null ThreadLocal> k = e.get(); if (k == key) return e; if (k == null) //找到为null的,直接清除 expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
setInitialValue,为其创建一个map
private T setInitialValue() { //返回null T value = initialValue(); Thread t = Thread.currentThread(); //一样的,返回ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) //有map设置当前key map.set(this, value); else //否则为其创建一个 createMap(t, value); //实际上就是null return value; }
initialValue,实际上仅是返回null,可以继承ThreadLocal重写此方法,自定义返回初始值
不了解的可以看这篇博客:https://www.cnblogs.com/pxza/
protected T initialValue() { return null; }
remove,存在map,找到key删除
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
ThreadLocalMap的remove,遍历map进行删除
private void remove(ThreadLocal> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); //清理这个Entry的同时,做一次整体清理 expungeStaleEntry(i); return; } } }
Entry继承WeakReference,实际上是指向ThreadLocal实例对象的虚引用
static class Entry extends WeakReference> { Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } }
在set/get方法中,会对k为null进行判断并清理如果使用强引用,当不需要使用当前ThreadLocal,将当前ThreadLocal的实例对象置为空即可被回收,但是在ThreadLocalMap中的Entry仍然指向当前ThreadLocal,无法被回收,会产生内存泄漏,只有ThreadMap能被回收,才能回收如果是弱引用,将ThreadLocal的生命周期和Thread解绑,只需要把当前外部使用的ThreadLocal的实例对象置为空即可,内部Entry指向的为弱引用,只要GC就会被回收,但是Entry中的value仍然存在,被Entry对象指向,无法被回收,也会产生内存泄漏
在不使用当前Entry时,需要tl.remove();,调用get/set中仍然会remove,但是存在长时间不调用get/set的情况
当线程来自于线程池,在归还线程的时候,ThreadLocalMap没有被清理掉,会影响下次使用,并导致空间越来越大 内存泄漏
真实原因跟Entry是否是弱引用没有关系,根源是使用完ThreadLocal没有及时remove,导致Map越来越大
没有手动删除Entry线程一直存在,ThreadLocalMap生命周期跟Thread一样
使用弱引用,避免ThreadLocalMap中仍指向ThreadLocal无法被回收使用完毕后,要及时remove,防止Entry指向的value不能被及时回收 扩容
setThreshold,初始化为2/3
private void setThreshold(int len) { threshold = len * 2 / 3; }
rehash,先清理
private void rehash() { //清掉空的位置 expungeStaleEntries(); //如果清空后,仍然大于 3/4,扩容 if (size >= threshold - threshold / 4) resize(); }
resize真正扩容,复制一份,扩容两倍
private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; //两倍 int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; for (int j = 0; j < oldLen; ++j) { //复制一份 Entry e = oldTab[j]; if (e != null) { ThreadLocal> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
由于ThreadLocalMap本身就需要不断的进行整体遍历remove,可以结合开放地址法,解决哈希冲突
解决哈希冲突的核心
https://www.cnblogs.com/pxza/
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
相关部分
private final int threadLocalHashCode = nextHashCode(); //Integer的原子操作,静态方法,从0开始,所以每个实例对象的哈希值都是不同的 private static AtomicInteger nextHashCode = new AtomicInteger(); //魔数,跟斐波那契数列有关,主要为了让哈希码能够均匀的分布在2的n次方数组内,不容易堆积在一起 private static final int HASH_INCREMENT = 0x61c88647; // private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
相当于取模运算,hashcode % size
& (INITIAL_CAPACITY - 1)
线性探测法 nextIndex ((i + 1 < len) ? i + 1 : 0);
一次探测下一个地址,知道有空的地址后插入,若整个空间都找不到空的地址会溢出如果当前长度为16,计算出来的i为14,此时tab[14]上有值,且key不相等,发生了hash冲突,那就+1找下一个,循环遍历
写到这里也结束了,在文章最后放上一个小小的福利,以下为小编自己在学习过程中整理出的一个关于 java开发 的学习思路及方向。从事互联网开发,最主要的是要学好技术,而学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯,更加需要准确的学习方向达到有效的学习效果。
由于内容较多就只放上一个大概的大纲,需要更及详细的学习思维导图的 点击我的Gitee获取。 还有 高级java全套视频教程 java进阶架构师 视频+资料+代码+面试题!
全方面的java进阶实践技术资料,并且还有技术大牛一起讨论交流解决问题。
上一篇 Spring MVC参数传递
下一篇 大数据面试葵花宝典之Spark进阶
版权所有 (c)2021-2022 MSHXW.COM
ICP备案号:晋ICP备2021003244-6号