1. 先進行一次thread mp (jstack -m <pid> 或者 kill -3 <pid> , 或者使用jconsole, jvisualvm等) (jstack 命令有一些選項不是每個平台都支持的, jconsole jvisualvm都是有界面的, 如果你要運行一般需要配置agent或者重定向display到某台機器).
2. 然後過了一段時間再做一次, 如果發現同一個thread NID 還是停在同一個地方, 基本上可以懷疑是否掛住了(一般只需要查看你業務相關的stack信息就行了).
3. 還有一種就是你的日誌很詳細, 也可以看到一些的情況(列印到某個地方就卡住了, 呵呵).
B. javase線程怎麼存儲到容器
Java 並發重要知識點
java 線程池
ThreadPoolExecutor 類分析
ThreadPoolExecutor 類中提供的四個構造方法。我們來看最長的那個,其餘三個都是在這個構造方法的基礎上產生(其他幾個構造方法說白點都是給定某些默認參數的構造方法比如默認制定拒絕策略是什麼)。
/**
* 用給定的初始參數創建一個新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//線程池的核心線程數量
int maximumPoolSize,//線程池的最大線程數
long keepAliveTime,//當線程數大於核心線程數時,多餘的空閑線程存活的最長時間
TimeUnit unit,//時間單位
BlockingQueue<Runnable> workQueue,//任務隊列,用來儲存等待執行任務的隊列
ThreadFactory threadFactory,//線程工廠,用來創建線程,一般默認即可
RejectedExecutionHandler handler//拒絕策略,當提交的任務過多而不能及時處理時,我們可以定製策略來處理任務
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
下面這些對創建非常重要,在後面使用線程池的過程中你一定會用到!所以,務必拿著小本本記清楚。
ThreadPoolExecutor 3 個最重要的參數:
corePoolSize : 核心線程數線程數定義了最小可以同時運行的線程數量。
maximumPoolSize : 當隊列中存放的任務達到隊列容量的時候,當前可以同時運行的線程數量變為最大線程數。
workQueue: 當新任務來的時候會先判斷當前運行的線程數量是否達到核心線程數,如果達到的話,新任務就會被存放在隊列中。
ThreadPoolExecutor其他常見參數 :
keepAliveTime:當線程池中的線程數量大於 corePoolSize 的時候,如果這時沒有新的任務提交,核心線程外的線程不會立即銷毀,而是會等待,直到等待的時間超過了 keepAliveTime才會被回收銷毀;
unit : keepAliveTime 參數的時間單位。
threadFactory :executor 創建新線程的時候會用到。
handler :飽和策略。關於飽和策略下面單獨介紹一下。
下面這張圖可以加深你對線程池中各個參數的相互關系的理解(圖片來源:《Java 性能調優實戰》):
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-nzbqGRz9-1654600571133)(https://javaguide.cn/assets/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%90%84%E4%B8%AA%E5%8F%82%E6%95%B0%E4%B9%8B%E9%97%B4%E7%9A%84%E5%85%B3%E7%B3%BB.d65f3309.png)]
ThreadPoolExecutor 飽和策略定義:
如果當前同時運行的線程數量達到最大線程數量並且隊列也已經被放滿了任務時,ThreadPoolTaskExecutor 定義一些策略:
ThreadPoolExecutor.AbortPolicy :拋出 RejectedExecutionException來拒絕新任務的處理。
ThreadPoolExecutor.CallerRunsPolicy :調用執行自己的線程運行任務,也就是直接在調用execute方法的線程中運行(run)被拒絕的任務,如果執行程序已關閉,則會丟棄該任務。因此這種策略會降低對於新任務提交速度,影響程序的整體性能。如果您的應用程序可以承受此延遲並且你要求任何一個任務請求都要被執行的話,你可以選擇這個策略。
ThreadPoolExecutor.DiscardPolicy :不處理新任務,直接丟棄掉。
ThreadPoolExecutor.DiscardOldestPolicy : 此策略將丟棄最早的未處理的任務請求。
舉個例子:
Spring 通過 ThreadPoolTaskExecutor 或者我們直接通過 ThreadPoolExecutor 的構造函數創建線程池的時候,當我們不指定 RejectedExecutionHandler 飽和策略的話來配置線程池的時候默認使用的是 ThreadPoolExecutor.AbortPolicy。在默認情況下,ThreadPoolExecutor 將拋出 RejectedExecutionException 來拒絕新來的任務 ,這代表你將丟失對這個任務的處理。 對於可伸縮的應用程序,建議使用 ThreadPoolExecutor.CallerRunsPolicy。當最大池被填滿時,此策略為我們提供可伸縮隊列。(這個直接查看 ThreadPoolExecutor 的構造函數源碼就可以看出,比較簡單的原因,這里就不貼代碼了。)
推薦使用 ThreadPoolExecutor 構造函數創建線程池
在《阿里巴巴 Java 開發手冊》「並發處理」這一章節,明確指出線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程。
為什麼呢?
使用線程池的好處是減少在創建和銷毀線程上所消耗的時間以及系統資源開銷,解決資源不足的問題。如果不使用線程池,有可能會造成系統創建大量同類線程而導致消耗完內存或者「過度切換」的問題。
另外,《阿里巴巴 Java 開發手冊》中強制線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 構造函數的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險
Executors 返回線程池對象的弊端如下(後文會詳細介紹到):
FixedThreadPool 和 SingleThreadExecutor : 允許請求的隊列長度為 Integer.MAX_VALUE,可能堆積大量的請求,從而導致 OOM。
CachedThreadPool 和 ScheledThreadPool : 允許創建的線程數量為 Integer.MAX_VALUE ,可能會創建大量線程,從而導致 OOM。
方式一:通過ThreadPoolExecutor構造函數實現(推薦)通過構造方法實現
方式二:通過 Executor 框架的工具類 Executors 來實現 我們可以創建三種類型的 ThreadPoolExecutor:
FixedThreadPool
SingleThreadExecutor
CachedThreadPool
對應 Executors 工具類中的方法如圖所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-YGd4ygZu-1654600571136)(https://javaguide.cn/assets/Executors%E5%B7%A5%E5%85%B7%E7%B1%BB.4b0cbd16.png)]
正確配置線程池參數
說到如何給線程池配置參數,美團的騷操作至今讓我難忘(後面會提到)!
我們先來看一下各種書籍和博客上一般推薦的配置線程池參數的方式,可以作為參考!
常規操作
很多人甚至可能都會覺得把線程池配置過大一點比較好!我覺得這明顯是有問題的。就拿我們生活中非常常見的一例子來說:並不是人多就能把事情做好,增加了溝通交流成本。你本來一件事情只需要 3 個人做,你硬是拉來了 6 個人,會提升做事效率嘛?我想並不會。 線程數量過多的影響也是和我們分配多少人做事情一樣,對於多線程這個場景來說主要是增加了上下文切換成本。不清楚什麼是上下文切換的話,可以看我下面的介紹。
上下文切換:
多線程編程中一般線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執行,CPU 採取的策略是為每個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會重新處於就緒狀態讓給其他線程使用,這個過程就屬於一次上下文切換。概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換回這個任務時,可以再載入這個任務的狀態。任務從保存到再載入的過程就是一次上下文切換。
上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味著消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。
Linux 相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。
類比於實現世界中的人類通過合作做某件事情,我們可以肯定的一點是線程池大小設置過大或者過小都會有問題,合適的才是最好。
如果我們設置的線程池數量太小的話,如果同一時間有大量任務/請求需要處理,可能會導致大量的請求/任務在任務隊列中排隊等待執行,甚至會出現任務隊列滿了之後任務/請求無法處理的情況,或者大量任務堆積在任務隊列導致 OOM。這樣很明顯是有問題的! CPU 根本沒有得到充分利用。
但是,如果我們設置線程數量太大,大量線程可能會同時在爭取 CPU 資源,這樣會導致大量的上下文切換,從而增加線程的執行時間,影響了整體執行效率。
有一個簡單並且適用面比較廣的公式:
CPU 密集型任務(N+1): 這種任務消耗的主要是 CPU 資源,可以將線程數設置為 N(CPU 核心數)+1,比 CPU 核心數多出來的一個線程是為了防止線程偶發的缺頁中斷,或者其它原因導致的任務暫停而帶來的影響。一旦任務暫停,CPU 就會處於空閑狀態,而在這種情況下多出來的一個線程就可以充分利用 CPU 的空閑時間。
I/O 密集型任務(2N): 這種任務應用起來,系統會用大部分的時間來處理 I/O 交互,而線程在處理 I/O 的時間段內不會佔用 CPU 來處理,這時就可以將 CPU 交出給其它線程使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些線程,具體的計算方法是 2N。
如何判斷是 CPU 密集任務還是 IO 密集任務?
CPU 密集型簡單理解就是利用 CPU 計算能力的任務比如你在內存中對大量數據進行排序。但凡涉及到網路讀取,文件讀取這類都是 IO 密集型,這類任務的特點是 CPU 計算耗費時間相比於等待 IO 操作完成的時間來說很少,大部分時間都花在了等待 IO 操作完成上。
美團的騷操作
美團技術團隊在《Java線程池實現原理及其在美團業務中的實踐》open in new window這篇文章中介紹到對線程池參數實現可自定義配置的思路和方法。
美團技術團隊的思路是主要對線程池的核心參數實現自定義可配置。這三個核心參數是:
corePoolSize : 核心線程數線程數定義了最小可以同時運行的線程數量。
maximumPoolSize : 當隊列中存放的任務達到隊列容量的時候,當前可以同時運行的線程數量變為最大線程數。
workQueue: 當新任務來的時候會先判斷當前運行的線程數量是否達到核心線程數,如果達到的話,新任務就會被存放在隊列中。
為什麼是這三個參數?
我在這篇《新手也能看懂的線程池學習總結》open in new window 中就說過這三個參數是 ThreadPoolExecutor 最重要的參數,它們基本決定了線程池對於任務的處理策略。
如何支持參數動態配置? 且看 ThreadPoolExecutor 提供的下面這些方法。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Sm89qdJZ-1654600571137)(https://javaguide.cn/assets/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.5ff332dc.png)]
格外需要注意的是corePoolSize, 程序運行期間的時候,我們調用 setCorePoolSize() 這個方法的話,線程池會首先判斷當前工作線程數是否大於corePoolSize,如果大於的話就會回收工作線程。
另外,你也看到了上面並沒有動態指定隊列長度的方法,美團的方式是自定義了一個叫做 的隊列(主要就是把LinkedBlockingQueue的capacity 欄位的final關鍵字修飾給去掉了,讓它變為可變的)。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cmNN5yAL-1654600571138)(https://javaguide.cn/assets/19a0255a-6ef3-4835-98d1-a839d1983332.b334d1e9.png)]
還沒看夠?推薦 why神的[《如何設置線程池參數?美團給出了一個讓面試官虎軀一震的回答。》open in new window](如何設置線程池參數?美團給出了一個讓面試官虎軀一震的回答。 (qq.com))這篇文章,深度剖析,很不錯哦!
Java 常見並發容器
JDK 提供的這些容器大部分在 java.util.concurrent 包中。
ConcurrentHashMap : 線程安全的 HashMap
CopyOnWriteArrayList : 線程安全的 List,在讀多寫少的場合性能非常好,遠遠好於 Vector。
ConcurrentLinkedQueue : 高效的並發隊列,使用鏈表實現。可以看做一個線程安全的 LinkedList,這是一個非阻塞隊列。
BlockingQueue : 這是一個介面,JDK 內部通過鏈表、數組等方式實現了這個介面。表示阻塞隊列,非常適合用於作為數據共享的通道。
ConcurrentSkipListMap : 跳錶的實現。這是一個 Map,使用跳錶的數據結構進行快速查找。
ConcurrentHashMap
我們知道 HashMap 不是線程安全的,在並發場景下如果要保證一種可行的方式是使用 Collections.synchronizedMap() 方法來包裝我們的 HashMap。但這是通過使用一個全局的鎖來同步不同線程間的並發訪問,因此會帶來不可忽視的性能問題。
所以就有了 HashMap 的線程安全版本—— ConcurrentHashMap 的誕生。
在 ConcurrentHashMap 中,無論是讀操作還是寫操作都能保證很高的性能:在進行讀操作時(幾乎)不需要加鎖,而在寫操作時通過鎖分段技術只對所操作的段加鎖而不影響客戶端對其它段的訪問。
CopyOnWriteArrayList
CopyOnWriteArrayList 簡介
public class CopyOnWriteArrayList<E>
extends Object
implements List<E>, RandomAccess, Cloneable, Serializable
在很多應用場景中,讀操作可能會遠遠大於寫操作。由於讀操作根本不會修改原有的數據,因此對於每次讀取都進行加鎖其實是一種資源浪費。我們應該允許多個線程同時訪問 List 的內部數據,畢竟讀取操作是安全的。
這和我們之前在多線程章節講過 ReentrantReadWriteLock 讀寫鎖的思想非常類似,也就是讀讀共享、寫寫互斥、讀寫互斥、寫讀互斥。JDK 中提供了 CopyOnWriteArrayList 類比相比於在讀寫鎖的思想又更進一步。為了將讀取的性能發揮到極致,CopyOnWriteArrayList 讀取是完全不用加鎖的,並且更厲害的是:寫入也不會阻塞讀取操作。只有寫入和寫入之間需要進行同步等待。這樣一來,讀操作的性能就會大幅度提升。那它是怎麼做的呢?
CopyOnWriteArrayList 是如何做到的?
CopyOnWriteArrayList 類的所有可變操作(add,set 等等)都是通過創建底層數組的新副本來實現的。當 List 需要被修改的時候,我並不修改原有內容,而是對原有數據進行一次復制,將修改的內容寫入副本。寫完之後,再將修改完的副本替換原來的數據,這樣就可以保證寫操作不會影響讀操作了。
從 CopyOnWriteArrayList 的名字就能看出 CopyOnWriteArrayList 是滿足 CopyOnWrite 的。所謂 CopyOnWrite 也就是說:在計算機,如果你想要對一塊內存進行修改時,我們不在原有內存塊中進行寫操作,而是將內存拷貝一份,在新的內存中進行寫操作,寫完之後呢,就將指向原來內存指針指向新的內存,原來的內存就可以被回收掉了。
CopyOnWriteArrayList 讀取和寫入源碼簡單分析
CopyOnWriteArrayList 讀取操作的實現
讀取操作沒有任何同步控制和鎖操作,理由就是內部數組 array 不會發生修改,只會被另外一個 array 替換,因此可以保證數據安全。
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
public E get(int index) {
return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
final Object[] getArray() {
return array;
}
CopyOnWriteArrayList 寫入操作的實現
CopyOnWriteArrayList 寫入操作 add()方法在添加集合的時候加了鎖,保證了同步,避免了多線程寫的時候會 出多個副本出來。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();//加鎖
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.Of(elements, len + 1);//拷貝新數組
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();//釋放鎖
}
}
ConcurrentLinkedQueue
Java 提供的線程安全的 Queue 可以分為阻塞隊列和非阻塞隊列,其中阻塞隊列的典型例子是 BlockingQueue,非阻塞隊列的典型例子是 ConcurrentLinkedQueue,在實際應用中要根據實際需要選用阻塞隊列或者非阻塞隊列。 阻塞隊列可以通過加鎖來實現,非阻塞隊列可以通過 CAS 操作實現。
從名字可以看出,ConcurrentLinkedQueue這個隊列使用鏈表作為其數據結構.ConcurrentLinkedQueue 應該算是在高並發環境中性能最好的隊列了。它之所有能有很好的性能,是因為其內部復雜的實現。
ConcurrentLinkedQueue 內部代碼我們就不分析了,大家知道 ConcurrentLinkedQueue 主要使用 CAS 非阻塞演算法來實現線程安全就好了。
ConcurrentLinkedQueue 適合在對性能要求相對較高,同時對隊列的讀寫存在多個線程同時進行的場景,即如果對隊列加鎖的成本較高則適合使用無鎖的 ConcurrentLinkedQueue 來替代。
BlockingQueue
BlockingQueue 簡介
上面我們己經提到了 ConcurrentLinkedQueue 作為高性能的非阻塞隊列。下面我們要講到的是阻塞隊列——BlockingQueue。阻塞隊列(BlockingQueue)被廣泛使用在「生產者-消費者」問題中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。當隊列容器已滿,生產者線程會被阻塞,直到隊列未滿;當隊列容器為空時,消費者線程會被阻塞,直至隊列非空時為止。
BlockingQueue 是一個介面,繼承自 Queue,所以其實現類也可以作為 Queue 的實現來使用,而 Queue 又繼承自 Collection 介面。下面是 BlockingQueue 的相關實現類:
BlockingQueue 的實現類
下面主要介紹一下 3 個常見的 BlockingQueue 的實現類:ArrayBlockingQueue、LinkedBlockingQueue 、PriorityBlockingQueue 。
ArrayBlockingQueue
ArrayBlockingQueue 是 BlockingQueue 介面的有界隊列實現類,底層採用數組來實現。
public class ArrayBlockingQueue<E>
extends AbstractQueue<E>
implements BlockingQueue<E>, Serializable{}
ArrayBlockingQueue 一旦創建,容量不能改變。其並發控制採用可重入鎖 ReentrantLock ,不管是插入操作還是讀取操作,都需要獲取到鎖才能進行操作。當隊列容量滿時,嘗試將元素放入隊列將導致操作阻塞;嘗試從一個空隊列中取一個元素也會同樣阻塞。
ArrayBlockingQueue 默認情況下不能保證線程訪問隊列的公平性,所謂公平性是指嚴格按照線程等待的絕對時間順序,即最先等待的線程能夠最先訪問到 ArrayBlockingQueue。而非公平性則是指訪問 ArrayBlockingQueue 的順序不是遵守嚴格的時間順序,有可能存在,當 ArrayBlockingQueue 可以被訪問時,長時間阻塞的線程依然無法訪問到 ArrayBlockingQueue。如果保證公平性,通常會降低吞吐量。如果需要獲得公平性的 ArrayBlockingQueue,可採用如下代碼:
private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);
1
1
LinkedBlockingQueue
LinkedBlockingQueue 底層基於單向鏈表實現的阻塞隊列,可以當做無界隊列也可以當做有界隊列來使用,同樣滿足 FIFO 的特性,與 ArrayBlockingQueue 相比起來具有更高的吞吐量,為了防止 LinkedBlockingQueue 容量迅速增,損耗大量內存。通常在創建 LinkedBlockingQueue 對象時,會指定其大小,如果未指定,容量等於 Integer.MAX_VALUE 。
相關構造方法:
/**
*某種意義上的無界隊列
* Creates a {@code LinkedBlockingQueue} with a capacity of
* {@link Integer#MAX_VALUE}.
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/**
*有界隊列
* Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
*
* @param capacity the capacity of this queue
* @throws IllegalArgumentException if {@code capacity} is not greater
* than zero
*/
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
PriorityBlockingQueue
PriorityBlockingQueue 是一個支持優先順序的無界阻塞隊列。默認情況下元素採用自然順序進行排序,也可以通過自定義類實現 compareTo() 方法來指定元素排序規則,或者初始化時通過構造器參數 Comparator 來指定排序規則。
PriorityBlockingQueue 並發控制採用的是可重入鎖 ReentrantLock,隊列為無界隊列(ArrayBlockingQueue 是有界隊列,LinkedBlockingQueue 也可以通過在構造函數中傳入 capacity 指定隊列最大的容量,但是 PriorityBlockingQueue 只能指定初始的隊列大小,後面插入元素的時候,如果空間不夠的話會自動擴容)。
簡單地說,它就是 PriorityQueue 的線程安全版本。不可以插入 null 值,同時,插入隊列的對象必須是可比較大小的(comparable),否則報 ClassCastException 異常。它的插入操作 put 方法不會 block,因為它是無界隊列(take 方法在隊列為空的時候會阻塞)。
推薦文章: 《解讀 Java 並發隊列 BlockingQueue》open in new window
ConcurrentSkipListMap
下面這部分內容參考了極客時間專欄《數據結構與演算法之美》open in new window以及《實戰 Java 高並發程序設計》。
為了引出 ConcurrentSkipListMap,先帶著大家簡單理解一下跳錶。
對於一個單鏈表,即使鏈表是有序的,如果我們想要在其中查找某個數據,也只能從頭到尾遍歷鏈表,這樣效率自然就會很低,跳錶就不一樣了。跳錶是一種可以用來快速查找的數據結構,有點類似於平衡樹。它們都可以對元素進行快速的查找。但一個重要的區別是:對平衡樹的插入和刪除往往很可能導致平衡樹進行一次全局的調整。而對跳錶的插入和刪除只需要對整個數據結構的局部進行操作即可。這樣帶來的好處是:在高並發的情況下,你會需要一個全局鎖來保證整個平衡樹的線程安全。而對於跳錶,你只需要部分鎖即可。這樣,在高並發環境下,你就可以擁有更好的性能。而就查詢的性能而言,跳錶的時間復雜度也是 O(logn) 所以在並發數據結構中,JDK 使用跳錶來實現一個 Map。
跳錶的本質是同時維護了多個鏈表,並且鏈表是分層的,
2級索引跳錶
最低層的鏈表維護了跳錶內所有的元素,每上面一層鏈表都是下面一層的子集。
跳錶內的所有鏈表的元素都是排序的。查找時,可以從頂級鏈表開始找。一旦發現被查找的元素大於當前鏈表中的取值,就會轉入下一層鏈表繼續找。這也就是說在查找過程中,搜索是跳躍式的。如上圖所示,在跳錶中查找元素 18。
在跳錶中查找元素18
查找 18 的時候原來需要遍歷 18 次,現在只需要 7 次即可。針對鏈表長度比較大的時候,構建索引查找效率的提升就會非常明顯。
從上面很容易看出,跳錶是一種利用空間換時間的演算法。
使用跳錶實現 Map 和使用哈希演算法實現 Map 的另外一個不同之處是:哈希並不會保存元素的順序,而跳錶內所有的元素都是排序的。因此在對跳錶進行遍歷時,你會得到一個有序的結果。所以,如果你的應用需要有序性,那麼跳錶就是你不二的選擇。JDK 中實現這一數據結構的類是 ConcurrentSkipListMap。
C. 請教java的線程池是如何回收線程的
工作線程回收需要滿足三個條件:
1) 參數allowCoreThreadTimeOut為true
2) 該線程在keepAliveTime時間內獲取不到任務,即空閑這么長時間
3) 當前線程池大小 > 核心線程池大小corePoolSize。
D. Java線程是否會被垃圾回收
上面的常式運行結果是兩個線程在程序被強制終止之前一直運行。threadisrunning...threadisrunning...threadisrunning...ExecutedSystem.gc(),WeakReferencestillkeepThread[Thread-0,5,main]threadisrunning...threadisrunning...threadisrunning...ExecutedSystem.gc(),WeakReferencestillkeepThread[Thread-0,5,main]threadisrunning...threadisrunning...
運行中的線程是稱之為垃圾回收根對象的一種,不會被垃圾回收。當垃圾回收器判斷一個對象是否可達,總是使用垃圾回收根對象作為參考點。
例如,主線程並沒有被引用,但是不會被垃圾回收。
垃圾回收根對象是可在堆之外被訪問的對象。一個對象可由於下列原因成為GC根對象:SystemClass
由自舉/系統類載入器載入的類。例如,rt.jar中所有諸如java.util.*的類。
JNILocal
原生代碼中的本地變數,例如用戶定義的JNI代碼或JVM內部代碼。
JNIGlobal
原生代碼中的全局變數,例如用戶定義的JNI代碼或JVM內部代碼。
ThreadBlock
當前活躍的線程塊中引用的對象。
Thread
啟動且未停止的線程。
BusyMonitor
其wait()或notify()方法被調用,或被同步synchronized的對象。例如,通過調用synchronized(Object)或者進入其某個synchronized方法。靜態方法對應類,非靜態方法對應對象。
JavaLocal
本地變數。例如,仍在線程的棧中的方法輸入參數或本地創建的對象。
NativeStack
(例如用戶定義的JNI代碼或JVM內部代碼這樣的)原生代碼的入或出參數。通常發生在許多方法有原生部分,方法參數處理的對象成為GC根對象。例如,參數用於文件、網路I/O或反射。
Finalizer
在隊列中等待其finalizer運行的對象。
Unfinalized
擁有finalize方法,但是還沒有被終結且不在finalizer隊列的對象。
Unreachable
從其他根對象不可達的對象,但是被內存分析器標記為根對象。
Unknown
沒有根類型的對象。一些轉儲(mp),例如IBM可移植對轉儲文件,沒有根信息。對於這些轉儲,內存分析器解析程序將沒有被其他根對象引用的對象標記為此類根對象。參考
E. Java垃圾回收無效線程嗎
前面是我自己理解的後面是復制的java中垃圾回收以前聽老師講好像是內存滿了他才去做一次整體垃圾回收,在回收垃圾的同時會調用finalize方法.你在構造一個類時可以構造一個類時覆蓋他的finalize方法以便於該類在被垃圾回收時執行一些代碼,比如釋放資源.1.JVM的gc概述gc即垃圾收集機制是指jvm用於釋放那些不再使用的對象所佔用的內存。java語言並不要求jvm有gc,也沒有規定gc如何工作。不過常用的jvm都有gc,而且大多數gc都使用類似的演算法管理內存和執行收集操作。在充分理解了垃圾收集演算法和執行過程後,才能有效的優化它的性能。有些垃圾收集專用於特殊的應用程序。比如,實時應用程序主要是為了避免垃圾收集中斷,而大多數OLTP應用程序則注重整體效率。理解了應用程序的工作負荷和jvm支持的垃圾收集演算法,便可以進行優化配置垃圾收集器。垃圾收集的目的在於清除不再使用的對象。gc通過確定對象是否被活動對象引用來確定是否收集該對象。gc首先要判斷該對象是否是時候可以收集。兩種常用的方法是引用計數和對象引用遍歷。1.1.引用計數引用計數存儲對特定對象的所有引用數,也就是說,當應用程序創建引用以及引用超出范圍時,jvm必須適當增減引用數。當某對象的引用數為0時,便可以進行垃圾收集。1.2.對象引用遍歷早期的jvm使用引用計數,現在大多數jvm採用對象引用遍歷。對象引用遍歷從一組對象開始,沿著整個對象圖上的每條鏈接,遞歸確定可到達(reachable)的對象。如果某對象不能從這些根對象的一個(至少一個)到達,則將它作為垃圾收集。在對象遍歷階段,gc必須記住哪些對象可以到達,以便刪除不可到達的對象,這稱為標記(marking)對象。下一步,gc要刪除不可到達的對象。刪除時,有些gc只是簡單的掃描堆棧,刪除未標記的未標記的對象,並釋放它們的內存以生成新的對象,這叫做清除(sweeping)。這種方法的問題在於內存會分成好多小段,而它們不足以用於新的對象,但是組合起來卻很大。因此,許多gc可以重新組織內存中的對象,並進行壓縮(compact),形成可利用的空間。為此,gc需要停止其他的活動活動。這種方法意味著所有與應用程序相關的工作停止,只有gc運行。結果,在響應期間增減了許多混雜請求。另外,更復雜的gc不斷增加或同時運行以減少或者清除應用程序的中斷。有的gc使用單線程完成這項工作,有的則採用多線程以增加效率。2.幾種垃圾回收機制2.1.標記-清除收集器這種收集器首先遍歷對象圖並標記可到達的對象,然後掃描堆棧以尋找未標記對象並釋放它們的內存。這種收集器一般使用單線程工作並停止其他操作。2.2.標記-壓縮收集器有時也叫標記-清除-壓縮收集器,與標記-清除收集器有相同的標記階段。在第二階段,則把標記對象復制到堆棧的新域中以便壓縮堆棧。這種收集器也停止其他操作。2.3.復制收集器這種收集器將堆棧分為兩個域,常稱為半空間。每次僅使用一半的空間,jvm生成的新對象則放在另一半空間中。gc運行時,它把可到達對象復制到另一半空間,從而壓縮了堆棧。這種方法適用於短生存期的對象,持續復制長生存期的對象則導致效率降低。2.4.增量收集器增量收集器把堆棧分為多個域,每次僅從一個域收集垃圾。這會造成較小的應用程序中斷。2.5.分代收集器這種收集器把堆棧分為兩個或多個域,用以存放不同壽命的對象。jvm生成的新對象一般放在其中的某個域中。過一段時間,繼續存在的對象將獲得使用期並轉入更長壽命的域中。分代收集器對不同的域使用不同的演算法以優化性能。2.6.並發收集器並發收集器與應用程序同時運行。這些收集器在某點上(比如壓縮時)一般都不得不停止其他操作以完成特定的任務,但是因為其他應用程序可進行其他的後台操作,所以中斷其他處理的實際時間大大降低。2.7.並行收集器並行收集器使用某種傳統的演算法並使用多線程並行的執行它們的工作。在多cpu機器上使用多線程技術可以顯著的提高java應用程序的可擴展性。3.SunHotSpot1.4.1JVM堆大小的調整SunHotSpot1.4.1使用分代收集器,它把堆分為三個主要的域:新域、舊域以及永久域。Jvm生成的所有新對象放在新域中。一旦對象經歷了一定數量的垃圾收集循環後,便獲得使用期並進入舊域。在永久域中jvm則存儲class和method對象。就配置而言,永久域是一個獨立域並且不認為是堆的一部分。下面介紹如何控制這些域的大小。可使用-Xms和-Xmx控制整個堆的原始大小或最大值。下面的命令是把初始大小設置為128M:java–Xms128m–Xmx256m為控制新域的大小,可使用-XX:NewRatio設置新域在堆中所佔的比例。下面的命令把整個堆設置成128m,新域比率設置成3,即新域與舊域比例為1:3,新域為堆的1/4或32M:java–Xms128m–Xmx128m–XX:NewRatio=3可使用-XX:NewSize和-XX:MaxNewsize設置新域的初始值和最大值。下面的命令把新域的初始值和最大值設置成64m:java–Xms256m–Xmx256m–Xmn64m永久域默認大小為4m。運行程序時,jvm會調整永久域的大小以滿足需要。每次調整時,jvm會對堆進行一次完全的垃圾收集。使用-XX:MaxPerSize標志來增加永久域搭大小。在WebLogicServer應用程序載入較多類時,經常需要增加永久域的最大值。當jvm載入類時,永久域中的對象急劇增加,從而使jvm不斷調整永久域大小。為了避免調整,可使用-XX:PerSize標志設置初始值。下面把永久域初始值設置成32m,最大值設置成64m。java-Xms512m-Xmx512m-Xmn128m-XX:PermSize=32m-XX:MaxPermSize=64m默認狀態下,HotSpot在新域中使用復制收集器。該域一般分為三個部分。第一部分為Eden,用於生成新的對象。另兩部分稱為救助空間,當Eden充滿時,收集器停止應用程序,把所有可到達對象復制到當前的from救助空間,一旦當前的from救助空間充滿,收集器則把可到達對象復制到當前的to救助空間。From和to救助空間互換角色。維持活動的對象將在救助空間不斷復制,直到它們獲得使用期並轉入舊域。使用-XX:SurvivorRatio可控制新域子空間的大小。同NewRation一樣,SurvivorRation規定某救助域與Eden空間的比值。比如,以下命令把新域設置成64m,Eden佔32m,每個救助域各佔16m:java-Xms256m-Xmx256m-Xmn64m-XX:SurvivorRation=2如前所述,默認狀態下HotSpot對新域使用復制收集器,對舊域使用標記-清除-壓縮收集器。在新域中使用復制收集器有很多意義,因為應用程序生成的大部分對象是短壽命的。理想狀態下,所有過渡對象在移出Eden空間時將被收集。如果能夠這樣的話,並且移出Eden空間的對象是長壽命的,那麼理論上可以立即把它們移進舊域,避免在救助空間反復復制。但是,應用程序不能適合這種理想狀態,因為它們有一小部分中長壽命的對象。最好是保持這些中長壽命的對象並放在新域中,因為復制小部分的對象總比壓縮舊域廉價。為控制新域中對象的復制,可用-XX:TargetSurvivorRatio控制救助空間的比例(該值是設置救助空間的使用比例。如救助空間位1M,該值50表示可用500K)。該值是一個百分比,默認值是50。當較大的堆棧使用較低的sruvivorratio時,應增加該值到80至90,以更好利用救助空間。用-XX:maxtenuringthreshold可控制上限。為放置所有的復制全部發生以及希望對象從eden擴展到舊域,可以把MaxTenuringThreshold設置成0。設置完成後,實際上就不再使用救助空間了,因此應把SurvivorRatio設成最大值以最大化Eden空間,設置如下:java…-XX:MaxTenuringThreshold=0–XX:SurvivorRatio=50000…4.BEAJRockitJVM的使用BeaWebLogic8.1使用的新的JVM用於Intel平台。在Bea安裝完畢的目錄下可以看到有一個類似於jrockit81sp1_141_03的文件夾。這就是Bea新JVM所在目錄。不同於HotSpot把Java位元組碼編譯成本地碼,它預先編譯成類。JRockit還提供了更細致的功能用以觀察JVM的運行狀態,主要是獨立的GUI控制台(只能適用於使用Jrockit才能使用jrockit81sp1_141_03自帶的console監控一些cpu及memory參數)或者WebLogicServer控制台。BeaJRockitJVM支持4種垃圾收集器:4.1.1.分代復制收集器它與默認的分代收集器工作策略類似。對象在新域中分配,即JRockit文檔中的nursery。這種收集器最適合單cpu機上小型堆操作。4.1.2.單空間並發收集器該收集器使用完整堆,並與背景線程共同工作。盡管這種收集器可以消除中斷,但是收集器需花費較長的時間尋找死對象,而且處理應用程序時收集器經常運行。如果處理器不能應付應用程序產生的垃圾,它會中斷應用程序並關閉收集。分代並發收集器這種收集器在護理域使用排它復制收集器,在舊域中則使用並發收集器。由於它比單空間共同發生收集器中斷頻繁,因此它需要較少的內存,應用程序的運行效率也較高,注意,過小的護理域可以導致大量的臨時對象被擴展到舊域中。這會造成收集器超負荷運作,甚至採用排它性工作方式完成收集。4.1.3.並行收集器該收集器也停止其他進程的工作,但使用多線程以加速收集進程。盡管它比其他的收集器易於引起長時間的中斷,但一般能更好的利用內存,程序效率也較高。默認狀態下,JRockit使用分代並發收集器。要改變收集器,可使用-Xgc:,對應四個收集器分別為gen,singlecon,gencon以及parallel。可使用-Xms和-Xmx設置堆的初始大小和最大值。要設置護理域,則使用-Xns:java–jrockit–Xms512m–Xmx512m–Xgc:gencon–Xns128m…盡管JRockit支持-verbose:gc開關,但它輸出的信息會因收集器的不同而異。JRockit還支持memory、load和codegen的輸出。注意:如果使用JRockitJVM的話還可以使用WLS自帶的console(C:\bea\jrockit81sp1_141_03\bin下)來監控一些數據,如cpu,memery等。要想能構監控必須在啟動服務時startWeblogic.cmd中加入-Xmanagement參數。5.如何從JVM中獲取信息來進行調整-verbose.gc開關可顯示gc的操作內容。打開它,可以顯示最忙和最空閑收集行為發生的時間、收集前後的內存大小、收集需要的時間等。打開-xx:+printgcdetails開關,可以詳細了解gc中的變化。打開-XX:+PrintGCTimeStamps開關,可以了解這些垃圾收集發生的時間,自jvm啟動以後以秒計量。最後,通過-xx:+PrintHeapAtGC開關了解堆的更詳細的信息。為了了解新域的情況,可以通過-XX:=PrintTenuringDistribution開關了解獲得使用期的對象權。6.Pdm系統JVM調整6.1.伺服器:前提內存1G單CPU可通過如下參數進行調整:-server啟用伺服器模式(如果CPU多,伺服器機建議使用此項)-Xms,-Xmx一般設為同樣大小。800m-Xmn是將NewSize與MaxNewSize設為一致。320m-XX:PerSize64m-XX:NewSize320m此值設大可調大新對象區,減少FullGC次數-XX:MaxNewSize320m-XX:NewRatoNewSize設了可不設。-XX:SurvivorRatio-XX:userParNewGC可用來設置並行收集-XX:ParallelGCThreads可用來增加並行度-XXUseParallelGC設置後可以使用並行清除收集器-XX:UseAdaptiveSizePolicy與上面一個聯合使用效果更好,利用它可以自動優化新域大小以及救助空間比值6.2.客戶機:通過在JNLP文件中設置參數來調整客戶端JVMJNLP中參數:initial-heap-size和max-heap-size這可以在framework的RequestManager中生成JNLP文件時加入上述參數,但是這些值是要求根據客戶機的硬體狀態變化的(如客戶機的內存大小等)。建議這兩個參數值設為客戶機可用內存的60%(有待測試)。為了在動態生成JNLP時以上兩個參數值能夠隨客戶機不同而不同,可靠慮獲得客戶機系統信息並將這些嵌到首頁index.jsp中作為連接請求的參數。在設置了上述參數後可以通過Visualgc來觀察垃圾回收的一些參數狀態,再做相應的調整來改善性能。一般的標準是減少fullgc的次數,最好硬體支持使用並行垃圾回收(要求多CPU)。
F. java中垃圾回收器讓工作線程停頓下來是怎麼做的
1、jvm中,在執行垃圾收集演算法時,Java應用程序的其他所有除了垃圾收集收集器線程之外的線程都被掛起。此時,系統只能允許GC線程進行運行,其他線程則會全部暫停,等待GC線程執行完畢後才能再次運行。這些工作都是由虛擬機在後台自動發起和自動完成的,是在用戶不可見的情況下把用戶正常工作的線程全部停下來,這對於很多的應用程序,尤其是那些對於實時性要求很高的程序來說是難以接受的。 但不是說GC必須STW(Stop-The-World,全局暫停), 你也可以選擇降低運行速度但是可以並發執行的收集演算法,這取決於你的業務。
G. java垃圾回收機制,當變數失去引用,出作用域之後,java垃圾回收線程,是立即回收嗎
對於:
Test t = new Test();
t = null;
垃圾回收肯定不會進行,盡管t已經沒有指向了,但它仍舊在方法內,它還可以被再次賦值,無需重新定義,只有一個對象失去所有引用,並離開所有相關過程或者方法時,才會被回收,回收適時進行,一般很快,但你無法從程序內部追蹤到,只要能追蹤到的,就說明引用還未釋放
H. java 線程池機制的原理是什麼
線程池屬於對象池.所有對象池都具有一個非常重要的共性,就是為了最大程度復用對象.那麼線程池的最
重要的特徵也就是最大程度利用線程.
首先,創建線程本身需要額外(相對於執行任務而必須的資源)的開銷.
作業系統在每創建一個線程時,至少需要創建以下資源:
(1) 線程內核對象:用於對線程上下文的管理.
(2) 用戶模式執行棧.
(3) 內核模式執行棧.
這些資源被線程佔有後作業系統和用戶都無法使用.
相反的過程,銷毀線程需要回收資源,也需要一定開銷.
其次,過多的線程將導致過度的切換.線程切換帶來的性能更是不可估量.系統完成線程切換要經過以下過程:
(1) 從用戶模式切換到內核模式.
(2) 將CPU寄存器的值保存到當前線程的內核對象中.
(3)打開一個自旋鎖,根據調度策略決定下一個要執行的線程.釋放自旋鎖,如果要執行的線程不是同一進
程中的線程,還需要切換虛擬內存等進程環境.
(4) 將要執行的線程的內核對象的值寫到CPU寄存器中.
(5) 切換到用戶模式執行新線程的執行邏輯.
所以線程池的目的就是為了減少創建和切換線程的額外開銷,利用已經的線程多次循環執行多個任務從而提
高系統的處理能力.
I. java中某一個函數或者線程執行完畢後,裡面創建的對象會不會被回收急急急急急急急急急!!!
d會被回收
abc不會 更確切的說是 123456 這個字元串對象不會被回收
java程序里字元串對象一旦被創建就會一直存在就算失去了所有引用也是一樣
不會造成泄漏 用了clear() 就等於把引用全都刪除了 失去了所有引用的對象(除了字元串對象)會被垃圾處理
J. java 當一個線程結束時(程序還在走),該線程的內存會回收嗎
Java的垃圾回收機制是自動發生的,發生的時間是不確定的,當內存不足是或是cpu空閑的時候,都有可能進行垃圾回收機制,如果想具體了解可以看看Java虛擬機相關內容.