不知道哪里的文章,总结性还是比较好的。但是代码凌乱,有的还没有图。如果找到原文了可以进行替换!

spring中的单例

spring中管理的bean实例默认情况下是单例的[sigleton类型],就还有prototype类型
按其作用域来讲有sigleton(单例),prototype(原型),request,session,global session。

spring中的单例与设计模式里面的单例略有不同,设计模式的单例是在整个应用中只有一个实例,而spring中的单例是在一个IoC容器中就只有一个实例。

spring中的单例也不会影响应用的并发访问,【不会出现各个线程之间的等待问题,或是死锁问题】。

大多数时候客户端都在访问我们应用中的业务对象,为了减少并发控制,在这个时候我们不应该在业务对象中设置那些容易造成出错的成员变量(成员变量的解决方式:1、方法的参数局部变量(相当于new),2、threadlocal/2、设置bean的scope=prototype),在并发访问时候这些成员变量将会是并发线程中的共享对象,那么这个时候就会出现意外情况。

可以参考博客Spring Bean Scope 有状态的Bean 无状态的Bean遇到的情况

引出两个问题(spring中并发所要考虑的)

先进行总结:

我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。
那么对于有状态的bean呢?Spring对一些(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态的bean采用ThreadLocal进行处理,让它们也成为线程安全的状态,因此有状态的Bean就可以在多线程中共享了。

如果用有状态的bean,也可以使用用prototype模式,每次在注入的时候就重新创建一个bean,在多线程中互不影响。

下面是分析的过程————

1、我们的Eic-server的所有的业务对象中的成员变量如,在Dao中的xxxDao,或controller中的xxxService,都会被多个线程共享,那么这些对象不会出现同步问题吗,比如会造成数据库的插入,更新异常?

2、我们的实体bean,从客户端传递到后台的controller-->service-->Dao,这一个流程中,他们这些对象都是单例的,那么这些单例的对象在处理我们的传递到后台的实体bean不会出问题吗?(实体bean在多线程中的解决方案)

答:[实体bean不是单例的],并没有交给spring来管理,每次我们都手动的New出来的【如EMakeType et = new EMakeType();】,所以即使是那些处理我们提交数据的业务处理类是被多线程共享的,但是他们处理的数据并不是共享的,数据时每一个线程都有自己的一份,所以在数据这个方面是不会出现线程同步方面的问题的。

(在这里补充下自己在项目开发中对于实体bean在多线程中的处理:1。对于实体bean一般通过方法参数的的形式传递(参数是局部变量),所以多线程之间不会有影响。2.有的地方对于有状态的bean直接使用prototype原型模式来进行解决。3.对于使用bean的地方可以通过new的方式来创建)

但是那些在Dao中的xxxDao,或controller中的xxxService,这些对象都是单例那么就不会出现线程同步的问题。话又说回来,这些对象虽然会被多个进程并发访问,可我们访问的是他们里面的方法,这些类里面通常不会含有成员变量,那个Dao里面的ibatisDao是框架里面封装好的,已经被测试,不会出现线程同步问题了。所以出问题的地方就是我们自己系统里面的业务对象,所以我们一定要注意这些业务对象里面千万不能要独立成员变量,否则会出错。

所以我们在应用中的业务对象如下例子;
controller中的成员变量List和paperService:
public class TestPaperController extends BaseController {
private static final int List = 0;
@Autowired
@Qualifier("papersService")
private TestPaperService papersService ;
public Page queryPaper(int pageSize, int page,TestPaper paper) throws EicException{
  RowSelection localRowSelection = getRowSelection(pageSize, page);
  List<TestPaper> paperList = papersService.queryPaper(paper,localRowSelection);
  Page localPage = new Page(page, localRowSelection.getTotalRows(),
    paperList);
  return localPage;
  
}

service里面的成员变量ibatisEntityDao:

@SuppressWarnings("unchecked")
@Service("papersService")
@Transactional(rollbackFor = { Exception.class })
public class TestPaperServiceImpl implements TestPaperService {
@Autowired
@Qualifier("ibatisEntityDao")
private IbatisEntityDao ibatisEntityDao;
private static final String NAMESPACE_TESTPAPER = "com.its.exam.testpaper.model.TestPaper";
private static final String BO_NAME[] = { "试卷仓库" };
private static final String BO_NAME2[] = { "试卷配置试题" };
private static final String BO_NAME1[] = { "试卷试题类型" };
private static final String NAMESPACE_TESTQUESTION="com.its.exam.testpaper.model.TestQuestion";
public List<TestPaper> queryPaper(TestPaper paper,RowSelection paramRowSelection) throws EicException{
  try {

return (List<TestPaper>) ibatisEntityDao.queryForListWithPage(
    NAMESPACE_TESTPAPER, "queryPaper", paper,paramRowSelection);
  } catch (Exception exception) {
   exception.printStackTrace();
   throw new EicException(exception, "eic", "0001", BO_NAME);
  }

}

由上面可以看出,虽然我们这个应用里面含有成员变量,但是并不会出现线程同步方面的问题,因为,controller里面的成员变量private TestPaperService papersService 之所以会成为成员变量,我们的目的是注入,将其实例化进而访问里面的方法,private static final int List = 0;是final的不会被改变。
service里面的private IbatisEntityDao ibatisEntityDao;是框架本身的线程同步问题已解决【其解决方案很有可能就是使用ThreadLocal,见下面】。

这下面的bean 一个是通过BeanFactory getBean得到,一个是业务对象testPaper.getClass()得到,通过不同客户端的浏览器访问,可得到下面结论,
springIoC容器管理的bean就是单例,因为不同的访问均得到相同的对象【在应用开启的状态下,不重新启动应用下,即在同一次的应用运行中】

-------------------------spring 中的sigleton ,这才是真正的整个应用下面就一个实例:class

com.its.exam.testpaper.service.impl.TestPaperServiceImpl$$EnhancerByCGLIB$$584b889d
-------------------------spring 中的sigleton ,这才是真正的整个应用下面就一个实例:class

com.its.exam.testpaper.service.impl.TestPaperServiceImpl$$EnhancerByCGLIB$$584b889d
-------------------------spring 中的sigleton ,这才是真正的整个应用下面就一个实例:class

com.its.exam.testpaper.service.impl.TestPaperServiceImpl$$EnhancerByCGLIB$$584b889d
-------------------------spring 中的sigleton ,这才是真正的整个应用下面就一个实例:class

com.its.exam.testpaper.service.impl.TestPaperServiceImpl$$EnhancerByCGLIB$$584b889d
spring无状态的支持
Spring框架对单例的支持是采用单例注册表的方式进行实现的,详见Spring的单例模式底层实现

spring有状态的支持

至于spring如何实现那些个有状态bean[如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder]的线程安全,即使用ThreadLocal实现的。如下原理: Spring中ThreadLocal的认识 还可以参考网上这篇文章:“浅谈Spring声明式事务管理ThreadLocal和JDKProxy”。(???????)

下面是对ThreadLocal的讲解:

当使用ThreadLocal维护变量(仅仅是变量,因为线程同步的问题就是成员变量的互斥访问出问题——根源之所在)时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

原理概念:为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。【每个线程其实是改变的是自己线程的副本,而不是真正要改变的变量,所以效果就是每个线程都有自己的,“这其实就将共享变相为人人有份!”】

虽然使用ThreadLocal会带来更多的内存开销,但这点开销是微不足道的。因为保存在ThreadLocal中的对象,通常都是比较小的对象。

基本概念:为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。

 它主要由四个方法组成initialValue(),get(),set(T),remove(),其中值得注意的是initialValue(),该方法是一个protected的方法,显然是为了子类重写而特意实现的。该方法返回当前线程在该线程局部变量的初始值,这个方法是一个延迟调用方法,在一个线程第1次调用get()或者set(Object)时才执行,并且仅执行1次。ThreadLocal中的确实实现直接返回一个null: 举例  ThreadLocal的原理

  ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。比如下面的示例实现:

public class ThreadLocal{<span style="color:#FF0000;">private Map values = Collections.synchronizedMap(new HashMap());</span>public Object get() {Thread curThread = Thread.currentThread();Object o = values.get(curThread);if (o == null && !values.containsKey(curThread)){o = initialValue();values.put(curThread, o);}return o;}public void set(Object newValue){  values.put(Thread.currentThread(), newValue);}public Object initialValue(){return null;}}

  

使用方法:ThreadLocal 的使用

  使用方法一:

  Hibernate的文档时看到了关于使ThreadLocal管理多线程访问的部分。具体代码如下

  

public static final ThreadLocal session = new ThreadLocal(); //使用ThreadLocal变量public static Session currentSession() {Session s = (Session)session.get();//open a new session,if this session has noneif(s == null){s = sessionFactory.openSession();session.set(s);}return s;}

  我们逐行分析上面的代码

  1、初始化一个ThreadLocal对象,ThreadLocal有三个成员方法 get()、set()、initialvalue()。

    2、如果不初始化initialvalue,则initialvalue返回null。

  3、session的get根据当前线程返回其对应的线程内部变量,也就是我们需要的net.sf.hibernate.Session(相当于对应每个数据库连接)。多线程情况下共享数据库链接是不安全的。ThreadLocal保证了每个线程都有自己的session(数据库连接)。

  5、如果是该线程是初次访问,自然,s(数据库连接)会是null,接着创建一个Session,具体就是行6。

  6、创建一个数据库连接实例 s

  7、保存该数据库连接s到ThreadLocal中。

  8、如果当前线程已经访问过数据库了,则从session中get()就可以获取该线程上次获取过的连接实例。

  使用方法二

   当要给线程初始化一个特殊值时,需要自己实现ThreadLocal的子类并重写该方法,通常使用一个内部匿名类对ThreadLocal进行子类化,EasyDBO中创建jdbc连接上下文就是这样做的:

  

public class JDBCContext{private static Logger logger = Logger.getLogger(JDBCContext.class);private DataSource ds;protected Connection connection;private boolean isValid = true;private static ThreadLocal jdbcContext; //ThreadLocal变量private JDBCContext(DataSource ds){this.ds = ds;createConnection();}public static JDBCContext getJdbcContext(javax.sql.DataSource ds){if(jdbcContext==null)jdbcContext=new JDBCContextThreadLocal(ds); //new的创建,看下面的自定义方法JDBCContext context = (JDBCContext) jdbcContext.get();if (context == null) {context = new JDBCContext(ds);}return context;}private static class JDBCContextThreadLocal extends ThreadLocal{public javax.sql.DataSource ds;public JDBCContextThreadLocal(javax.sql.DataSource ds){this.ds=ds;}protected synchronized Object initialValue() {return new JDBCContext(ds);}}}

  简单的实现版本

  代码清单1 SimpleThreadLocal
 

public class SimpleThreadLocal {private Map valueMap = Collections.synchronizedMap(new HashMap());public void set(Object newValue) {valueMap.put(Thread.currentThread(), newValue); //①键为线程对象,值为本线程的变量副本}public Object get() {Thread currentThread = Thread.currentThread();Object o = valueMap.get(currentThread);    //②返回本线程对应的变量if (o == null && !valueMap.containsKey(currentThread)) {//③如果在Map中不存在,放到Map中保存起来。o = initialValue();valueMap.put(currentThread, o);}return o;}public void remove() {valueMap.remove(Thread.currentThread());}public Object initialValue() {return null;}}

  虽然代码清单9‑3这个ThreadLocal实现版本显得比较幼稚,但它和JDK所提供的ThreadLocal类在实现思路上是相近的。


ThreadLocal和synchronized之间的区别:

在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。但在有些情况下,synchronized不能保证多线程对共享变量的正确读写。例如类有一个类变量,该类变量会被多个类方法读写,当多线程操作该类的实例对象时,如果线程对类变量有读取、写入操作就会发生类变量读写错误,即便是在类方法前加上synchronized也无效,因为同一个线程在两次调用方法之间时锁是被释放的,这时其它线程可以访问对象的类方法,读取或修改类变量。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。

  下面举例说明:
  

public class QuerySvc {private String sql;private static ThreadLocal sqlHolder = new ThreadLocal();public QuerySvc() {}public void execute() {System.out.println("Thread " + Thread.currentThread().getId() +" Sql is " + sql);System.out.println("Thread " + Thread.currentThread().getId() +" Thread Local variable Sql is " + sqlHolder.get());}public String getSql() {return sql;}public void setSql(String sql) {this.sql = sql;sqlHolder.set(sql);}}

  为了说明多线程访问对于类变量和ThreadLocal变量的影响,QuerySvc中分别设置了类变量sql和ThreadLocal变量,使用时先创建 QuerySvc的一个实例对象,然后产生多个线程,分别设置不同的sql实例对象,然后再调用execute方法,读取sql的值,看是否是set方法中写入的值。这种场景类似web应用中多个请求线程携带不同查询条件对一个servlet实例的访问,然后servlet调用业务对象,并传入不同查询条件,最后要保证每个请求得到的结果是对应的查询条件的结果。

  使用QuerySvc的工作线程如下:

  public class Work extends Thread {

  private QuerySvc querySvc;

  private String sql;

  public Work(QuerySvc querySvc,String sql) {

  this.querySvc = querySvc;

  this.sql = sql;

  }

  public void run() {

  querySvc.setSql(sql);

  querySvc.execute();

  }

  }

  运行线程代码如下:

  QuerySvc qs = new QuerySvc();

  for (int k=0; k<10>

  String sql = "Select * from table where id =" + k;

  new Work(qs,sql).start();

  }

  先创建一个QuerySvc实例对象,然后创建若干线程来调用QuerySvc的set和execute方法,每个线程传入的sql都不一样,从运行结果可以看出sql变量中值不能保证在execute中值和set设置的值一样,在 web应用中就表现为一个用户查询的结果不是自己的查询条件返回的结果,而是另一个用户查询条件的结果;而ThreadLocal中的值总是和set中设置的值一样,这样通过使用ThreadLocal获得了线程安全性。

总结比较:

  如果一个对象要被多个线程访问,而该对象存在类变量被不同类方法读写,为获得线程安全,可以用ThreadLocal来替代类变量。

同步机制的比较  ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

  在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

  而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

  由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal版本。

  概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

  Spring使用ThreadLocal解决线程安全问题

  我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。(贼重要)

  一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9‑2所示:

  这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。

  下面的实例能够体现Spring对有状态Bean的改造思路:

  代码清单3 TopicDao:非线程安全

  public class TopicDao {

  private Connection conn;①一个非线程安全的变量

  public void addTopic(){

  Statement stat = conn.createStatement();②引用非线程安全变量

  …

  }

  }

  由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:

  代码清单4 TopicDao:线程安全

  import java.sql.Connection;

  import java.sql.Statement;

  public class TopicDao {

  ①使用ThreadLocal保存Connection变量

  private static ThreadLocal connThreadLocal = new ThreadLocal();

  public static Connection getConnection(){

  ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,

  并将其保存到线程本地变量中。

  if (connThreadLocal. get() == null) {

  Connection conn = ConnectionManager.getConnection();

  connThreadLocal.set(conn);

  return conn;

  }else{

  return connThreadLocal. get();③直接返回线程本地变量

  }

  }

  public void addTopic() {

  ④从ThreadLocal中获取线程对应的Connection

  Statement stat = getConnection().createStatement();

  }

  }

  不同的线程在使用TopicDao时,先判断connThreadLocal.是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。

  当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。

小结  ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
结论就是:在spring管理【ThreadLocal管理的类变量,他也仅仅是在管理变量而已】的,要访问线程不安全的对象中的变量的时候都会将原来的对象copy一份,进而访问这个copy

版本,所以从来就没有机会访问原来的对象,反而这个原来应该被访问的对象倒是成了闲置、冗余的对象了。

Spring中Singleton模式的线程安全相关推荐

  1. (转)Spring中Singleton模式的线程安全

    不知道哪里的文章,总结性还是比较好的.但是代码凌乱,有的还没有图.如果找到原文了可以进行替换! spring中的单例 spring中管理的bean实例默认情况下是单例的[sigleton类型],就还有 ...

  2. Spring中的Bean是线程安全的么?

    1.架构师系列内容:架构师学习笔记(持续更新) 答: 首先Spring 中的Bean是哪里来的? spring中的bean是初始化时,通过扫描,利用反射new出来的.并且缓存在IOC 容器中,所以Sp ...

  3. Spring 中的bean 是线程安全的吗?

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 作者:myseries cnblogs.com/myser ...

  4. spring 中单利模式的理解

    一.Spring单例模式与线程安全 Spring框架里的bean,或者说组件,获取实例的时候都是默认的单例模式,这是在多线程开发的时候要尤其注意的地方. 单例模式的意思就是只有一个实例.单例模式确保某 ...

  5. Spring中策略模式实现方法

    一.定义 在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改.这种类型的设计模式属于行为型模式.在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而 ...

  6. js中singleton模式解析及运用

    singleton模式,又名单例模式.顾名思义,就是只能实例化一次的类(javascript中没有真正的类,我们通常用函数来模拟类,习惯称之为"伪类").具体地说,singleto ...

  7. 复盘Spring中定时任务和异步线程池

    ​ 项目中最近使用了多个定时任务处理业务需求,于是在实现业务逻辑过程中,产生了上图一些思考和疑问,现在利用空余时间进行一次复盘. 项目搭建 项目搭建环境:JDK1.8+SpringBoot 主启动类: ...

  8. Spring中Singleton作用域和Prototype作用域

    目录 基本概念 演示 基本概念 singleton作用域就是单例模式:用spring容器实现单例模式 而prototype作用域可以理解为多例模式! Singleton作用域例子: 左侧的ref就是右 ...

  9. Spring 中策略模式的 2 个经典应用

    点击蓝色"程序猿DD"关注我哟 加个"星标",不忘签到哦 转自头条号程序汪汪 背景 程序员在项目实战中,策略模式用的非常多. 学习目标 会在Spring项目中运 ...

最新文章

  1. innodb参数汇总
  2. 用标准 GHOST镜像xpe系统(EWF保护模式为RAM)时,写保护丢失问题的解决方法
  3. qq纵横四海源码_【0基础】纵横中文网python爬虫实战
  4. Linux怎么对文件内容trim,Linux平台下SSD的TRIM指令的最佳使用方式(不区别对待NVMe)...
  5. ORA-39070:无法打开日志文件
  6. 6-7 jmu-Java-07多线程-Thread (3分)
  7. php标准代码格式,PHP PSR代码格式规范
  8. web平台安全测试方案
  9. chrome手机版怎么扫描二维码_照片扫描仪软件手机版-照片扫描仪手机版官网版下载v3.2.0...
  10. GDUT 第一次组队赛 Team up! Team up! Team up!(三,dp,dfs)
  11. 解决Eclipse中无法直接使用sun.misc.BASE64Encoder及sun.misc.BASE64Decoder的问题---gxl
  12. 一个正经的电商运营每天应该看哪些数据?
  13. [Vue warn]: Extraneous non-props attributes (style) were passed to component but could not be 警告
  14. 主、谓、宾、定、状、补
  15. YC出品的创业第一课:How to start a startup
  16. 网工知识角|如何理解网络拓扑中的下一跳地址
  17. Java+Selenium+Chrome、Firefox自动化测试环境搭建
  18. Arcgis中碎小斑块的处理
  19. ST32/GD32嵌入式硬件开发总目录
  20. 如果你已经掌握了 Python 101,那么你可能比 OpenAI 的原型 Codex 更擅长编程

热门文章

  1. Vue-vue-cli的安装
  2. Spring In Action 03 ---面向切面的Spring
  3. java控制订单过期时间_订单自动过期实现方案
  4. 电脑插了两根内存条只读出来一根,另一条是为硬件保留的内存空间
  5. fossid安装教程_源代码怎么使用,源代码生成app教程
  6. 2019上半年各大手机销量榜单:华为第一,苹果第五,三星没落!
  7. vc 触摸屏电脑 显示触摸屏软键盘
  8. project.pbxproj文件介绍
  9. Excel——使用OFFSET、MATCH、COUNTA实现二级菜单
  10. 计算机网络时有时无,电脑WiFi时有时无不稳定的解决方法 | 我爱分享网