詳談ThreadLocal-單例模式下高并發(fā)線(xiàn)程安全
ThreadLocal-單例模式下高并發(fā)線(xiàn)程安全
在多例的情況下,每個(gè)對(duì)象在堆中聲明內(nèi)存空間,多線(xiàn)程對(duì)應(yīng)的Java棧中的句柄或指針指向堆中不同的對(duì)象,對(duì)象各自變量的變更只會(huì)印象到對(duì)應(yīng)的棧,也就是對(duì)應(yīng)的線(xiàn)程中,不會(huì)影響到其它線(xiàn)程。所以多例的情況下不需要考慮線(xiàn)程安全的問(wèn)題,因?yàn)橐欢ㄊ前踩摹?/p>

而在單例的情況下卻完全不一樣了,在堆中只有一個(gè)對(duì)象,多線(xiàn)程對(duì)應(yīng)的Java棧中的句柄或指針指向同一個(gè)對(duì)象,方法的參數(shù)變量和方法內(nèi)變量是線(xiàn)程安全的,因?yàn)槊繄?zhí)行一個(gè)方法,都會(huì)在獨(dú)立的空間創(chuàng)建局部變量,它不是共享的資源。但是成員變量只有一份,所有指向堆中該對(duì)象的句柄或指針都可以隨時(shí)修改和讀取它,所以是非線(xiàn)程安全的。

為了解決線(xiàn)程安全的問(wèn)題,我們有3個(gè)思路:
- 第一每個(gè)線(xiàn)程獨(dú)享自己的操作對(duì)象,也就是多例,多例勢(shì)必會(huì)帶來(lái)堆內(nèi)存占用、頻繁GC、對(duì)象初始化性能開(kāi)銷(xiāo)等待等一些列問(wèn)題。
- 第二單例模式枷鎖,典型的案例是HashTable和HashMap,對(duì)讀取和變更的操作用synchronized限制起來(lái),保證同一時(shí)間只有一個(gè)線(xiàn)程可以操作該對(duì)象。雖然解決了內(nèi)存、回收、構(gòu)造、初始化等問(wèn)題,但是勢(shì)必會(huì)因?yàn)殒i競(jìng)爭(zhēng)帶來(lái)高并發(fā)下性能的下降。
- 第三個(gè)思路就是今天重點(diǎn)推出的ThreadLocal。單例模式下通過(guò)某種機(jī)制維護(hù)成員變量不同線(xiàn)程的版本。
- 假設(shè)三個(gè)人想從鏡子中看自己,第一個(gè)方案就是每人發(fā)一個(gè)鏡子互不干擾,第二個(gè)方案就是只有一個(gè)鏡子,一個(gè)人站在鏡子前其他人要排隊(duì)等候,第三個(gè)方案就是我這里發(fā)明了一種“魔鏡”,所有人站在鏡子前可以并且只能看到自己?。?!
主程序:
public static void main(String[] args) {
//Mirror是個(gè)單例的,只構(gòu)建了一個(gè)對(duì)象
Mirror mirror = new Mirror("魔鏡");
//三個(gè)線(xiàn)程都在用這面鏡子
MirrorThread thread1 = new MirrorThread(mirror,"張三");
MirrorThread thread2 = new MirrorThread(mirror,"李四");
MirrorThread thread3 = new MirrorThread(mirror,"王二");
thread1.start();
thread2.start();
thread3.start();
}
很好理解,創(chuàng)建了一面鏡子,3個(gè)人一起照鏡子。
MirrorThread:
public class MirrorThread extends Thread{
private Mirror mirror;
private String threadName;
public MirrorThread(Mirror mirror, String threadName){
this.mirror = mirror;
this.threadName = threadName;
}
//照鏡子
public String lookMirror() {
return threadName+" looks like "+ mirror.getNowLookLike().get();
}
//化妝
public void makeup(String makeupString) {
mirror.getNowLookLike().set(makeupString);
}
@Override
public void run() {
int i = 1;//閾值
while(i<5) {
try {
long nowFace = (long)(Math.random()*5000);
sleep(nowFace);
StringBuffer sb = new StringBuffer();
sb.append("第"+i+"輪從");
sb.append(lookMirror());
makeup(String.valueOf(nowFace));
sb.append("變?yōu)?);
sb.append(lookMirror());
System.out.println(sb);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
}
}
也很好理解,就是不斷的更新自己的外貌同時(shí)從鏡子里讀取自己的外貌。
重點(diǎn)是Mirror:
public class Mirror {
private String mirrorName;
//每個(gè)人要看到自己的樣子,所以這里要用ThreadLocal
private ThreadLocal<String> nowLookLike;
public Mirror(String mirrorName){
this.mirrorName=mirrorName;
nowLookLike = new ThreadLocal<String>();
}
public String getMirrorName() {
return mirrorName;
}
public ThreadLocal<String> getNowLookLike() {
return nowLookLike;
}
}
對(duì)每個(gè)人長(zhǎng)的樣子用ThreadLocal類(lèi)型來(lái)表示。
先看測(cè)試結(jié)果:
第1輪從張三 looks like null變?yōu)閺埲?looks like 3008
第2輪從張三 looks like 3008變?yōu)閺埲?looks like 490
第1輪從王二 looks like null變?yōu)橥醵?looks like 3982
第1輪從李四 looks like null變?yōu)槔钏?looks like 4390
第2輪從王二 looks like 3982變?yōu)橥醵?looks like 1415
第2輪從李四 looks like 4390變?yōu)槔钏?looks like 1255
第3輪從王二 looks like 1415變?yōu)橥醵?looks like 758
第3輪從張三 looks like 490變?yōu)閺埲?looks like 2746
第3輪從李四 looks like 1255變?yōu)槔钏?looks like 845
第4輪從李四 looks like 845變?yōu)槔钏?looks like 1123
第4輪從張三 looks like 2746變?yōu)閺埲?looks like 2126
第4輪從王二 looks like 758變?yōu)橥醵?looks like 4516
OK,一面鏡子所有人一起照,而且每個(gè)人都只能看的到自己的變化,這就達(dá)成了單例線(xiàn)程安全的目的。
我們來(lái)細(xì)看下它是怎么實(shí)現(xiàn)的。
先來(lái)看Thread:
Thread中維護(hù)了一個(gè)ThreadLocal.ThreadLocalMapthreadLocals = null; ThreadLocalMap這個(gè)Map的key是ThreadLocal,value是維護(hù)的成員變量?,F(xiàn)在的跟蹤鏈?zhǔn)荰hread->ThreadLocalMap-><ThreadLocal,Object>,那么我們只要搞明白Thread怎么跟ThreadLocal關(guān)聯(lián)的,從線(xiàn)程里找到自己關(guān)心的成員變量的快照這條線(xiàn)就通了。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
再來(lái)看ThreadLocal:它里面核心方法兩個(gè)get()和set(T)
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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
方法里通過(guò)Thread.currentThread()的方法得到當(dāng)前線(xiàn)程,然后做為key存儲(chǔ)到當(dāng)前線(xiàn)程對(duì)象的threadLocals中,也就是TreadLocalMap中。
OK,這樣整個(gè)關(guān)系鏈已經(jīng)建立,真正要去訪(fǎng)問(wèn)的成員變量在一個(gè)map中,key是線(xiàn)程號(hào),值是屬于該線(xiàn)程的快照。
ThreadLocal里還有map的創(chuàng)建createMap(t, value)、取值時(shí)對(duì)象的初始值setInitialValue()、線(xiàn)程結(jié)束時(shí)對(duì)象的釋放remove()等細(xì)節(jié),有興趣的可以繼續(xù)跟進(jìn)了解下。
ThreadLocal應(yīng)用其實(shí)很多,例如Spring容器中實(shí)例默認(rèn)是單例的,transactionManager也一樣,那么事務(wù)在處理時(shí)單例的manager是如何控制每個(gè)線(xiàn)程的事務(wù)要如何處理呢,這里面就應(yīng)用了大量的ThreadLocal。
多線(xiàn)程中的ThreadLocal
1.ThreadLocal概述
多線(xiàn)程的并發(fā)問(wèn)題主要存在于多個(gè)線(xiàn)程對(duì)于同一個(gè)變量進(jìn)行修改產(chǎn)生的數(shù)據(jù)不一致的問(wèn)題,同一個(gè)變量指的值同一個(gè)對(duì)象的成員變量或者是同一個(gè)類(lèi)的靜態(tài)變量。之前我們常聽(tīng)過(guò)盡量不要使用靜態(tài)變量,會(huì)引起并發(fā)問(wèn)題,那么隨著Spring框架的深入人心,單例中的成員變量也出現(xiàn)了多線(xiàn)程并發(fā)問(wèn)題。Struts2接受參數(shù)采用成員變量自動(dòng)封裝,為此在Spring的配置采用多例模式,而SpringMVC將Spring的容器化發(fā)揮到極致,將接受的參數(shù)放到了注解和方法的參數(shù)中,從而避免了單例出現(xiàn)的線(xiàn)程問(wèn)題。今天,我們討論的是JDK從1.2就出現(xiàn)的一個(gè)并發(fā)工具類(lèi)ThreadLocal,他除了加鎖這種同步方式之外的另一種保證一種規(guī)避多線(xiàn)程訪(fǎng)問(wèn)出現(xiàn)線(xiàn)程不安全的方法,當(dāng)我們?cè)趧?chuàng)建一個(gè)變量后,如果每個(gè)線(xiàn)程對(duì)其進(jìn)行訪(fǎng)問(wèn)的時(shí)候訪(fǎng)問(wèn)的都是線(xiàn)程自己的變量這樣就不會(huì)存在線(xiàn)程不安全問(wèn)題。我們先看一下官方是怎么解釋這個(gè)變量的?


大致意思是:此類(lèi)提供了局部變量表。這些變量與普通變量不同不同之處是,每一個(gè)通過(guò)get或者set方法訪(fǎng)問(wèn)一個(gè)線(xiàn)程都是他自己的,將變量的副本獨(dú)立初始化。ThreadLocal實(shí)例通常作用于希望將狀態(tài)與線(xiàn)程關(guān)聯(lián)的類(lèi)中的私有靜態(tài)字段(例如,用戶(hù)ID或交易ID)。
只要線(xiàn)程是活動(dòng)的并且可以訪(fǎng)問(wèn){@code ThreadLocal}實(shí)例, 每個(gè)線(xiàn)程都會(huì)對(duì)其線(xiàn)程局部變量的副本保留隱式引用。 線(xiàn)程消失后,其線(xiàn)程本地實(shí)例的所有副本都將進(jìn)行垃圾回收(除非存在對(duì)這些副本的其他引用)。也就是說(shuō),如果創(chuàng)建一個(gè)ThreadLocal變量,那么訪(fǎng)問(wèn)這個(gè)變量的每個(gè)線(xiàn)程都會(huì)有這個(gè)變量的一個(gè)副本,在實(shí)際多線(xiàn)程操作的時(shí)候,操作的是自己本地內(nèi)存中的變量,從而規(guī)避了線(xiàn)程安全問(wèn)題。而每個(gè)線(xiàn)程的副本全部放到ThreadLocalMap中。
2. ThreadLocal簡(jiǎn)單實(shí)用
public class ThreadLocalExample {
public static class MyRunnable implements Runnable {
private ThreadLocal<Double> threadLocal = new ThreadLocal();
private Double variable;
@Override
public void run() {
threadLocal.set(Math.floor(Math.random() * 100D));
variable = Math.floor(Math.random() * 100D);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println("ThreadValue==>"+threadLocal.get());
System.out.println("Variable==>"+variable);
}
}
public static void main(String[] args) {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);
thread1.start();
thread2.start();
}
}

通過(guò)上面的例子,我們發(fā)現(xiàn)將Double放入ThreadLocal中,不會(huì)出現(xiàn)多線(xiàn)程并發(fā)問(wèn)題,而成員變量variable卻發(fā)生了多線(xiàn)程并發(fā)問(wèn)題。
3.ThreadLocal的內(nèi)部原理
通過(guò)源碼我們發(fā)現(xiàn)ThreadLocal主要提供了下面五個(gè)方法:
/**
* 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.
*
* 返回此線(xiàn)程局部變量的當(dāng)前線(xiàn)程副本中的值。
* 如果該變量沒(méi)有當(dāng)前線(xiàn)程的值,則首先將其初始化為調(diào)用{@link #initialValue}方法返回的值。
* @return the current thread's value of this thread-local
*/
public T get() { }
/**
* 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.
*
* 將此線(xiàn)程局部變量的當(dāng)前線(xiàn)程副本設(shè)置為指定值。
* 大多數(shù)子類(lèi)將不需要重寫(xiě)此方法,而僅依靠{@link #initialValue}方法來(lái)設(shè)置線(xiàn)程局部變量的值。
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
* 要存儲(chǔ)在此本地線(xiàn)程的當(dāng)前線(xiàn)程副本中的值。
*/
public void set(T value) { }
/**
* 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.
* 刪除此線(xiàn)程局部變量的當(dāng)前線(xiàn)程值。
* 如果此線(xiàn)程局部變量隨后被當(dāng)前線(xiàn)程{@linkplain #get read}調(diào)用,
* 則其值將通過(guò)調(diào)用其{@link #initialValue}方法來(lái)重新初始化,
* 除非當(dāng)前值是在此期間被設(shè)置{@linkplain #set set}。
* 這可能會(huì)導(dǎo)致在當(dāng)前線(xiàn)程中多次調(diào)用{@code initialValue}方法。
* @since 1.5
*/
public void remove() { }
/**
* Returns the current thread's "initial value" for this
* thread-local variable. This method will be invoked the first
* time a thread accesses the variable with the {@link #get}
* method, unless the thread previously invoked the {@link #set}
* method, in which case the {@code initialValue} method will not
* be invoked for the thread. Normally, this method is invoked at
* most once per thread, but it may be invoked again in case of
* subsequent invocations of {@link #remove} followed by {@link #get}.
* 返回此線(xiàn)程局部變量的當(dāng)前線(xiàn)程的“初始值”。
* 除非線(xiàn)程先前調(diào)用了{(lán)@link #set}方法,
* 否則線(xiàn)程第一次使用{@link #get}方法訪(fǎng)問(wèn)該變量時(shí)將調(diào)用此方法,
* 在這種情況下,{@ code initialValue}方法將不會(huì)為線(xiàn)程被調(diào)用。
* 通常,每個(gè)線(xiàn)程最多調(diào)用一次此方法,
* 但是在隨后調(diào)用{@link #remove}之后再調(diào)用{@link #get}的情況下,可以再次調(diào)用此方法。
*
* <p>This implementation simply returns {@code null}; if the
* programmer desires thread-local variables to have an initial
* value other than {@code null}, {@code ThreadLocal} must be
* subclassed, and this method overridden. Typically, an
* anonymous inner class will be used.
* 此實(shí)現(xiàn)僅返回{@code null};如果程序員希望線(xiàn)程局部變量的初始值不是{@code null},
* 則必須將{@code ThreadLocal}子類(lèi)化,并重寫(xiě)此方法。通常,將使用匿名內(nèi)部類(lèi)。
*
* @return the initial value for this thread-local
*/
protected T initialValue(){ }
3.1 get方法
public T get() {
//獲取當(dāng)前線(xiàn)程
Thread t = Thread.currentThread();
//通過(guò)當(dāng)前線(xiàn)程獲取ThreadLocalMap
//Thread類(lèi)中包含一個(gè)ThreadLocalMap的成員變量
ThreadLocalMap map = getMap(t);
//如果不為空,則通過(guò)ThreadLocalMap中獲取對(duì)應(yīng)value值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果為空,需要初始化值
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//如果為空,則創(chuàng)建
createMap(t, value);
return value;
}
首先是取得當(dāng)前線(xiàn)程,然后通過(guò)getMap(t)方法獲取到一個(gè)map,map的類(lèi)型為T(mén)hreadLocalMap。然后接著下面獲取到<key,value>鍵值對(duì),注意這里獲取鍵值對(duì)傳進(jìn)去的是 this,而不是當(dāng)前線(xiàn)程t。 如果獲取成功,則返回value值。如果map為空,則調(diào)用setInitialValue方法返回value。
在setInitialValue方法中,首先執(zhí)行了initialValue方法(我們上面提到的最后一個(gè)方法),接著通過(guò)當(dāng)前線(xiàn)程獲取ThreadLocalMap,如果不存在則創(chuàng)建。創(chuàng)建的代碼很簡(jiǎn)單,只是通過(guò)ThreadLocal對(duì)象和設(shè)置的Value值創(chuàng)建ThreadLocalMap對(duì)象。
3.2 set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
這個(gè)方法和setInitialValue方法的業(yè)務(wù)邏輯基本相同,只不過(guò)setInitialValue調(diào)用了initialValue()的鉤子方法。這里代碼簡(jiǎn)單,我們就不做過(guò)多解釋。
3.3 remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
這個(gè)方法是從jdk1.5才出現(xiàn)的。處理邏輯也很很簡(jiǎn)單。通過(guò)當(dāng)前線(xiàn)程獲取到ThreadLocalMap對(duì)象,然后移除此ThreadLocal。
3.4 initialValue方法
protected T initialValue() {
return null;
}
是不是感覺(jué)簡(jiǎn)單了,什么也沒(méi)有處理,直接返回一個(gè)null,那么何必如此設(shè)計(jì)呢?當(dāng)我們發(fā)現(xiàn)他的修飾符就會(huì)發(fā)現(xiàn),他應(yīng)該是一個(gè)鉤子方法,主要用于提供子類(lèi)實(shí)現(xiàn)的。追溯到源碼中我們發(fā)現(xiàn),Supplier的影子,這就是和jdk8的lamda表達(dá)式關(guān)聯(lián)上了。
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
4. 總結(jié)
在每個(gè)線(xiàn)程Thread內(nèi)部有一個(gè)ThreadLocal.ThreadLocalMap類(lèi)型的成員變量threadLocals,這個(gè)threadLocals就是用來(lái)存儲(chǔ)實(shí)際的變量副本的,鍵值為當(dāng)前ThreadLocal變量,value為變量副本(即T類(lèi)型的變量)。之所以這里是一個(gè)map,是因?yàn)橥ㄟ^(guò)線(xiàn)程會(huì)存在多個(gè)類(lèi)中定義ThreadLocal的成員變量。初始時(shí),在Thread里面,threadLocals為空,當(dāng)通過(guò)ThreadLocal變量調(diào)用get()方法或者set()方法,就會(huì)對(duì)Thread類(lèi)中的threadLocals進(jìn)行初始化,并且以當(dāng)前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals; 然后在當(dāng)前線(xiàn)程里面,如果要使用副本變量,就可以通過(guò)get方法在threadLocals里面查找。
5. ThreadLocalMap引發(fā)的內(nèi)存泄漏
ThreadLocal屬于一個(gè)工具類(lèi),他為用戶(hù)提供get、set、remove接口操作實(shí)際存放本地變量的threadLocals(調(diào)用線(xiàn)程的成員變量),也知道threadLocals是一個(gè)ThreadLocalMap類(lèi)型的變量。下面我們來(lái)看看ThreadLocalMap這個(gè)類(lèi)的一個(gè)entry:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object val
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
public WeakReference(T referent) {
super(referent); //referent:ThreadLocal的引用
}
//Reference構(gòu)造方法
Reference(T referent) {
this(referent, null);//referent:ThreadLocal的引用
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
在上面的代碼中,我們可以看出,當(dāng)前ThreadLocal的引用k被傳遞給WeakReference的構(gòu)造函數(shù),所以ThreadLocalMap中的key為T(mén)hreadLocal的弱引用。當(dāng)一個(gè)線(xiàn)程調(diào)用ThreadLocal的set方法設(shè)置變量的時(shí)候,當(dāng)前線(xiàn)程的ThreadLocalMap就會(huì)存放一個(gè)記錄,這個(gè)記錄的key值為T(mén)hreadLocal的弱引用,value就是通過(guò)set設(shè)置的值。如果當(dāng)前線(xiàn)程一直存在且沒(méi)有調(diào)用該ThreadLocal的remove方法,如果這個(gè)時(shí)候別的地方還有對(duì)ThreadLocal的引用,那么當(dāng)前線(xiàn)程中的ThreadLocalMap中會(huì)存在對(duì)ThreadLocal變量的引用和value對(duì)象的引用,是不會(huì)釋放的,就會(huì)造成內(nèi)存泄漏。
考慮這個(gè)ThreadLocal變量沒(méi)有其他強(qiáng)依賴(lài),如果當(dāng)前線(xiàn)程還存在,由于線(xiàn)程的ThreadLocalMap里面的key是弱引用,所以當(dāng)前線(xiàn)程的ThreadLocalMap里面的ThreadLocal變量的弱引用在gc的時(shí)候就被回收,但是對(duì)應(yīng)的value還是存在的這就可能造成內(nèi)存泄漏(因?yàn)檫@個(gè)時(shí)候ThreadLocalMap會(huì)存在key為null但是value不為null的entry項(xiàng))。
總結(jié):THreadLocalMap中的Entry的key使用的是ThreadLocal對(duì)象的弱引用,在沒(méi)有其他地方對(duì)ThreadLoca依賴(lài),ThreadLocalMap中的ThreadLocal對(duì)象就會(huì)被回收掉,但是對(duì)應(yīng)的不會(huì)被回收,這個(gè)時(shí)候Map中就可能存在key為null但是value不為null的項(xiàng),這需要實(shí)際的時(shí)候使用完畢及時(shí)調(diào)用remove方法避免內(nèi)存泄漏。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
自定義JmsListenerContainerFactory時(shí),containerFactory字段解讀
這篇文章主要介紹了自定義JmsListenerContainerFactory時(shí),containerFactory字段解讀,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
Java Socket實(shí)現(xiàn)聊天室附1500行源代碼
Socket是應(yīng)用層與TCP/IP協(xié)議族通信的中間軟件抽象層,它是一組接口。本篇文章手把手帶你通過(guò)Java Socket來(lái)實(shí)現(xiàn)自己的聊天室,大家可以在過(guò)程中查缺補(bǔ)漏,溫故而知新2021-10-10
淺談shiro的SecurityManager類(lèi)結(jié)構(gòu)
下面小編就為大家?guī)?lái)一篇淺談shiro的SecurityManager類(lèi)結(jié)構(gòu)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07
SpringSecurity授權(quán)機(jī)制的實(shí)現(xiàn)(AccessDecisionManager與投票決策)
本文主要介紹了SpringSecurity授權(quán)機(jī)制的實(shí)現(xiàn),其核心是AccessDecisionManager和投票系統(tǒng),下面就來(lái)介紹一下,感興趣的可以了解一下2025-04-04
java無(wú)鎖hashmap原理與實(shí)現(xiàn)詳解
本文主要介紹了java無(wú)鎖hashmap原理與實(shí)現(xiàn),大家參考使用吧2014-01-01

