国产片侵犯亲女视频播放_亚洲精品二区_在线免费国产视频_欧美精品一区二区三区在线_少妇久久久_在线观看av不卡

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - 詳談ThreadLocal-單例模式下高并發線程安全

詳談ThreadLocal-單例模式下高并發線程安全

2021-12-20 14:19牛麥康納 Java教程

這篇文章主要介紹了ThreadLocal-單例模式下高并發線程安全,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教

ThreadLocal-單例模式下高并發線程安全

在多例的情況下,每個對象在堆中聲明內存空間,多線程對應的Java棧中的句柄或指針指向堆中不同的對象,對象各自變量的變更只會印象到對應的棧,也就是對應的線程中,不會影響到其它線程。所以多例的情況下不需要考慮線程安全的問題,因為一定是安全的。

詳談ThreadLocal-單例模式下高并發線程安全

而在單例的情況下卻完全不一樣了,在堆中只有一個對象,多線程對應的Java棧中的句柄或指針指向同一個對象,方法的參數變量和方法內變量是線程安全的,因為每執行一個方法,都會在獨立的空間創建局部變量,它不是共享的資源。但是成員變量只有一份,所有指向堆中該對象的句柄或指針都可以隨時修改和讀取它,所以是非線程安全的。

詳談ThreadLocal-單例模式下高并發線程安全

為了解決線程安全的問題,我們有3個思路:

  • 第一每個線程獨享自己的操作對象,也就是多例,多例勢必會帶來堆內存占用、頻繁GC、對象初始化性能開銷等待等一些列問題。
  • 第二單例模式枷鎖,典型的案例是HashTable和HashMap,對讀取和變更的操作用synchronized限制起來,保證同一時間只有一個線程可以操作該對象。雖然解決了內存、回收、構造、初始化等問題,但是勢必會因為鎖競爭帶來高并發下性能的下降。
  • 第三個思路就是今天重點推出的ThreadLocal。單例模式下通過某種機制維護成員變量不同線程的版本。
  • 假設三個人想從鏡子中看自己,第一個方案就是每人發一個鏡子互不干擾,第二個方案就是只有一個鏡子,一個人站在鏡子前其他人要排隊等候,第三個方案就是我這里發明了一種“魔鏡”,所有人站在鏡子前可以并且只能看到自己?。?!

主程序:

public static void main(String[] args) {
		//Mirror是個單例的,只構建了一個對象
		Mirror mirror = new Mirror("魔鏡");
		
		//三個線程都在用這面鏡子
		MirrorThread thread1 = new MirrorThread(mirror,"張三");
		MirrorThread thread2 = new MirrorThread(mirror,"李四");
		MirrorThread thread3 = new MirrorThread(mirror,"王二");
		
		thread1.start();
		thread2.start();
		thread3.start();
	}

很好理解,創建了一面鏡子,3個人一起照鏡子。

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("變為");
				sb.append(lookMirror());
				System.out.println(sb);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			i++;
		}
	}
}

也很好理解,就是不斷的更新自己的外貌同時從鏡子里讀取自己的外貌。

重點是Mirror:

public class Mirror {
	private String mirrorName;	
	//每個人要看到自己的樣子,所以這里要用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;
	}
}

對每個人長的樣子用ThreadLocal類型來表示。

先看測試結果:

第1輪從張三 looks like null變為張三 looks like 3008

第2輪從張三 looks like 3008變為張三 looks like 490

第1輪從王二 looks like null變為王二 looks like 3982

第1輪從李四 looks like null變為李四 looks like 4390

第2輪從王二 looks like 3982變為王二 looks like 1415

第2輪從李四 looks like 4390變為李四 looks like 1255

第3輪從王二 looks like 1415變為王二 looks like 758

第3輪從張三 looks like 490變為張三 looks like 2746

第3輪從李四 looks like 1255變為李四 looks like 845

第4輪從李四 looks like 845變為李四 looks like 1123

第4輪從張三 looks like 2746變為張三 looks like 2126

第4輪從王二 looks like 758變為王二 looks like 4516

OK,一面鏡子所有人一起照,而且每個人都只能看的到自己的變化,這就達成了單例線程安全的目的。

我們來細看下它是怎么實現的。

先來看Thread:

Thread中維護了一個ThreadLocal.ThreadLocalMapthreadLocals = null; ThreadLocalMap這個Map的key是ThreadLocal,value是維護的成員變量。現在的跟蹤鏈是Thread->ThreadLocalMap-><ThreadLocal,Object>,那么我們只要搞明白Thread怎么跟ThreadLocal關聯的,從線程里找到自己關心的成員變量的快照這條線就通了。

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);
      }

再來看ThreadLocal:它里面核心方法兩個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);
  }

方法里通過Thread.currentThread()的方法得到當前線程,然后做為key存儲到當前線程對象的threadLocals中,也就是TreadLocalMap中。

OK,這樣整個關系鏈已經建立,真正要去訪問的成員變量在一個map中,key是線程號,值是屬于該線程的快照。

ThreadLocal里還有map的創建createMap(t, value)、取值時對象的初始值setInitialValue()、線程結束時對象的釋放remove()等細節,有興趣的可以繼續跟進了解下。

ThreadLocal應用其實很多,例如Spring容器中實例默認是單例的,transactionManager也一樣,那么事務在處理時單例的manager是如何控制每個線程的事務要如何處理呢,這里面就應用了大量的ThreadLocal。

 

多線程中的ThreadLocal

1.ThreadLocal概述

多線程的并發問題主要存在于多個線程對于同一個變量進行修改產生的數據不一致的問題,同一個變量指的值同一個對象的成員變量或者是同一個類的靜態變量。之前我們常聽過盡量不要使用靜態變量,會引起并發問題,那么隨著Spring框架的深入人心,單例中的成員變量也出現了多線程并發問題。Struts2接受參數采用成員變量自動封裝,為此在Spring的配置采用多例模式,而SpringMVC將Spring的容器化發揮到極致,將接受的參數放到了注解和方法的參數中,從而避免了單例出現的線程問題。今天,我們討論的是JDK從1.2就出現的一個并發工具類ThreadLocal,他除了加鎖這種同步方式之外的另一種保證一種規避多線程訪問出現線程不安全的方法,當我們在創建一個變量后,如果每個線程對其進行訪問的時候訪問的都是線程自己的變量這樣就不會存在線程不安全問題。我們先看一下官方是怎么解釋這個變量的?

詳談ThreadLocal-單例模式下高并發線程安全

詳談ThreadLocal-單例模式下高并發線程安全

大致意思是:此類提供了局部變量表。這些變量與普通變量不同不同之處是,每一個通過get或者set方法訪問一個線程都是他自己的,將變量的副本獨立初始化。ThreadLocal實例通常作用于希望將狀態與線程關聯的類中的私有靜態字段(例如,用戶ID或交易ID)。

只要線程是活動的并且可以訪問{@code ThreadLocal}實例, 每個線程都會對其線程局部變量的副本保留隱式引用。 線程消失后,其線程本地實例的所有副本都將進行垃圾回收(除非存在對這些副本的其他引用)。也就是說,如果創建一個ThreadLocal變量,那么訪問這個變量的每個線程都會有這個變量的一個副本,在實際多線程操作的時候,操作的是自己本地內存中的變量,從而規避了線程安全問題。而每個線程的副本全部放到ThreadLocalMap中。

2. ThreadLocal簡單實用

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();
  } 
}

詳談ThreadLocal-單例模式下高并發線程安全

通過上面的例子,我們發現將Double放入ThreadLocal中,不會出現多線程并發問題,而成員變量variable卻發生了多線程并發問題。

3.ThreadLocal的內部原理

通過源碼我們發現ThreadLocal主要提供了下面五個方法:

/**
* 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.
* 
* 返回此線程局部變量的當前線程副本中的值。
* 如果該變量沒有當前線程的值,則首先將其初始化為調用{@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.
*
* 將此線程局部變量的當前線程副本設置為指定值。
* 大多數子類將不需要重寫此方法,而僅依靠{@link #initialValue}方法來設置線程局部變量的值。
*
* @param value the value to be stored in the current thread's copy of
*              this thread-local.
*              要存儲在此本地線程的當前線程副本中的值。
*/
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.
* 刪除此線程局部變量的當前線程值。
* 如果此線程局部變量隨后被當前線程{@linkplain #get read}調用,
* 則其值將通過調用其{@link #initialValue}方法來重新初始化,
* 除非當前值是在此期間被設置{@linkplain #set set}。
* 這可能會導致在當前線程中多次調用{@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}.
* 返回此線程局部變量的當前線程的“初始值”。
* 除非線程先前調用了{@link #set}方法,
* 否則線程第一次使用{@link #get}方法訪問該變量時將調用此方法,
* 在這種情況下,{@ code initialValue}方法將不會為線程被調用。
* 通常,每個線程最多調用一次此方法,
* 但是在隨后調用{@link #remove}之后再調用{@link #get}的情況下,可以再次調用此方法。
*
* <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.
* 此實現僅返回{@code null};如果程序員希望線程局部變量的初始值不是{@code null},
* 則必須將{@code ThreadLocal}子類化,并重寫此方法。通常,將使用匿名內部類。
*
* @return the initial value for this thread-local
*/
protected T initialValue(){ }

3.1 get方法

public T get() {
  //獲取當前線程
  Thread t = Thread.currentThread();
  //通過當前線程獲取ThreadLocalMap
  //Thread類中包含一個ThreadLocalMap的成員變量
  ThreadLocalMap map = getMap(t);
  //如果不為空,則通過ThreadLocalMap中獲取對應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
      //如果為空,則創建
      createMap(t, value);
  return value;
}

首先是取得當前線程,然后通過getMap(t)方法獲取到一個map,map的類型為ThreadLocalMap。然后接著下面獲取到<key,value>鍵值對,注意這里獲取鍵值對傳進去的是 this,而不是當前線程t。 如果獲取成功,則返回value值。如果map為空,則調用setInitialValue方法返回value。

在setInitialValue方法中,首先執行了initialValue方法(我們上面提到的最后一個方法),接著通過當前線程獲取ThreadLocalMap,如果不存在則創建。創建的代碼很簡單,只是通過ThreadLocal對象和設置的Value值創建ThreadLocalMap對象。

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);
}

這個方法和setInitialValue方法的業務邏輯基本相同,只不過setInitialValue調用了initialValue()的鉤子方法。這里代碼簡單,我們就不做過多解釋。

3.3 remove方法

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

這個方法是從jdk1.5才出現的。處理邏輯也很很簡單。通過當前線程獲取到ThreadLocalMap對象,然后移除此ThreadLocal。

3.4 initialValue方法

protected T initialValue() {
  return null;
}

是不是感覺簡單了,什么也沒有處理,直接返回一個null,那么何必如此設計呢?當我們發現他的修飾符就會發現,他應該是一個鉤子方法,主要用于提供子類實現的。追溯到源碼中我們發現,Supplier的影子,這就是和jdk8的lamda表達式關聯上了。

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. 總結

在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值為當前ThreadLocal變量,value為變量副本(即T類型的變量)。之所以這里是一個map,是因為通過線程會存在多個類中定義ThreadLocal的成員變量。初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,并且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals; 然后在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。

5. ThreadLocalMap引發的內存泄漏

ThreadLocal屬于一個工具類,他為用戶提供get、set、remove接口操作實際存放本地變量的threadLocals(調用線程的成員變量),也知道threadLocals是一個ThreadLocalMap類型的變量。下面我們來看看ThreadLocalMap這個類的一個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構造方法     
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;
}

在上面的代碼中,我們可以看出,當前ThreadLocal的引用k被傳遞給WeakReference的構造函數,所以ThreadLocalMap中的key為ThreadLocal的弱引用。當一個線程調用ThreadLocal的set方法設置變量的時候,當前線程的ThreadLocalMap就會存放一個記錄,這個記錄的key值為ThreadLocal的弱引用,value就是通過set設置的值。如果當前線程一直存在且沒有調用該ThreadLocal的remove方法,如果這個時候別的地方還有對ThreadLocal的引用,那么當前線程中的ThreadLocalMap中會存在對ThreadLocal變量的引用和value對象的引用,是不會釋放的,就會造成內存泄漏。

考慮這個ThreadLocal變量沒有其他強依賴,如果當前線程還存在,由于線程的ThreadLocalMap里面的key是弱引用,所以當前線程的ThreadLocalMap里面的ThreadLocal變量的弱引用在gc的時候就被回收,但是對應的value還是存在的這就可能造成內存泄漏(因為這個時候ThreadLocalMap會存在key為null但是value不為null的entry項)。

總結:THreadLocalMap中的Entry的key使用的是ThreadLocal對象的弱引用,在沒有其他地方對ThreadLoca依賴,ThreadLocalMap中的ThreadLocal對象就會被回收掉,但是對應的不會被回收,這個時候Map中就可能存在key為null但是value不為null的項,這需要實際的時候使用完畢及時調用remove方法避免內存泄漏。

以上為個人經驗,希望能給大家一個參考,也希望大家多多支持服務器之家。

原文鏈接:https://yejingtao.blog.csdn.net/article/details/78806902

延伸 · 閱讀

精彩推薦
  • Java教程Java實現搶紅包功能

    Java實現搶紅包功能

    這篇文章主要為大家詳細介紹了Java實現搶紅包功能,采用多線程模擬多人同時搶紅包,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙...

    littleschemer13532021-05-16
  • Java教程升級IDEA后Lombok不能使用的解決方法

    升級IDEA后Lombok不能使用的解決方法

    最近看到提示IDEA提示升級,尋思已經有好久沒有升過級了。升級完畢重啟之后,突然發現好多錯誤,本文就來介紹一下如何解決,感興趣的可以了解一下...

    程序猿DD9332021-10-08
  • Java教程Java BufferWriter寫文件寫不進去或缺失數據的解決

    Java BufferWriter寫文件寫不進去或缺失數據的解決

    這篇文章主要介紹了Java BufferWriter寫文件寫不進去或缺失數據的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望...

    spcoder14552021-10-18
  • Java教程20個非常實用的Java程序代碼片段

    20個非常實用的Java程序代碼片段

    這篇文章主要為大家分享了20個非常實用的Java程序片段,對java開發項目有所幫助,感興趣的小伙伴們可以參考一下 ...

    lijiao5352020-04-06
  • Java教程小米推送Java代碼

    小米推送Java代碼

    今天小編就為大家分享一篇關于小米推送Java代碼,小編覺得內容挺不錯的,現在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧...

    富貴穩中求8032021-07-12
  • Java教程Java8中Stream使用的一個注意事項

    Java8中Stream使用的一個注意事項

    最近在工作中發現了對于集合操作轉換的神器,java8新特性 stream,但在使用中遇到了一個非常重要的注意點,所以這篇文章主要給大家介紹了關于Java8中S...

    阿杜7482021-02-04
  • Java教程Java使用SAX解析xml的示例

    Java使用SAX解析xml的示例

    這篇文章主要介紹了Java使用SAX解析xml的示例,幫助大家更好的理解和學習使用Java,感興趣的朋友可以了解下...

    大行者10067412021-08-30
  • Java教程xml與Java對象的轉換詳解

    xml與Java對象的轉換詳解

    這篇文章主要介紹了xml與Java對象的轉換詳解的相關資料,需要的朋友可以參考下...

    Java教程網2942020-09-17
主站蜘蛛池模板: 成年人免费看片 | 青青久视频 | 白浆一区| 欧美日韩视频在线第一区 | 中文字幕日韩av | 欧美91 | 成人h动漫在线看 | 美女视频一区 | 成人在线观看网站 | 国产中文字幕在线免费观看 | 国产一区影院 | 色站综合| 四虎免费紧急入口观看 | 亚洲精品专区 | 三区在线| 久久国产精品久久久久久 | 日韩精品一区二区三区中文字幕 | 成人在线免费小视频 | 色黄视频| 日韩美女国产精品 | 亚洲黄网在线观看 | 日韩精品一区二区三区在线 | 毛片在线网址 | 国产精品欧美久久久久久 | 午夜操操操 | 最新国产在线视频 | 91av国产视频 | 欧美另类国产 | 高清av电影| 日韩精品在线免费观看 | 亚洲国产精品久久 | 国产精品久久久久久久久久东京 | 91色在线观看 | 亚洲永久免费视频 | 午夜精品久久久久久久久 | 一区二区三区在线免费观看 | 91在线精品一区二区三区 | 久久久精品| 亚洲人成网站999久久久综合 | 91精品福利少妇午夜100集 | 欧美在线不卡视频 |