Java并发编程学习之ThreadLocal源码详析

 更新时间:2018年06月04日 10:06:34   转载 作者:狂小白  
这篇文章主要给大家介绍了关于Java并发编程学习之源码分析ThreadLocal的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

前言

多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的,多线程访问同一个共享变量特别容易出现并发问题,特别是多个线程需要对一个共享变量进行写入时候,为了保证线程安全,

一般需要使用者在访问共享变量的时候进行适当的同步,如下图所示:

 

可以看到同步的措施一般是加锁,这就需要使用者对锁也要有一定了解,这显然加重了使用者的负担。那么有没有一种方式当创建一个变量的时候,每个线程对其进行访问的时候访问的是自己线程的变量呢?其实ThreaLocal就可以做这个事情,注意一下,ThreadLocal的出现并不是为了解决上面的问题而出现的。

ThreadLocal是在JDK包里面提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而避免了线程安全问题,创建一个ThreadLocal变量后,

每个线程会拷贝一个变量到自己的本地内存,如下图:

好了,现在我们思考一个问题:ThreadLocal的实现原理,ThreadLocal作为变量的线程隔离方式,其内部又是如何实现的呢?

首先我们要看ThreadLocal的类图结构,如下图所示:

 如

上类图可见,Thread类中有一个threadLocals和inheritableThreadLocals 都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap,默认每个线程中这两个变量都为null,只有当线程第一次调用了ThreadLocal的set或者get方法的时候才会创建。

其实每个线程的本地变量不是存到ThreadLocal实例里面的,而是存放到调用线程的threadLocals变量里面。也就是说ThreadLocal类型的本地变量是存放到具体线程内存空间的。 

ThreadLocal其实就是一个外壳,它通过set方法把value值放入调用线程threadLocals里面存放起来,当调用线程调用它的get方法的时候再从当前线程的threadLocals变量里面拿出来使用。如果调用线程如果一直不终止的话,那么这个本地变量会一直存放到调用线程的threadLocals变量里面,

因此,当不需要使用本地变量时候可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals变量里面删除该本地变量。可能还有人会问threadLocals为什么设计为Map结构呢?很明显是因为每个线程里面可以关联多个ThreadLocal变量。

接下来我们可以进入到ThreadLocal中的源码如看看,如下代码所示:

主要看set,get,remove这三个方法的实现逻辑,如下:

先看set(T var1)方法

public void set(T var1) {     //(1)获取当前线程
 Thread var2 = Thread.currentThread();     //(2) 当前线程作为key,去查找对应的线程变量,找到则设置
 ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
 if(var3 != null) {
 var3.set(this, var1);
 } else {       //(3) 第一次调用则创建当前线程对应的Hashmap
 this.createMap(var2, var1);
 }
 }

如上代码(1)首先获取调用线程,然后使用当前线程作为参数调用了 getMap(var2) 方法,getMap(Thread var2) 代码如下:

   ThreadLocal.ThreadLocalMap getMap(Thread var1) {
 return var1.threadLocals;
 }

可知getMap(var2) 所作的就是获取线程自己的变量threadLocals,threadlocal变量是绑定到了线程的成员变量里面。

如果getMap(var2) 返回不为空,则把 value 值设置进入到 threadLocals,也就是把当前变量值放入了当前线程的内存变量 threadLocals,threadLocals 是个 HashMap 结构,其中 key 就是当前 ThreadLocal 的实例对象引用,value 是通过 set 方法传递的值。

如果 getMap(var2) 返回空那说明是第一次调用 set 方法,则创建当前线程的 threadLocals 变量,下面看 createMap(var2, var1) 里面做了啥呢?

 void createMap(Thread var1, T var2) {
 var1.threadLocals = new ThreadLocal.ThreadLocalMap(this, var2);
 }

可以看到的就是创建当前线程的threadLocals变量。

接下来我们再看get()方法,代码如下:

public T get() {    //(4)获取当前线程
 Thread var1 = Thread.currentThread();    //(5)获取当前线程的threadLocals变量
 ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);    //(6)如果threadLocals不为null,则返回对应本地变量值
 if(var2 != null) {
 ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
 if(var3 != null) {
 Object var4 = var3.value;
 return var4;
 }
 }
    //(7)threadLocals为空则初始化当前线程的threadLocals成员变量。
 return this.setInitialValue();
 }

代码(4)首先获取当前线程实例,如果当前线程的threadLocals变量不为null则直接返回当前线程的本地变量。否则执行代码(7)进行初始化,setInitialValue()的代码如下:

private T setInitialValue() {    //(8)初始化为null
 Object var1 = this.initialValue();
 Thread var2 = Thread.currentThread();
 ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);    //(9)如果当前线程变量的threadLocals变量不为空
 if(var3 != null) {
 var3.set(this, var1);    //(10)如果当前线程的threadLocals变量为空
 } else {
 this.createMap(var2, var1);
 }

 return var1;
 }

如上代码如果当前线程的 threadLocals 变量不为空,则设置当前线程的本地变量值为 null,否者调用 createMap 创建当前线程的 createMap 变量。

接着我们在看看void remove()方法,代码如下:

public void remove() {
 ThreadLocal.ThreadLocalMap var1 = this.getMap(Thread.currentThread());
 if(var1 != null) {
 var1.remove(this);
 }
 }

如上代码,如果当前线程的 threadLocals 变量不为空,则删除当前线程中指定 ThreadLocal 实例的本地变量。

接下来我们看看具体演示demo,代码如下:

/**
 * Created by cong on 2018/6/3.
 */
public class ThreadLocalTest {
 //(1)打印函数
 static void print(String str) {
 //1.1 打印当前线程本地内存中localVariable变量的值
 System.out.println(str + ":" + localVariable.get());
 //1.2 清除当前线程本地内存中localVariable变量
 //localVariable.remove();
 }

 //(2) 创建ThreadLocal变量
 static ThreadLocal<String> localVariable = new ThreadLocal<>();
 public static void main(String[] args) {

 //(3) 创建线程one
 Thread threadOne = new Thread(new Runnable() {
 public void run() {
 //3.1 设置线程one中本地变量localVariable的值
 localVariable.set("线程1的本地变量");
 //3.2 调用打印函数
 print("线程1----->");
 //3.3打印本地变量值
 System.out.println("移除线程1本地变量后的结果" + ":" + localVariable.get());

 }
 });
 //(4) 创建线程two
 Thread threadTwo = new Thread(new Runnable() {
 public void run() {
 //4.1 设置线程one中本地变量localVariable的值
 localVariable.set("线程2的本地变量");
 //4.2 调用打印函数
 print("线程2----->");
 //4.3打印本地变量值
 System.out.println("移除线程2本地变量后的结果" + ":" + localVariable.get());

 }
 });
 //(5)启动线程
 threadOne.start();
 threadTwo.start();
 }
}

代码(2)创建了一个 ThreadLocal 变量;

代码(3)、(4)分别创建了线程 1和 2;

代码(5)启动了两个线程;

线程 1 中代码 3.1 通过 set 方法设置了 localVariable 的值,这个设置的其实是线程 1 本地内存中的一个拷贝,这个拷贝线程 2 是访问不了的。然后代码 3.2 调用了 print 函数,代码 1.1 通过 get 函数获取了当前线程(线程 1)本地内存中 localVariable 的值;

线程 2 执行类似线程 1。 

运行结果如下:

 

这里要注意一下ThreadLocal的内存泄漏问题

  每个线程内部都有一个名字为 threadLocals 的成员变量,该变量类型为 HashMap,其中 key 为我们定义的 ThreadLocal 变量的 this 引用,value 则为我们 set 时候的值,每个线程的本地变量是存到线程自己的内存变量 threadLocals 里面的,如果当前线程一直不消失那么这些本地变量会一直存到,

所以可能会造成内存泄露,所以使用完毕后要记得调用 ThreadLocal 的 remove 方法删除对应线程的 threadLocals 中的本地变量。

解开代码1.2的注释后,再次运行,运行结果如下:

 

我们有没有想过这样的一个问题:子线程中是否获取到父线程中设置的 ThreadLocal 变量的值呢?

  这里可以告诉大家,在子线程中是获取不到父线程中设置的 ThreadLocal 变量的值的。那么有办法让子线程访问到父线程中的值吗?为了解决该问题 InheritableThreadLocal 应运而生,InheritableThreadLocal 继承自 ThreadLocal,提供了一个特性,就是子线程可以访问到父线程中设置的本地变量。

首先我们先进入InheritableThreadLocal这个类的源码去看,如下:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
 public InheritableThreadLocal() {
 }
  //(1)
 protected T childValue(T var1) {
 return var1;
 }
  //(2)
 ThreadLocalMap getMap(Thread var1) {
 return var1.inheritableThreadLocals;
 }
  //(3)
 void createMap(Thread var1, T var2) {
 var1.inheritableThreadLocals = new ThreadLocalMap(this, var2);
 }
}

 可以看到InheritableThreadlocal继承ThreadLocal,并重写了三个方法,在上面的代码已经标出了。代码(3)可知InheritableThreadLocal重写createMap方法,那么可以知道现在当第一次调用set方法时候创建的是当前线程的inhertableThreadLocals变量的实例,而不再是threadLocals。

代码(2)可以知道当调用get方法获取当前线程的内部map变量时候,获取的是inheritableThreadLocals,而不再是threadLocals。

关键地方来了,重写的代码(1)是何时被执行的,以及如何实现子线程可以访问父线程本地变量的。这个要从Thread创建的代码看起,Thread的默认构造函数以及Thread.java类的构造函数如下:

/**
 * Created by cong on 2018/6/3.
 */
  public Thread(Runnable target) {
  init(null, target, "Thread-" + nextThreadNum(), 0);
  }
  private void init(ThreadGroup g, Runnable target, String name,
  long stackSize, AccessControlContext acc) {
  //...
  //(4)获取当前线程
  Thread parent = currentThread();
  //...
  //(5)如果父线程的inheritableThreadLocals变量不为null
  if (parent.inheritableThreadLocals != null)
  //(6)设置子线程中的inheritableThreadLocals变量
  this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  this.stackSize = stackSize;
  tid = nextThreadID();
  }

创建线程时候在构造函数里面会调用init方法,前面讲到了inheritableThreadLocal类get,set方法操作的是变量inheritableThreadLocals,所以这里inheritableThreadLocal变量就不为null,所以会执行代码(6),下面看createInheritedMap方法源码,如下:

  static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
 return new ThreadLocalMap(parentMap);
 }

可以看到createInheritedMap内部使用父线程的inheritableThreadLocals变量作为构造函数创建了一个新的ThreadLocalMap变量,然后赋值给了子线程的inheritableThreadLocals变量,那么接着进入到ThreadLocalMap的构造函数里面做了什么,源码如下:

private ThreadLocalMap(ThreadLocalMap parentMap) {
  Entry[] parentTable = parentMap.table;
  int len = parentTable.length;
  setThreshold(len);
  table = new Entry[len];

  for (int j = 0; j < len; j++) {
  Entry e = parentTable[j];
  if (e != null) {
   @SuppressWarnings("unchecked")
   ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
   if (key != null) {
   //(7)调用重写的方法
   Object value = key.childValue(e.value);//返回e.value
   Entry c = new Entry(key, value);
   int h = key.threadLocalHashCode & (len - 1);
   while (table[h] != null)
    h = nextIndex(h, len);
   table[h] = c;
   size++;
   }
  }
  }
 }

如上代码所做的事情就是把父线程的inhertableThreadLocals成员变量的值复制到新的ThreadLocalMap对象,其中代码(7)InheritableThreadLocal类重写的代码(1)也映入眼帘了。

总的来说:InheritableThreadLocal类通过重写代码(2)和(3)让本地变量保存到了具体线程的inheritableThreadLocals变量里面,线程通过InheritableThreadLocal类实例的set 或者 get方法设置变量时候就会创建当前线程的inheritableThreadLocals变量。当父线程创建子线程时候,

构造函数里面就会把父线程中inheritableThreadLocals变量里面的本地变量拷贝一份复制到子线程的inheritableThreadLocals变量里面。

好了原理了解到位了,接下来进行一个例子来验证上面所了解的东西,如下:

package com.hjc;
/**
 * Created by cong on 2018/6/3.
 */
public class InheritableThreadLocalTest {
 //(1) 创建线程变量
 public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
 public static void main(String[] args) {
 //(2) 设置线程变量
 threadLocal.set("hello Java");
 //(3) 启动子线程
 Thread thread = new Thread(new Runnable() {
  public void run() {
  //(4)子线程输出线程变量的值
  System.out.println("子线程:" + threadLocal.get());

  }
 });
 thread.start();
 //(5)主线程输出线程变量值
 System.out.println("父线程:" + threadLocal.get());
 }
}

运行结果如下:

 

也就是说同一个 ThreadLocal 变量在父线程中设置值后,在子线程中是获取不到的。根据上节的介绍,这个应该是正常现象,因为子线程调用 get 方法时候当前线程为子线程,而调用 set 方法设置线程变量是 main 线程,两者是不同的线程,自然子线程访问时候返回 null。

那么有办法让子线程访问到父线程中的值吗?答案是有,就用我们上面原理分析的InheritableThreadLocal。

将上面例子的代码(1)修改为:

 //(1) 创建线程变量
 public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>();

运行结果如下:

 

可知现在可以从子线程中正常的获取到线程变量值了。那么什么情况下需要子线程可以获取到父线程的 threadlocal 变量呢?

  情况还是蛮多的,比如存放用户登录信息的 threadlocal 变量,很有可能子线程中也需要使用用户登录信息,再比如一些中间件需要用统一的追踪 ID 把整个调用链路记录下来的情景。

Spring Request Scope 作用域 Bean 中 ThreadLocal 的使用

  我们知道 Spring 中在 XML 里面配置 Bean 的时候可以指定 scope 属性来配置该 Bean 的作用域为 singleton、prototype、request、session 等,其中作用域为 request 的实现原理就是使用 ThreadLocal 实现的。如果你想让你 Spring 容器里的某个 Bean 拥有 Web 的某种作用域,

则除了需要 Bean 级上配置相应的 scope 属性,还必须在 web.xml 里面配置如下:

<listener>
 <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>

这里主要看RequestContextListener的两个方法:

public void requestInitialized(ServletRequestEvent requestEvent)

public void requestDestroyed(ServletRequestEvent requestEvent)

当一个web请求过来时候会执行requestInitialized方法:

public void requestInitialized(ServletRequestEvent requestEvent) {
    .......省略
  HttpServletRequest request = (HttpServletRequest) requestEvent.getServletRequest();
  ServletRequestAttributes attributes = new ServletRequestAttributes(request);
  request.setAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE, attributes);
  LocaleContextHolder.setLocale(request.getLocale());
  //设置属性到threadlocal变量
  RequestContextHolder.setRequestAttributes(attributes);
 }
 public static void setRequestAttributes(RequestAttributes attributes) {

  setRequestAttributes(attributes, false);
 }
 public static void setRequestAttributes(RequestAttributes attributes, boolean inheritable) {
  if (attributes == null) {
   resetRequestAttributes();
  }
  else {
   //默认inheritable=false
   if (inheritable) {
    inheritableRequestAttributesHolder.set(attributes);
    requestAttributesHolder.remove();
   }
   else {
    requestAttributesHolder.set(attributes);
    inheritableRequestAttributesHolder.remove();
   }
  }
 }

可以看到上面源码,由于默认inheritable 为FALSE,我们的属性值都放到了requestAttributesHoder里面,而它的定义是:

   private static final ThreadLocal<RequestAttributes> requestAttributesHolder =new NamedThreadLocal<RequestAttributes>("Request attributes");

  private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =new NamedInheritableThreadLocal<RequestAttributes>("Request context");

其中NamedThreadLocal<T> extends ThreadLocal<T>,所以不具有继承性。

其中 NamedThreadLocal<T> extends ThreadLocal<T>,所以不具有继承性。

NameInheritableThreadLocal<T> extends InheritableThreadLocal<T>,所以具有继承性,所以默认放入到RequestContextHolder里面的属性值在子线程中获取不到。

当请求结束时候调用requestDestroyed方法,源码如下:

public void requestDestroyed(ServletRequestEvent requestEvent) {
  ServletRequestAttributes attributes =
    (ServletRequestAttributes) requestEvent.getServletRequest().getAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE);
  ServletRequestAttributes threadAttributes =
    (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  if (threadAttributes != null) {
   // 我们很有可能在最初的请求线程中
   if (attributes == null) {
    attributes = threadAttributes;
   }
   //请求结束则清除当前线程的线程变量。
   LocaleContextHolder.resetLocaleContext();
   RequestContextHolder.resetRequestAttributes();
  }
  if (attributes != null) {
   attributes.requestCompleted();
  }
 }

接下来从时序图看一下 Web请求调用逻辑如何:

 

 也就是说每次发起一个Web请求在Tomcat中context(具体应用)处理前,host匹配后会设置下RequestContextHolder属性,让requestAttributesHolder不为空,在请求结束时会清除。

因此,默认情况下放入RequestContextHolder里面的属性子线程访问不到,Spring 的request作用域的bean是使用threadlocal实现的。 

接下来进行一个例子模拟请求,代码如下:

web.xml配置如下:

因为是 request 作用域,所以必须是 Web 项目,并且需要配置 RequestContextListener 到 web.xml。

<listener>
  <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>

接着注入一个 request 作用域 bean 到 IOC 容器。代码如下:

<bean id="requestBean" class="hjc.test.RequestBean"
  scope="request">
  <property name="name" value="hjc" />
  <aop:scoped-proxy />
 </bean>

测试代码如下:

@WebResource("/testService")
public class TestRpc {
 @Autowired
 private RequestBean requestInfo;
 @ResourceMapping("test")
 public ActionResult test(ErrorContext context) {
  ActionResult result = new ActionResult();
  pvgInfo.setName("hjc");
  String name = requestInfo.getName();
  result.setValue(name);
  return result;
 }
}

如上首先配置 RequestContextListener 到 web.xml 里面,然后注入了 Request 作用域的 RequestBean 的实例到 IOC 容器,最后 TestRpc 内注入了 RequestBean 的实例,方法 test 首先调用了 requestInfo 的方法 setName 设置 name 属性,然后获取 name 属性并返回。

这里如果 requestInfo 对象是单例的,那么多个线程同时调用 test 方法后,每个线程都是设置-获取的操作,这个操作不是原子性的,会导致线程安全问题。而这里声明的作用域为 request 级别,也是每个线程都有一个 requestInfo 的本地变量。 

上面例子方法请求的时序图如下:

 

我们要着重关注调用test时候发生了什么:

 

其实前面创建的 requestInfo 是被经过 CGliB 代理后的(感兴趣的可以研究下 ScopedProxyFactoryBean 这类),所以这里调用 setName 或者 getName 时候会被 DynamicAdvisedInterceptor 拦截的,拦击器里面最终会调用到 RequestScope 的 get 方法获取当前线程持有的本地变量。

关键来了,我们要看一下RequestScope的get方法的源码如下:

public Object get(String name, ObjectFactory objectFactory) {

  RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();//(1)
  Object scopedObject = attributes.getAttribute(name, getScope());
  if (scopedObject == null) {
   scopedObject = objectFactory.getObject();//(2)
   attributes.setAttribute(name, scopedObject, getScope());//(3)
  }
  return scopedObject;
 }

可知当发起一个请求时候,首先会通过 RequestContextListener.requestInitialized 里面调用 RequestContextHolder.setRequestAttributess 设置 requestAttributesHolder。

然后请求被路由到 TestRpc 的 test 方法后,test 方法内第一次调用 setName 方法时候,最终会调用 RequestScope.get()方法,get 方法内代码(1)获取通过 RequestContextListener.requestInitialized 设置的线程本地变量 requestAttributesHolder 保存的属性集的值。

接着看该属性集里面是否有名字为 requestInfo 的属性,由于是第一次调用,所以不存在,所以会执行代码(2)让 Spring 创建一个 RequestInfo 对象,然后设置到属性集 attributes,也就是保存到了当前请求线程的本地内存里面了。然后返回创建的对象,调用创建对象的 setName。

最后test 方法内紧接着调用了 getName 方法,最终会调用 RequestScope.get() 方法,get 方法内代码(1)获取通过 RequestContextListener.requestInitialized 设置的线程本地变量 RequestAttributes,然后看该属性集里面是否有名字为 requestInfo 的属性,

由于是第一次调用 setName 时候已经设置名字为 requestInfo 的 bean 到 ThreadLocal 变量里面了,并且调用 setName 和 getName 的是同一个线程,所以这里直接返回了调用 setName 时候创建的 RequestInfo 对象,然后调用它的 getName 方法。

到目前为止我们了解ThreadLocal 的实现原理,并指出 ThreadLocal 不支持继承性;然后紧接着讲解了 InheritableThreadLocal 是如何补偿了 ThreadLocal 不支持继承的特性;最后简单的介绍了 Spring 框架中如何使用 ThreadLocal 实现了 Reqeust Scope 的 Bean。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

相关文章

  • Selenium Webdriver实现截图功能的示例

    Selenium Webdriver实现截图功能的示例

    今天小编就为大家分享一篇Selenium Webdriver实现截图功能的示例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-05-05
  • Java AES加密解密的简单实现方法

    Java AES加密解密的简单实现方法

    下面小编就为大家带来一篇Java AES加密解密的简单实现方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • struts2实现多文件上传的示例代码

    struts2实现多文件上传的示例代码

    本篇文章主要介绍了struts2实现多文件上传的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-03-03
  • Java小程序求圆的周长和面积实例

    Java小程序求圆的周长和面积实例

    这篇文章主要介绍了首先用蒙塔卡洛算法求圆周率近似值,然后根据此近似值输出圆的周长和面积,具有一定参考价值,需要的朋友可以了解下。
    2017-09-09
  • JUC之Semaphore源码分析

    JUC之Semaphore源码分析

    这篇文章主要为大家详细分析了JUC之Semaphore源码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-03-03
  • java httpclient设置超时时间和代理的方法

    java httpclient设置超时时间和代理的方法

    这篇文章主要介绍了java httpclient设置超时时间和代理的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-02-02
  • Intellij Idea中进行Mybatis逆向工程的实现

    Intellij Idea中进行Mybatis逆向工程的实现

    这篇文章主要介绍了Intellij Idea中进行Mybatis逆向工程的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-05-05
  • Java判断List中相同值元素的个数实例

    Java判断List中相同值元素的个数实例

    今天小编就为大家分享一篇Java判断List中相同值元素的个数实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-07-07
  • spring boot实现过滤器和拦截器demo

    spring boot实现过滤器和拦截器demo

    本篇文章主要介绍了spring boot实现过滤器和拦截器demo ,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-02-02
  • Java实现商城订单超时取消功能

    Java实现商城订单超时取消功能

    大多数的B2C商城项目都会有限时活动,当用户下单后都会有支付超时时间,当订单超时后订单的状态就会自动变成已取消 ,这个功能的实现有很多种方法,本文的实现方法适合大多数比较小的商城使用。具体实现方式可以跟随小编一起看看吧
    2019-12-12

最新评论