好吧,最近我特么是跟高并发杠上了。。
单例模式想必很很常见,而往往单例模式跟static相关。单例模式的初衷是为了在任何条件下我只得到一个实例,包括类和变量。而往往需要我们用static关键字去修饰达到单例的效果。最近高并发接触得比较多,使用缓存就需要用单例。因为你针对某一个key的缓存只可能定义成“一份”。所以缓存类的实例需要用到单例模式。但是在高并发的条件下,控制不好的话,很容易出问题。下面写个小例子,就能看出是什么问题了……
@Controller public class TestAction { @RequestMapping("/test/context.json") @ResponseBody public void test() { Thread t = Thread.currentThread(); new Thread(new TestThread("count")).start(); try { t.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(new TestThread("count")).start(); } } class TestThread implements Runnable{ private String attr; public TestThread(String attr) { this.attr = attr; } @Override public void run() { List<String> list = Test3.getList(attr); list.add("d"); System.out.println(list); System.out.println("==========="); } }
这里用启动两个线程TestThread模拟“并发”。
而我们再模拟使用到单例模式的情形:
public class Test3 { private static List<String> list = new ArrayList<String>(); public static List<String> getList(String attr) { WebApplicationContext wc = ContextLoader.getCurrentWebApplicationContext(); //这里用到ServletContext模拟缓存的情况 ServletContext sc = wc.getServletContext(); String count = (String) sc.getAttribute(attr); if(StringUtils.equals("1", count)) { //啥也不做 }else{ sc.setAttribute(attr, "1"); list.add("a"); list.add("b"); list.add("c"); } return list; } }
其中list是static的全局变量。这里用ServletContext的特性模拟了缓存的情况。
看看TestAction中定义的线程TestThread,该线程被启动了2次,(模拟并发),并且2次都是传入同一个参数(模拟相同条件)"count"。
浏览器输入TestAction注解的Url,可发现控制台打印如下:
[a, b, c, d]
===========
[a, b, c, d, d]
===========
再次输入该Url,打印如下:
[a, b, c, d, d, d]
===========
[a, b, c, d, d, d, d]
===========
问题已很明显了,线程第一次执行时,集合本来为[a.b.c]被它修改(add("d"))之后,集合被覆盖为[a,b,c,d]了;同理,第二次输入Url之后,集合又被线程第二次执行时覆盖为[a,b,c,d,d]了·,所以此次在进行add("d")操作之后,集合被覆盖为[a,b,c,d,d,d]啦,以此类推……
其实这种问题是比较容易被忽视的,并发条件下,你对一个“公共”的变量(一般是由static修饰),常见场景如缓存的操作(这里是add("d"))修改,会不断更新【最初】的变量值,【新】的线程再次访问时,得到的已经不是【最初】的值了。这显然是不对的,我们需要做到对一个公共变量进行多线程访问时,线程与线程之间的访问不彼此影响,即:线程不会修改公共的变量值,不影响其他线程的访问。
注意:需要注意这种情况只涉及到线程需要对拿到的公共变量修改时,纯读取的话,没必要注意这个问题。
如何解决呢?我们只需拷贝一个公共变量的“副本”,即可达到想要的效果:
改变Test3的方法如下:
public static List<String> getList(String attr) { WebApplicationContext wc = ContextLoader.getCurrentWebApplicationContext(); ServletContext sc = wc.getServletContext(); String count = (String) sc.getAttribute(attr); if(StringUtils.equals("1", count)) { //啥也不做 }else{ sc.setAttribute(attr, "1"); list.add("a"); list.add("b"); list.add("c"); } List<String> copyList = new ArrayList<String>(list); return copyList; }
copyList是公共变量的副本,这样,当有N个线程去访问公共变量时,得到的是副本,你之后再对该副本进行任何操作,都不会影响公共变量,从而不影响其他线程对该公共变量的访问,确保其他线程拿到的都是【最初】的公共变量。
同样,访问Url,
打印如下:
[a, b, c, d]
===========
[a, b, c, d]
===========
再次访问:
[a, b, c, d]
===========
[a, b, c, d]
===========
说明:问题解决。
相关推荐
深入浅出:讲解单例模式,多线程安全和并发访问问题.让你轻松应对面试
单例模式单例模式单例模式单例模式单例模式单例模式单例模式单例模式
C#单例模式C#单例模式详解C#单例模式详解C#单例模式详解
2020-02-10 王争设计模式之美进入课程讲述:冯永吉时长 10:21大小 8.31M上两节课中,我们针对单例模式,讲解了单例的应用场景、几种常见的代码实现
单例模式详解~~单例模式详解~~单例模式详解~~
典型的情况是,那些对象的类型被遍及一个软件系统的不同对象访问,因此需要一个全局的访问指针,这便是众所周知的单例模式的应用。当然这只有在你确信你不再需要任何多于一个的实例的情况下。 单例模式的用意在于前...
php单例模式php单例模式php单例模式php单例模式
一个简单的java工程,包含注释,一目了然,其中包含了单例模式的所有实现方式,懒汉式,饿汉式,双重校验,枚举,静态内部类等方式实现单例。
Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式...
设计模式之七种单例模式代码及ppt,包含多线程环境测试和反序列化测试
单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。一般用于全局接口(比如...同时跟多线程有关,并发中怎么处理多线程去操作这个单利进行实例问题
几种单例模式的书写方式
单例模式和工厂模式结合应用,实现了产品的生产,适合用做课程设计,包含详细文档和代码。Java语言。喜欢的可以下载来看看那
java Singleton单例模式 java Singleton单例模式
目录 单例模式的概念 单例模式的要点 单例模式类图 单例模式归类 单例模式的应用场景 单例模式解决的问题 单例模式的实现方式 单例模式实现方式对比 单例模式的概念 单例模式,顾名思义就是只有一个实例,并且由它...
该资源是多线程并发下的单例模式-源码,几乎包含了所有方式实现的单例模式,并且能够确保在多线程并发下的线程安全性。 读者可结合本人博客 http://blog.csdn.net/cselmu9?viewmode=list 中的《线程并发之单例模式...
单例模式--只能弹出一个窗体 只能弹出一个窗体
PHP单例模式访问数据库 php 单例模式 数据库 设计模式
设计模式之单例模式 懒汉式,饿汉式,同步枷锁
设计模式C++学习之单例模式(Singleton)