摘要:多線程一線程模型實現線程有三種方式使用內核線程實現使用用戶線程實現和使用用戶線程加輕量級進程混合實現。這種輕量級進程與內核線程之間的關系稱為一對一的線程模型。是通知所有等待對象控制權的線程繼續運行。
Java多線程 一、Java線程模型
實現線程有三種方式:使用內核線程實現、使用用戶線程實現和使用用戶線程加輕量級進程混合實現。內核線程是直接由操作系統內核支持的線程,通過內核完成線程切換,內核通過操縱調度器對線程進行調度,并負責將線程的任務映射到各個處理器上。
程序不會直接使用內核線程,而是去使用內核線程的高級接口-輕量級進程。每個輕量級進程由一個內核線程支持。這種輕量級進程與內核線程之間1:1的關系稱為一對一的線程模型。Sun JDK 的Windows版和Linux版都是使用一對一的線程模型,一條Java線程就映射到一條輕量級進程中。由于內核線程的支持,每個輕量級進程成為一個獨立的調度單元,一個輕量級進程的阻塞不會影響整個進程。但也是因為基于內核線程實現,各種線程操作,如創建、析構及同步都要進行系統調用,需要在用戶態和內核態中來回切換,調用代價高,其次輕量級進程消耗一定的內核資源,因此一個系統支持輕量級進程的數量有限。
線程調度是指系統為線程分配處理器使用權的過程,分為協同式調度和搶占式調度。協同式調度的多線程系統,線程執行時間由線程本身控制,線程完成自己的工作之后,主動通知系統切換到另一個線程上。優點是實現簡單,切換操作是由線程主動的,對線程可知,沒有線程同步問題。缺點是線程執行時間不可控制,如果一個線程阻塞,可能導致整個系統奔潰。搶占式調度的多線程系統,每個線程有系統分配執行時間,線程的切換不由線程本身決定。(yield可以讓出執行時間,但線程本身無法獲取執行時間)優點是線程執行時間系統可控。Java使用的線程調度方式就是搶占式調度。
三、Java線程狀態Java線程的6種狀態:
New(新建)
Runnable(可運行)
Blocked(阻塞)
Waiting(等待)
Timed waiting(限時等待)
Terminated(終止)
線程創建成功但尚未啟動就是New;Runable狀態的線程可能正在執行,也可能在等待CPU分配執行時間;當線程等待另一個線程通知調度器一個條件時就進入等待狀態,例如Object.wait、Thread.join;當這些方法指定時間參數時就成了限時等待;當一個線程試圖獲取一個內部的對象鎖,而該鎖被另一線程持有時,該線程進入阻塞狀態;當線程因run方法正常退出而自然死亡,或者因為沒有捕獲的異常死亡都會導致線程進入Terminated狀態。
四、中斷Java中斷機制是一種協作機制,通過中斷并不能直接終止另一個線程,而需要被中斷的線程自己處理中斷。當對一個線程調用interrup方法時,線程的中斷狀態將被置位。這是每一個線程都具有的boolean標志位。每個線程都應該不時地檢查這個標志,以判斷線程是否被中斷,并及時處理。
public void interrupt() { if (this != Thread.currentThread()) checkAccess(); synchronized (blockerLock) { Interruptible b = blocker; if (b != null) { interrupt0(); // Just to set the interrupt flag b.interrupt(this); return; } } interrupt0(); } public static boolean interrupted() { return currentThread().isInterrupted(true); } public boolean isInterrupted() { return isInterrupted(false); } private native boolean isInterrupted(boolean ClearInterrupted);
可以看到,interrupt方法通過設置中斷位來完成中斷。interrupted方法和isInterrupted方法都是通過調用native方法來檢測中斷的,interrupted是一個靜態方法,用來檢測當前線程是否被中斷,而且interrupted會清除該線程的中斷狀態;isInterrupted是一個實例方法,可用來檢驗是否有線程被中斷,該方法不會改變中斷狀態。
一般來說,我們中斷線程的目的很可能是想停止線程執行。怎么停止線程執行呢?我們可以在判斷中斷置位后,用return退出run方法。但這樣設計并不優雅,另外一種方式,就是拋出InterruptedException并在run方法里捕獲。捕獲后怎么處理也是件值得考慮的事,最好的方法是直接拋給調用者處理,但run方法是重寫方法,結構已固定,無法拋出異常,我們還可以在捕獲InterruptedException后重新中斷當前線程,讓調用者檢測。
五、線程相關方法 1.Object.wait()、Object.wait(long timeout)、Object.notify()、Object.notifyAll()wait方法是掛起當前線程,釋放當前對象的控制權(釋放鎖),然后線程處于等待狀態。notify是通知正在等待對象控制權(鎖)的線程可以繼續運行。notifyAll是通知所有等待對象控制權的線程繼續運行。這幾個方法是基于monitor監視器鎖來實現的,所以必須在同步塊內執行。
2.Thread.sleep()、Thread.yield()、Thread.join()sleep讓當前線程暫停指定時間。wait方法依賴于同步,sleep可以直接調用。因為sleep只是暫時讓出CPU的執行權,并不釋放鎖,而wait需要釋放鎖。舉個簡單的例子:
public class WaitTest { private static Object o = new Object(); static class Thread1 extends Thread{ @Override public void run() { try { synchronized(o){ System.out.println("Thread1--start"); //o.wait(); Thread.sleep(2000); System.out.println("Thread1--end"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.out.println("a"); } } } static class Thread2 extends Thread{ @Override public void run() { synchronized(o){ System.out.println("Thread2--start"); o.notify(); System.out.println("Thread2--end"); } } } public static void main(String[] args) throws InterruptedException{ Thread1 t1 = new Thread1(); Thread2 t2 = new Thread2(); t1.start(); Thread.sleep(100);//保證t1先獲得鎖 t2.start(); } }
在線程1里分別調用sleep和wait會有不同的結果,調用sleep時線程1不會釋放鎖,所以會打印完“Thread1 start”、“Thread1 end”,再進入線程2的打印。調用wait時,打印完“Thread1-start”,就會釋放鎖,這時線程2的打印得以繼續進行,會打印“Thread2 start”。
Thread.yield()方法會將當前線程從Running轉為Runnable,讓出當前對進程的使用,以便其他線程有機會執行,不過調度器可以忽虐該方法,也不能指定暫停時間,一般只用來調試和測試。
Thread.join()方法用于將異步的線程“合并”為同步的線程,父線程等待子線程執行完成后再執行。其實并不算合并,而是調用join的線程進入限時等待,不斷檢查子線程狀態,在子線程執行完成后恢復執行。看一下它的實現原理:
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
可以看到,join是通過子線程不斷輪詢自己狀態一直到執行完畢才返回繼續執行父線程。
六、同步 1.synchronized、ReentrantLock與鎖優化在Java中,最基本的互斥同步手段就是synchronized關鍵字,synchronized自動提供一個鎖以及相關的條件。synchronized同步塊對于同一條線程來說是可重入的,其次,同步塊在已進入的線程執行完之前,會阻塞后面其他線程的進入。前面提到,Java的線程是映射到系統原生線程上,阻塞或喚醒一個線程,都需要操作系統幫忙完成,需要從用戶態轉為核心態,這需要耗費很長時間。因此synchronized是重量級操作,虛擬機本身會有一些優化手段,比如在阻塞之前加入自旋等待過程,避免頻繁切入核心態之中。
重入鎖(ReentrantLock)與synchronized相似,具備一樣的線程重入性,一個表現為API層面的互斥鎖,另一個表現為原生語法層面的互斥鎖。ReetrantLock增加了一些功能:等待可中斷、公平鎖和鎖綁定多個條件。
公平鎖是指多個線程等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖,可以通過帶布爾值的構造函數要求使用公平鎖。
/** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
等待可中斷是指持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待:
import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockTestOne { static int count = 0; static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) throws Exception{ Thread a = new Thread(new CountThread("a")); Thread b = new Thread(new CountThread("b")); a.start(); Thread.sleep(100);//確保b線程后執行,a能先獲得鎖 b.start(); Thread.sleep(500);//等待0.5s后,a線程還沒有釋放鎖,通過中斷放棄等待 b.interrupt(); } static class CountThread extends Thread{ String name; CountThread(String name){ this.name = name; } @Override public void run() { /*try{ lock.lockInterruptibly(); }catch(InterruptedException e){ System.out.println("Thread "+name+" interrupted"); return; }*/ lock.lock(); System.out.println(Thread.currentThread().isInterrupted()); try{ System.out.println("Thread "+name+" begin"); for(int i=0; i<2000000; i++){ for(int j=0; j<100000; j++){ count++; } } System.out.println("Thread "+name+" end"); }finally{ lock.unlock(); } } } }
我們先看lock.lock的執行結果:
可以看見a線程執行完后b才開始執行,且b線程的中斷位已被置位。說明lock是阻塞式的獲取鎖,只有在成功獲取到鎖以后才處理中斷信息,并且怎么處理由調用端決定,lock只負責給中斷位置位。
再看一下lock.lockInterruptibly的執行結果:
可以看到,lockInterruptibly會立即處理中斷信息,拋出InterruptedException,而不用等到獲取鎖。
鎖綁定多個條件是指一個ReentrantLock對象可以同時綁定多個Condition對象。在synchronized中,鎖對象的wait和notify其實實現一個隱含的條件,如果要和多個條件關聯,必須額外添加鎖。
public class ReentrantLockTestTwo { static ReentrantLock lock = new ReentrantLock(); static Condition productCondition = lock.newCondition(); static Condition customerCondition = lock.newCondition(); static Set
上面展示了ReentrantLock鎖綁定多個條件??梢钥吹轿覀冊诋a品上加鎖并在鎖上新建了兩個條件:生產條件和使用條件。當產品數量多于6時,讓生產線程等待,小于2時,讓使用線程等待。從執行結果可以看出,每次喚醒的線程只可能是生產或使用線程的一種,而并沒有喚醒這個鎖上的所有線程。
鎖優化有幾種措施:自旋鎖與自適應鎖、鎖消除、鎖粗化、輕量級鎖和偏向鎖。
前面提到同步塊會阻塞其他線程,而線程的阻塞和恢復需要系統切換狀態,耗費較長時間。所以如果持有鎖的線程很快就會釋放鎖時,我們并不需要讓等待線程阻塞,而是讓它執行一個忙循環,這就是所謂的自旋鎖。但自旋鎖雖然避免了線程切換的開銷,卻要占用處理器時間。當鎖被長時間占用時,自旋鎖除了浪費處理器資源就沒有作用了。JDK1.6引入了自適應的自旋鎖,有系統決定自旋時間,改善性能。
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除主要判定依據來源于逃逸分析的數據支持。
如果一系列的連續操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現在循環體中,頻繁地進行互斥同步操作也會導致不必要的性能損耗。這時可以鎖粗化。
偏向鎖是消除數據在無競爭情況下的同步,所謂偏向,是指其偏向第一個獲得它的線程。假設JVM啟用了偏向鎖,當鎖對象第一次被線程獲得的時候,虛擬機將會把對象頭的標志位設為“01”,即偏向模式。同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Word中,如果CAS成功,持有偏向鎖的線程以后每次進入這個鎖相關的同步塊時,虛擬機都不再進行任何同步操作。第二個線程來訪問時,檢查原來持有對象鎖線程是否存活,若已介素則偏向鎖偏向第二個線程,否則第一個線程如果存活,通過線程棧檢查對象是否處于鎖定狀態,如果無鎖,則撤銷偏向恢復到未鎖定對象,如果仍然鎖定,則升級為輕量級鎖。
輕量級鎖是在無競爭情況(個人認為是輕度競爭)下使用CAS操作去消除同步使用的互斥量,線程在執行同步塊之前,虛擬機在當前線程的棧幀中建立Lock Record來存儲對象目前Mark Word的拷貝,然后JVM通過CAS替換對象Mark Word為Lock Record的指針。如果成功,對象處于輕量級鎖定,失敗說明存在額外線程競爭鎖,則嘗試自旋,如果自旋時間內還未獲得鎖,則開始膨脹,修改MarkWord為重量級鎖的指針,并且阻塞自己。
2.線程局部變量ThreadLocal同步機制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。ThreadLocal會在每個線程中為變量創建一個副本,即每個線程內部都會有一個該變量,且在線程內部任何地方可以使用,線程之間互不影響,這樣需要在多線程使用的變量就不存在線程安全問題。
ThreadLocal本身并不存儲變量值,它本身其實只是一個鍵值對的鍵,用來讓線程從ThreadLocalMap中獲取Value,ThreadLocalMap是每個線程內部的容器。
可以看一下ThreadLocal的源碼:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } 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(); }
同時,我們看一下ThreadLocalMap的源碼,會發現它的key使用的是ThreadLocal的弱引用。至于為什么用弱引用,是因為從上圖我們可以看見一共有兩條引用鏈到ThreadLocal變量,如果ThreadLocalRef置空,也就是程序不再訪問ThreadLocal變量了。此時如果key使用的是強引用,那么根據判斷對象存亡的可達性分析算法,ThreadLocal并不會被回收,因為還有一條GC root的引用鏈到ThreadLocal上;如果使用的是弱引用,我們知道弱引用只會存活到下一次JVM GC時,ThreadLocal就可以被回收。
static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } }
使用弱引用ThreadLocal固然可以被回收,但是帶來新的問題。ThreadLocal被回收后ThreadLocalMap中會出現key為null的Entry,意味著沒有辦法訪問這些key為null的Entry的value,如果當前線程遲遲不結束,value對應的對象不被回收,就會導致內存泄漏。從下面的代碼看到,ThreadLocal的set、get、remove方法在一些時機下會清理這些value,但這不及時,還是會有一些內存泄漏,最好的辦法時我們可以通過每次使用完ThreadLocal后,調用它的remove方法來避免這種情況。
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(); } private Entry getEntry(ThreadLocal> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); } 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; } private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }3.Collections
Collections作為集合的工具類,除了提供一些有效的算法之外,還可以對集合進行包裝。其中一種就是非同步集合包裝成同步集合。
public staticMap synchronizedMap(Map m) { return new SynchronizedMap<>(m); } private static class SynchronizedMap implements Map , Serializable { private static final long serialVersionUID = 1978198479659022715L; private final Map m; // Backing Map final Object mutex; // Object on which to synchronize SynchronizedMap(Map m) { this.m = Objects.requireNonNull(m); mutex = this; } SynchronizedMap(Map m, Object mutex) { this.m = m; this.mutex = mutex; } public int size() { synchronized (mutex) {return m.size();} } public boolean isEmpty() { synchronized (mutex) {return m.isEmpty();} } public boolean containsKey(Object key) { synchronized (mutex) {return m.containsKey(key);} } public boolean containsValue(Object value) { synchronized (mutex) {return m.containsValue(value);} } public V get(Object key) { synchronized (mutex) {return m.get(key);} } public V put(K key, V value) { synchronized (mutex) {return m.put(key, value);} } public V remove(Object key) { synchronized (mutex) {return m.remove(key);} } public void putAll(Map extends K, ? extends V> map) { synchronized (mutex) {m.putAll(map);} } public void clear() { synchronized (mutex) {m.clear();} } private transient Set keySet; private transient Set > entrySet; private transient Collection values; public Set keySet() { synchronized (mutex) { if (keySet==null) keySet = new SynchronizedSet<>(m.keySet(), mutex); return keySet; } } public Set > entrySet() { synchronized (mutex) { if (entrySet==null) entrySet = new SynchronizedSet<>(m.entrySet(), mutex); return entrySet; } } public Collection values() { synchronized (mutex) { if (values==null) values = new SynchronizedCollection<>(m.values(), mutex); return values; } } public boolean equals(Object o) { if (this == o) return true; synchronized (mutex) {return m.equals(o);} } public int hashCode() { synchronized (mutex) {return m.hashCode();} } public String toString() { synchronized (mutex) {return m.toString();} } // Override default methods in Map @Override public V getOrDefault(Object k, V defaultValue) { synchronized (mutex) {return m.getOrDefault(k, defaultValue);} } @Override public void forEach(BiConsumer super K, ? super V> action) { synchronized (mutex) {m.forEach(action);} } @Override public void replaceAll(BiFunction super K, ? super V, ? extends V> function) { synchronized (mutex) {m.replaceAll(function);} } @Override public V putIfAbsent(K key, V value) { synchronized (mutex) {return m.putIfAbsent(key, value);} } @Override public boolean remove(Object key, Object value) { synchronized (mutex) {return m.remove(key, value);} } @Override public boolean replace(K key, V oldValue, V newValue) { synchronized (mutex) {return m.replace(key, oldValue, newValue);} } @Override public V replace(K key, V value) { synchronized (mutex) {return m.replace(key, value);} } @Override public V computeIfAbsent(K key, Function super K, ? extends V> mappingFunction) { synchronized (mutex) {return m.computeIfAbsent(key, mappingFunction);} } @Override public V computeIfPresent(K key, BiFunction super K, ? super V, ? extends V> remappingFunction) { synchronized (mutex) {return m.computeIfPresent(key, remappingFunction);} } @Override public V compute(K key, BiFunction super K, ? super V, ? extends V> remappingFunction) { synchronized (mutex) {return m.compute(key, remappingFunction);} } @Override public V merge(K key, V value, BiFunction super V, ? super V, ? extends V> remappingFunction) { synchronized (mutex) {return m.merge(key, value, remappingFunction);} } private void writeObject(ObjectOutputStream s) throws IOException { synchronized (mutex) {s.defaultWriteObject();} } }
可以看見,其實包裝的原理很簡單,無非是對原來的所有操作加上同步鎖,這樣非同步集合就成了同步集合。
4.ReentrantReadWriteLock讀寫鎖其實就是共享鎖和排它鎖。如果對資源加了寫鎖,其他線程無法再獲得讀鎖或寫鎖,但持有寫鎖的線程,可以對資源加讀鎖(鎖降級)。如果線程對資源加了讀鎖,其他線程可以繼續加讀鎖。舉個例子:幾個人一起開發,SVN服務器上的代碼大家可以同時查看,但對同一段代碼的修改提交同時只能一個人操作。這里查看就需要讀鎖,提交就需要加寫鎖,如下代碼。
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; public class ReentrantReadWriteLockTest { static int readCount = 0; static int writeCount = 0; static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); static String code = "hello world"; static ReadLock readlock = lock.readLock(); static WriteLock writelock = lock.writeLock(); public static void main(String[] args) { ReadThread r = new ReadThread(); WriteThread w = new WriteThread(); for(int i=0; i<3; i++){ new Thread(r).start(); new Thread(w).start(); } } static class ReadThread extends Thread{ @Override public void run() { while(true){ readlock.lock(); try{ readCount ++; System.out.println("同時有"+readCount+"個線程同時讀的內容: "+code); String temp = new String(code); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(code.equals(temp)); readCount --; }finally{ readlock.unlock(); } } } } static class WriteThread extends Thread{ @Override public void run() { while(true){ writelock.lock(); try{ writeCount ++; code = code + "a"; System.out.println("同時有"+writeCount+"個線程寫的內容: "+code); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } writeCount --; }finally{ writelock.unlock(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
我們來看一下運行結果:
可以看到,有多個線程同時讀取代碼,但任意時刻只有一個線程進行更改。且讀的時候不允許更改(代碼是通過比較前后兩次讀到的內容來驗證讀寫鎖不兼容的,這不夠嚴謹,暫時沒有想到更好例子)。至于鎖降級,因為在修改數據后寫線程沒有再用到數據,所以上例中沒有用鎖降級,在此摘抄一段話來說明其必要性。
參考:《深入理解Java虛擬機》、《Java并發編程實戰》、《Java核心技術》。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/68754.html
摘要:超詳細的面試題總結一之基本知識多線程和虛擬機創建線程有幾種不同的方式你喜歡哪一種為什么繼承類實現接口應用程序可以使用框架來創建線程池實現接口。死亡線程方法執行結束,或者因異常退出了方法,則該線程結束生命周期。死亡的線程不可再次復生。 超詳細的Java面試題總結(一)之Java基本知識 多線程和Java虛擬機 創建線程有幾種不同的方式?你喜歡哪一種?為什么? 繼承Thread類 實現R...
摘要:基礎知識復習后端掘金的作用表示靜態修飾符,使用修飾的變量,在中分配內存后一直存在,直到程序退出才釋放空間。將對象編碼為字節流稱之為序列化,反之將字節流重建成對象稱之為反序列化。 Java 學習過程|完整思維導圖 - 后端 - 掘金JVM 1. 內存模型( 內存分為幾部分? 堆溢出、棧溢出原因及實例?線上如何排查?) 2. 類加載機制 3. 垃圾回收 Java基礎 什么是接口?什么是抽象...
摘要:線程可以被稱為輕量級進程。一個守護線程是在后臺執行并且不會阻止終止的線程。其他的線程狀態還有,和。上下文切換是多任務操作系統和多線程環境的基本特征。在的線程中并沒有可供任何對象使用的鎖和同步器。 原文:Java Multi-Threading and Concurrency Interview Questions with Answers 翻譯:并發編程網 - 鄭旭東 校對:方騰飛 多...
摘要:多線程和并發問題是技術面試中面試官比較喜歡問的問題之一。線程可以被稱為輕量級進程。一個守護線程是在后臺執行并且不會阻止終止的線程。其他的線程狀態還有,和。上下文切換是多任務操作系統和多線程環境的基本特征。 多線程和并發問題是 Java 技術面試中面試官比較喜歡問的問題之一。在這里,從面試的角度列出了大部分重要的問題,但是你仍然應該牢固的掌握Java多線程基礎知識來對應日后碰到的問題。(...
閱讀 1798·2021-11-24 10:21
閱讀 1208·2021-09-22 15:25
閱讀 3170·2019-08-30 15:55
閱讀 708·2019-08-30 15:54
閱讀 3461·2019-08-30 14:20
閱讀 1659·2019-08-30 14:06
閱讀 638·2019-08-30 13:11
閱讀 3144·2019-08-29 16:43