Copyright © 2015 Powered by MWeb, Theme used GitHub CSS.
在阅读Dubbo源码时发现Dubbo针对java的spi机制做了扩展。那么spi究竟是什么呢?
SPI全称为Service Provider Interface,是Java提供的用来加载第三方实现的service的API。
比如java.sql.Driver
接口,第三方厂商比如MySql,PostgreSql均会实现这个接口来提供接入数据库的实现,Java的SPI机制就可以某个接口寻找服务实现。
当服务提供者实现了一种接口后,需要在classpath的META-INF/services
中创建一个以服务接口名命名的文件,文件的内容是这个接口的具体实现类。当调用者需要用到这个服务时,就会查找对应jar包下的META-INF/services
中的配置,如果找到了,那么就会实例化这个实现类,调用者就可以调用对应的服务了。Java中,查找实现类的工具类是java.util.ServiceLoader
。
接下来我们自己实现一个简单的SPI服务,从中可以学习到SPI的机制。
People
的interface,提供一个hello
的方法Jack
与Jenny
为People
的实现module,各自继承People
接口,并实现自己的代码逻辑,本例为了简单只是单纯的输出了各自的名字,如下所示:META-INF/services
文件夹,并且在该文件夹下创建一个名为io.wkz.People
的文件,文件内容分别为Jack
与Jenny
的全限定名。其结构如下:ServiceLodaer
加载People
的实现类,并且分别调用hello
方法,如下所示:观察main函数会发现,我们并没有在main函数下实例化Jack
或Jenny
,但是我们在调用服务时,仍然调用到了这两个类的实例。从这里可以发现,SPI的核心思想其实就是解耦。
既然在上一个例子里我们是使用的ServiceLoader
来获取不同的服务提供者,那么我们就进入ServiceLoader
源码探索一番吧。
首先查看一下ServiceLoader
的属性字段
public final class ServiceLoader<S>
implements Iterable<S>
{
//看属性名可知此字段就是查找配置文件的前缀文件夹名,从这里也了解了为什么必须要在META-INF/services下新增配置文件。
private static final String PREFIX = "META-INF/services/";
// 代表被加载的类或者接口
private final Class<S> service;
// 类加载器,用于加载并实例化服务
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文
private final AccessControlContext acc;
// 实例化后的服务缓存,用的是LinkedHashMap,所以是按顺序排列的,具体排列顺序是按实例化的先后顺序
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 迭代器,Lazy代表是懒的
private LazyIterator lookupIterator;
...
}
属性字段上比较简单,接下来我们看一下ServiceLoader.load
方法,跟踪代码发现其最终是实例化了一个ServiceLoader
类并返回。继续进入构造方法
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
构造函数里前面三行属于防御式编程,将null
处理掉,要么报错,要么设置默认值。最后一行的reload
方法是清空providers
,并且实例化一个LazyIterator
用于后期迭代。
LazyIterator
也比较简单,看名字就是实现了Iterator接口的。比较关键的方法是nextService
private S nextService() {
//防御式编程
if (!hasNextService())
throw new NoSuchElementException();
//nextName是在hasNextService方法中赋值的,这里获取到就置空
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//使用当前的ClassLoader加载类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
//加载失败,抛错。
fail(service,
"Provider " + cn + " not found");
}
//判断加载的类是不是当前service的子类或者实现类。
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//实例化类。注意这里,这步表明我们实现的service必须要有一个无参构造函数
S p = service.cast(c.newInstance());
//进行缓存。这里可以看ServiceLoader的Iterator实现。是先判断的providers是不是hasNext,如果不是,则进入到本LazyIterator中,从一定程度上解决了创建时就加载全部实现类的问题,但是并没有治本。
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
//防御式编程
throw new Error(); // This cannot happen
}
从代码层面上看,我们可以得出几点结论:
Java提供的SPI机制,优势在于解耦,调用方不需要明确耦合第三方实现类,而是交由ServiceLoader提供。当程序需要使用另一种第三方实现类时,直接替换依赖就可以,无需重构代码。
缺点其实在上面说到了。这里就不赘述了。Dubbo其实也是针对这些默认SPI的缺点才自己实现了Dubbo SPI。
面试时,很多人都会被问到关于ThreadLocal的内容,如果没有看过ThreadLocal相关的代码的话,关于这方面的面试题都会回答的很混乱。今天,我们就从ThreadLocal的源代码入手,来彻底解开ThreadLocal的原理。
我们知道,看java的源码时,一般都是先去看一个类上面的注释,类上的注释会表明该类是用来做什么的。下面节选一部分ThreadLocal的类注释:
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*/
翻译过来的意思就是:
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其get 或 set方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
很多人以为ThreadLocal是用来解决线程同步的问题的,其实这是非常大的误解。ThreadLocal虽然提供了一种多线程下成员变量问题的解决方式,但是它并不是用来解决多线程共享变量问题的。线程同步机制是多个线程共享同一个成员变量,而ThreadLocal是为每一个线程创建独立的变量,每一个线程都可以独立的修改自己的变量而不需要担心会修改其他线程的变量。
ThreadLocal常用的一共有4种方法
public T get()
返回此线程局部变量的当前线程副本中的值。public void set(T value)
将此线程局部变量的当前线程副本中的值设置为指定值。public void remove()
移除此线程局部变量当前线程的值。private T setInitialValue()
返回此线程局部变量的当前线程的“初始值”。同时,ThreadLocal下有一个内部类叫做ThreadLocal.ThreadLocalMap
,这个类才是实现线程隔离机制的核心,上面的get set remove
最终操作的数据结构都是该内部类。看ThreadLocalMap
的名字也能大概猜出该类是基于键值对的方式存储的,key是当前的ThreadLocal实例,value是对应线程的变量副本。
所以从上面的说明来看,我们可以得出如下两个结论
下图是两者的关系
public class SeqCount {
private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
// 实现initialValue()
public Integer initialValue() {
return 0;
}
};
public int nextSeq(){
seqCount.set(seqCount.get() + 1);
return seqCount.get();
}
public static void main(String[] args){
SeqCount seqCount = new SeqCount();
SeqThread thread1 = new SeqThread(seqCount);
SeqThread thread2 = new SeqThread(seqCount);
SeqThread thread3 = new SeqThread(seqCount);
SeqThread thread4 = new SeqThread(seqCount);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
private static class SeqThread extends Thread{
private SeqCount seqCount;
SeqThread(SeqCount seqCount){
this.seqCount = seqCount;
}
public void run() {
for(int i = 0 ; i < 3 ; i++){
System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
}
}
}
}
运行结果为
可以看到,三个线程是分别累加的自己的独立的数据,相互之间没有任何的干扰。
我们先从get
方法入手
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
get
方法流程是首先获取当前的线程t
,然后通过getMap(t)
获取到ThreadLocalMap
实例map
,如果map
不为null
,那么就通过map.getEntry
获取ThreadLocalMap.Entry
实例e
,如果e
不为null
,那么就返回e.value
,否则调用setInitialValue
获取默认值。
getMap
方法源码:
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到方法只有一行,非常的简洁,就是返回了Thread
实例t
的threadLocals
变量。那么这个变量又是什么呢?继续跟踪到Thread
类的源码中,
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
就是一个ThreadLocal.ThreadLocalMap
的实例,默认为null
。
ThreadLocalMap
内部利用Entry
来实现键值对的存储,Entry
的源码如下:
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到Entry
是一个WeakReference
弱引用,其key是ThreadLocal
实例,关于弱引用这里不再赘述,可以参考其他文章。
ThreadLocalMap
的方法比较多,我们着重介绍两个方法getEntry
与set
方法
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
//获取key的hash值,用于在table中查找对应的Entry
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//当当前位置一次找到了对应的Entry,直接返回
if (e != null && e.get() == key)
return e;
else
//当当前位置为null或者当前位置存储的并不是要找的Entry时,进入此方法查找
return getEntryAfterMiss(key, i, e);
}
getEntry
没有太大的难度,与HashMap.get
的初始思路比较一致,都是先计算hash,然后去对应的位置查找。但是ThreadLocalMap
与HashMap
不一致的地方在于,HashMap
针对hash碰撞所采用的方式是链表法(即,将所有hash冲突的元素保存在一个链表中),而ThreadLocalMap
所采用的方式是开放定址法(即,当发现冲突时,遍历table到接下来的一个空位,将其存储在这里。)。读者可以思考一下为什么同是散列表的实现,为什么这两者要使用不同的hash冲突解决方式。
由于ThreadLocalMap
使用的开放定址法,因此当查找不到时会调用getEntryAfterMiss
方法,源码如下:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
整体也没有难度,需要注意的一点是当遍历到k == null
时,会调用expungeStaleEntry
方法会rehash当前节点到下一个null
节点之间的键值对,辅助gc。
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
与get
方法类似,也是先获取当前线程的ThreadLocalMap
,当ThreadLocalMap
不为null
时,调用ThreadLocalMap.set
方法保存;当ThreadLocalMap
为null
时,调用createMap
方法初始化ThreadLocalMap
实例,并写入一个键值对。ThreadLoalMap.set
方法不多赘述,只要了解了开放定址法就很简单了。
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
跟上面的set
或get
方法类似,只不过最后不需要考虑ThreadLocalMap
为空的情况。
protected T initialValue() {
return null;
}
默认是返回null
,我们可以根据业务不同设置不同的返回值即可。
前面提到每个Thread都有一个ThreadLocal.ThreadLocalMap的map,该map的key为ThreadLocal实例,它为一个弱引用,我们知道弱引用有利于GC回收。当ThreadLocal的key == null时,GC就会回收这部分空间,但是value却不一定能够被回收,因为他还与Current Thread存在一个强引用关系。
由于存在这个强引用关系,会导致value无法回收。如果这个线程对象不会销毁那么这个强引用关系则会一直存在,就会出现内存泄漏情况。所以说只要这个线程对象能够及时被GC回收,就不会出现内存泄漏。如果碰到线程池,那就更坑了。
那么要怎么避免这个问题呢?
在前面提过,在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情况,会对value设置为null。当然我们也可以显示调用ThreadLocal的remove()方法进行处理。
Copyright © 2015 Powered by MWeb, Theme used GitHub CSS.