摘要:的線程機制是搶占式。會讓出當多個線程并發的對主存中的數據進行操作時,有且只有一個會成功,其余均失敗。和對象只有在困難的多線程問題中才是必須的。
并發簡述
并發通常是用于提高運行在單處理器上的程序的性能。在單 CPU 機器上使用多任務的程序在任意時刻只在執行一項工作。
并發編程使得一個程序可以被劃分為多個分離的、獨立的任務。一個線程就是在進程中的一個單一的順序控制流。java的線程機制是搶占式。
線程的好處是提供了輕量級的執行上下文切換,只改變了程序的執行序列和局部變量。
多線程的主要缺陷:
volatile關鍵字等待共享資源的時候性能降低。
需要處理線程的額外 CPU 花費。
糟糕的程序設計導致不必要的復雜度。
有可能產生一些病態行為,若餓死、競爭、死鎖和活鎖。
不同平臺導致的不一樣。
源來:
當程序運行,JVM會為每一個線程分配一個獨立的緩存用于提高執行效率,每一個線程都在自己獨立的緩存中操作各自的數據。一個線程在緩沖中對數據進行修改,寫入到主存后,其他線程無法得知數據已被更改,仍在操作緩存中已過時的數據,為了解決這個問題,提供了volatile關鍵字,實現內存可見,一旦主存數據被修改,便致使其他線程緩存數據行無效,強制前往主存獲取新數據。
Example:內存不可見,導致主線程無法結束。
class ThreadDemo implements Runnable { //添加volatile關鍵字可實現內存可見性 public volatile boolean flag = false; public boolean flag = Boolean.false; @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { } flag = Boolean.true; System.out.println("ThreadDemo over"); } public boolean isFlag() { return flag; } } public class TestVolatile { public static void main(String[] args) { ThreadDemo demo = new ThreadDemo(); new Thread(demo).start(); while (true) { if (demo.flag || demo.isFlag()) { System.out.println("Main over"); break; } } } }/*output:打印ThreadDemo over,主線程持續循環*/
作用:
當多個線程操作共享數據時,保證內存中的數據可見性。采用底層的內存柵欄,及時的將緩存中修改的數據刷新到主存中,并導致其他線程所緩存的數據無效,使得這些線程必須去主存中獲取修改的數據。
優缺點:
保證內存可見性,讓各個線程能夠彼此獲取最新的內存數據。
較傳統synchronized加鎖操作提高了效率,若有線程正在操作被synchronized修飾的代碼塊數據時,其他線程試圖進行操作,發現已被其他線程占用,試圖操作的線程必須掛起,等到下一次繼續嘗試操作。
對volatile修飾的數據被修改后,其他線程必須前往主存中讀取,若修改頻繁,需要不斷讀取主存數據,效率將會降低。
使用volatile,底層采用內存柵欄,JVM將不會對其提供指令重排序及其優化。
不具備互斥性。多個線程可以同時對數據進行操作,只是由原來的在緩存操作轉變成了直接在主存中操作。(synchronized是互斥的,一個線程正在執行,其他線程必須掛起等待)
不保證變量的原子性。使用volatile僅僅是一個能保證可見性的輕量級同步策略。
原子變量與 CAS 算法Example:使用volatile修飾,number自增問題。
class ThreadDemo implements Runnable { public volatile int number = 0; @Override public void run() { try { Thread.sleep(200); } catch (Exception e) { } System.out.print(getIncrementNumber() + " "); } public int getIncrementNumber() { return ++number; } } public class TestAtomic { public static void main(String[] args) { ThreadDemo demo = new ThreadDemo(); for (int i = 0; i < 10; i++) { new Thread(demo).start(); } } }/*output: 1 5 4 7 3 9 2 1 8 6 */
// ++number底層原理思想 int temp = number; // ① number = number + 1; // ② temp = number; // ③ return temp; // ④
由 ++number 可知,返回的是 temp 中存儲的值,且自增是一個多步操作,當多個線程調用 incrementNumber方法時,方法去主存中獲取 number 值放入 temp 中,根據 CPU 時間片切換,當 A 線程完成了 ③ 操作時,時間片到了被中斷,A 線程開始執行 ① 時不幸被中斷,接著 A 獲取到了CPU執行權,繼續執行完成 ④ 操作更新了主存中的值,緊接著 B 線程開始執行,但是 B 線程 temp中存儲的值已經過時了。注意:自增操作為四步,只有在第四步的時候才會刷新主存的值,而不是number = number + 1 操作就反映到主存中去。如圖所示:
源來:
volatile只能保證內存可見性,對多步操作的變量,無法保證其原子性,為了解決這個問題,提供了原子變量。
作用:
原子變量既含有volatile的內存可見性,又提供了對變量原子性操作的支持,采用底層硬件對并發操作共享數據的 CAS(Compare-And-Swap)算法,保證數據的原子性。
提供的原子類:
類 | 描述 |
---|---|
AtomicBoolean | 一個 boolean值可以用原子更新。 |
AtomicInteger | 可能原子更新的 int值。 |
AtomicIntegerArray | 一個 int數組,其中元素可以原子更新。 |
AtomicIntegerFieldUpdater |
基于反射的實用程序,可以對指定類的指定的 volatile int字段進行原子更新。 |
AtomicLong | 一個 long值可以用原子更新。 |
AtomicLongArray | 可以 long地更新元素的 long數組。 |
AtomicLongFieldUpdater |
基于反射的實用程序,可以對指定類的指定的 volatile long字段進行原子更新。 |
AtomicMarkableReference |
AtomicMarkableReference維護一個對象引用以及可以原子更新的標記位。 |
AtomicReference |
可以原子更新的對象引用。 |
AtomicReferenceArray |
可以以原子方式更新元素的對象引用數組。 |
AtomicReferenceFieldUpdater |
一種基于反射的實用程序,可以對指定類的指定的 volatile volatile引用原子更新。 |
AtomicStampedReference |
AtomicStampedReference維護對象引用以及可以原子更新的整數“印記”。 |
DoubleAccumulator | 一個或多個變量一起維護使用提供的功能更新的運行的值 double 。 |
DoubleAdder | 一個或多個變量一起保持初始為零 double和。 |
LongAccumulator | 一個或多個變量,它們一起保持運行 long使用所提供的功能更新值。 |
LongAdder | 一個或多個變量一起保持初始為零 long總和。 |
CAS算法:
CAS(Compare-And-Swap)是底層硬件對于原子操作的一種算法,其包含了三個操作數:內存值(V),預估值(A),更新值(B)。當且僅當 V == A 時, 執行 V = B 操作;否則不執行任何結果。這里需要注意,A 和 B 兩個操作數是原子性的,同一時刻只能有一個線程進行AB操作。
優缺點:
操作失敗時,直接放棄結果,并不釋放對CPU的控制權,進而可以繼續嘗試操作,不必掛起等待。(synchronized會讓出CPU)
當多個線程并發的對主存中的數據進行操作時,有且只有一個會成功,其余均失敗。
原子變量中封裝了用于對數據的原子操作,簡化了代碼的編寫。
Collection并發類HashMap 與 HashTable簡述
HashMap是線程不安全的,而HashTable是線程安全的,因為HashTable所維護的Hash表存在著獨占鎖,當多個線程并發訪問時,只能有一個線程可進行操作,但是對于復合操作時,HashTable仍然存在線程安全問題,不使用HashTable的主要原因還是效率低下。
// 功能:不包含obj,則添加 if (!hashTable.contains(obj)) { // 復合操作,執行此處時線程中斷,obj被其他線程添加至容器中,此處繼續執行將導致重復添加 hashTable.put(obj); } 可知上述兩個操作需要 “原子性”,為了達到效果,還不是得對代碼塊進行同步
ConcurrentHashMap
采用鎖分段機制,分為 16 個段(并發級別),每一個段下有一張表,該表采用鏈表結構鏈接著各個元素,每個段都使用獨立的鎖。當多個線程并發操作的時候,根據各自的級別不同,操作不同的段,多個線程并行操作,明顯提高了效率,其次還提供了復合操作的諸多方法。注:jdk1.8由原來的數組+單向鏈表結構轉換成數據+單向鏈表+紅黑樹結構。
ConcurrentSkipListMap和ConcurrentSkipListSet
有序的哈希表,通過跳表實現,不允許null作為鍵或值。ConcurrentSkipListMap詳解
CopyOnWriteArrayList 和 CopyOnWriteArraySet
對collection進行寫入操作時,將導致創建整個底層數組的副本,而源數組將保留在原地,使得復制的數組在被修改時,讀取操作可以安全的執行。當修改完成時,一個原子性的操作將把心的數組換人,使得新的讀取操作可以看到新的修改。
好處之一是當多個迭代器同時遍歷和修改列表時,不會拋出ConcurrentModificationException。
小結:
當期望許多線程訪問一個給定 collection 時,ConcurrentHashMap 通常優于同步的 HashMap
ConcurrentSkipListMap 通常優于同步的 TreeMap。
當期望的讀數和遍歷遠遠大于列表的更新數時,CopyOnWriteArrayList 優于同步的 ArrayList。
并發迭代操作多時,可選擇CopyOnWriteArrayList 和 CopyOnWriteArraySet。
高并發情況下,可選擇ConcurrentSkipListMap和ConcurrentSkipListSet
CountDownLatch閉鎖源由:
當一個修房子的 A 線程正在執行,需要磚頭時,開啟了一個線程 B 去拉磚頭,此時 A 線程需要等待 B 線程的結果后才能繼續執行時,但是線程之間都是并行操作的,為了解決這個問題,提供了CountDownLatch。
作用:
一個同步輔助類,為了保證執行某些操作時,“所有準備事項都已就緒”,僅當某些操作執行完畢后,才能執行后續的代碼塊,否則一直等待。
CountDownLatch中存在一個鎖計數器,如果鎖計數器不為 0 的話,它會阻塞任何一個調用 await() 方法的線程。也就是說,當一個線程調用 await() 方法時,如果鎖計數器不等于 0,那么就會一直等待鎖計數器為 0 的那一刻,這樣就解決了需要等待其他線程執行完畢才執行的需求。
Example:
class ThreadDemo implements Runnable { private CountDownLatch latch = null; public ThreadDemo(CountDownLatch latch) { this.latch = latch; } @Override public void run() { try { System.out.println("execute over"); } finally { latch.countDown(); // 必須保證計數器減一 } } } public class TestCountDownLatch { public static void main(String[] args) { final int count = 10; final CountDownLatch latch = new CountDownLatch(count); ThreadDemo demo = new ThreadDemo(latch); for (int i = 0; i < count; ++i) { new Thread(demo).start(); } try { latch.await(); // 等待計數器為 0 System.out.println("其他線程結束,繼續往下執行..."); } catch (InterruptedException e) { e.printStackTrace(); } } }/**output: execute over ... 其他線程結束,繼續往下執行... */
細節:
子線程完畢后,必須調用 countDown() 方法使得 鎖計數器減一,否則將會導致調用 await() 方法的線程持續等待,盡可能的放置在 finally 中。
鎖計數器的個數與子線程數最好相等,只要計數器等于 0,不論是否還存在子線程,await() 方法將得到響應,繼續執行后續代碼。
Callable接口源由:
當開啟一個線程執行運算時,可能會需要該線程的計算結果,之前的 implements Runnable 和 extends Thread 的 run() 方法并沒有提供可以返回的功能,因此提供了 Callable接口。 Callable 的運行結果, 需要使用 FutureTask 類來接受。
Example:
class ThreadDemo implements CallableLock同步鎖和Condition線程通信控制對象{ private Integer cycleValue; public ThreadDemo(Integer cycleValue) { this.cycleValue = cycleValue; } @Override public Integer call() throws Exception { int result = 0; for (int i=0; i task = new FutureTask<>(demo); new Thread(task).start(); Integer result = task.get(); // 等待計算結果返回, 閉鎖 System.out.println(result); } }/*output:1073741825 */
Lock:在進行性能測試時,使用Lock通常會比使用synchronized要高效許多,并且synchronized的開銷變化范圍很大,而Lock相對穩定。只有在性能調優時才使用Lock對象。
Condition: 替代了 Object 監視器方法的使用,描述了可能會與鎖有關的條件標量,相比 Object 的 notifyAll() ,Condition 的 signalAll() 更安全。Condition 實質上被綁定到一個鎖上,使用newCondition() 方法為 Lock 實例獲取 Condition。
Lock和Condition對象只有在困難的多線程問題中才是必須的。
synchonized與Lock的區別:
synchonized | Lock |
---|---|
隱式鎖 | 顯示鎖 |
JVM底層實現,由JVM維護 | 由程序員手動維護 |
靈活控制(也有風險) |
“虛假喚醒”:當一個線程A在等待時,被另一個線程喚醒,被喚醒的線程不一定滿足了可繼續向下執行的條件,如果被喚醒的線程未滿足條件,而又向下執行了,那么稱這個現象為 “虛假喚醒”。
// 安全的方式,保證退出等待循環前,一定能滿足條件 while (條件) { wait(); }
Example:生產消費者
// 產品car class Car { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); private boolean available = false; // false:無貨;true有貨 public void put(){ lock.lock(); try { while (available) { // 有貨等待 condition.await(); } System.out.println(Thread.currentThread().getName() + "put(): 進貨"); available = true; condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void get() { lock.lock(); try { while (!available) { // 無貨等待 condition.await(); } System.out.println(Thread.currentThread().getName() + "get():出貨"); available = false; condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } // 消費者 class Consume implements Runnable { private Car car; public Consume(Car car) { this.car = car; } @Override public void run() { for (int i=0; i 每一個 對lock()的調用都必須緊跟著一個 try-finally 子句,用以保證可以在任何情況下都能釋放鎖,任務在調用 await()、signal()、signalAll()之前,必須擁有鎖。lock.lock(); try { ... // 業務代碼 } finally { lock.unlock(); }ReadWriteLock讀寫鎖源由:
上述講解的鎖都是讀寫一把鎖,不論是讀或寫,都是一把鎖解決,當多線程訪問數據時,若發生了一千次操作,其中的寫操作只執行了一次,數據的更新率非常低,那么每次進行讀操作時,都要加鎖讀取”不會更改的“數據,顯然是不必要的開銷,因此出現了 ReadWriteLock 讀寫鎖,該對象提供讀鎖和寫鎖。
作用:
ReadWriteLock 維護了一對相關的鎖,一個用于只讀操作,另一個用于寫入操作。只要沒有 write寫入操作,那么多個線程可以同時進行持有讀鎖。而寫入鎖是獨占的,當執行寫操作時,其他線程不可寫,也不可讀。
性能的提升取決于讀寫操作期間讀取數據相對于修改數據的頻率,如果讀取操作遠遠大于寫入操作時,便能增強并發性。
Example:
class Demo { private int value = 0; private ReadWriteLock lock = new ReentrantReadWriteLock(); public void read() { lock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + " : " + value); } finally { lock.readLock().unlock(); } } public void write(int value) { lock.writeLock().lock(); try { this.value = value; System.out.println("write(" + value + ")"); } finally { lock.writeLock().unlock(); } } } class ReadLock implements Runnable { private Demo demo = null; public ReadLock(Demo demo) { this.demo = demo; } @Override public void run() { for (int i=0; i<20; ++i) { demo.read(); try { Thread.sleep(320); } catch (InterruptedException e) { } } } } class WriteLock implements Runnable { private Demo demo = null; public WriteLock(Demo demo) { this.demo = demo; } @Override public void run() { for (int i=0; i<10; ++i) { demo.write(i); try { Thread.sleep(200); } catch (InterruptedException e) { } } } } public class TestReadWriteLock { public static void main(String[] args) { Demo demo = new Demo(); ReadLock readLock = new ReadLock(demo); WriteLock writeLock = new WriteLock(demo); for (int i=0; i<3; ++i) { new Thread(readLock, i + "--").start(); } new Thread(writeLock).start(); } }/**output: 0-- : 0 1-- : 0 2-- : 0 write(0) write(1) 1-- : 1 2-- : 1 0-- : 1 write(2) write(3) 1-- : 3 0-- : 3 ... */線程池與線程調度源來:
在傳統操作中(如連接數據庫),當我們需要使用一個線程的時候,就 直接創建一個線程,線程完畢后被垃圾收集器回收。每一次需要線程的時候,不斷的創建與銷毀,大大增加了資源的開銷。
作用:
線程池維護著一個線程隊列,該隊列中保存著所有等待著的線程,避免了重復的創建與銷毀而帶來的開銷。
體系結構:
Execuotr:負責線程的使用與調度的根接口。 |- ExecutorService:線程池的主要接口。 |- ForkJoinPool:采用分而治之技術將任務分解。 |- ThreadPoolExecutor:線程池的實現類。 |- ScheduledExecutorService:負責線程調度的子接口。 |- ScheduledThreadPoolExecutor:負責線程池的調度。繼承ThreadPoolExecutor并實現ScheduledExecutorService接口Executors 工具類API描述:
方法 描述 ExecutorService newFixedThreadPool(int nThreads) 創建一個可重用固定數量的無界隊列線程池。使用了有限的線程集來執行所提交的所有任務。創建的時候可以一次性預先進行代價高昂的線程分配。 ExecutorService newWorkStealingPool(int parallelism) 創建一個維護足夠的線程以支持給定的parallelism并行級別的線程池。 ExecutorService newSingleThreadExecutor() 創建一個使用單個線程運行的無界隊列的執行程序。 ExecutorService newCachedThreadPool() 創建一個根據需要創建新線程的線程池,當有可用線程時將重新使用以前構造的線程。 ScheduledExecutorService newSingleThreadScheduledExecutor() 創建一個單線程執行器,可以調度命令在給定的延遲之后運行,或定期執行。 ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 創建一個線程池,可以調度命令在給定的延遲之后運行,或定期執行。 ThreadFactory privilegedThreadFactory() 返回一個用于創建與當前線程具有相同權限的新線程的線程工廠。 補充:
ExecutorService.shutdown():防止新任務被提交,并繼續運行被調用之前所提交的所有任務,待任務都完成后退出。
CachedThreadPoo在程序執行過程中通常會創建與所需數量相同的線程,然后在它回收舊線程時停止創建新線程,是Executor的首選。僅當這個出現問題時,才需切換 FixedThreadPool。
SingleThreadExecutor: 類似于線程數量為 1 的FixedThreadPool,但它提供了不會存在兩個及以上的線程被并發調用的并發。
Example:線程池
public class TestThreadPool { public static void main(String[] args) throws Exception { ExecutorService pool = Executors.newFixedThreadPool(2); for (int i = 0; i < 10; ++i) { Futurefuture = pool.submit(new Callable () { @Override public String call() throws Exception { return Thread.currentThread().getName(); } }); String threadName = future.get(); System.out.println(threadName); } pool.shutdown(); // 拒絕新任務并等待正在執行的線程完成當前任務后關閉。 } }/**output: pool-1-thread-1 pool-1-thread-2 pool-1-thread-1 pool-1-thread-2 ... */ Example:線程調度
public class TestThreadPool { public static void main(String[] args) throws Exception { ScheduledExecutorService pool = Executors.newScheduledThreadPool(2); for (int i = 0; i < 5; ++i) { ScheduledFuturefuture = pool.schedule(new Callable () { @Override public String call() throws Exception { return Thread.currentThread().getName() + " : " + Instant.now(); } }, 1, TimeUnit.SECONDS); // 延遲執行單位為 1秒的任務 String result = future.get(); System.out.println(result); } pool.shutdown(); } }/**output: pool-1-thread-1 : 2019-03-18T12:10:31.260Z pool-1-thread-1 : 2019-03-18T12:10:32.381Z pool-1-thread-2 : 2019-03-18T12:10:33.382Z pool-1-thread-1 : 2019-03-18T12:10:34.383Z pool-1-thread-2 : 2019-03-18T12:10:35.387Z */ 注意:若沒有執行 shutdown()方法,則線程會一直等待而不停止。
ForkJoinPool分支/合并框架源由:
在一個線程隊列中,假如隊頭的線程由于某種原因導致了阻塞,那么在該隊列中的后繼線程需要等待隊頭線程結束,只要隊頭一直阻塞,這個隊列中的所有線程都將等待。此時,可能其他線程隊列都已經完成了任務而空閑,這種情況下,就大大減少了吞吐量。
ForkJoin的“工作竊取”模式:
當執行一個新任務時,采用分而治之的思想,將其分解成更小的任務執行,并將分解的任務加入到線程隊列中,當某一個線程隊列沒有任務時,會隨機從其他線程隊列中“偷取”一個任務,放入自己的隊列中執行。
Example:
// 求次方: value為底,size為次方數 class CountPower extends RecursiveTask{ private static final long serialVersionUID = 1L; public Long value = 0L; public int size = 0; public static final Long CRITICAL = 10L; // 閾值 public CountPower(Long value, int size) { this.value = value; this.size = size; } @Override protected Long compute() { // 當要開方的此時 小于 閾值,則計算 (視為最小的任務單元) if(size <= CRITICAL) { Long sum = 1L; for (int i=0; i 根據分而治之的思想進行分解,需要一個結束遞歸的條件,該條件內的代碼就是被分解的最小單元。使用fork()在當前任務正在運行的池中異步執行此任務,即將該任務壓入線程隊列。調用join()`返回計算結果。RecursiveTask是有返回值的task,RecursiveAction則是沒有返回值的。
參考尚硅谷JUC視頻教程
《java編程思想》第 21 章 并發
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73784.html
摘要:發布的對象內部狀態可能會破壞封裝性,使程序難以維持不變性條件。不變性線程安全性是不可變對象的固有屬性之一。可變對象必須通過安全方式來發布,并且必須是線程安全的或者有某個鎖保護起來。 線程的優缺點 線程是系統調度的基本單位。線程如果使用得當,可以有效地降低程序的開發和維護等成本,同時提升復雜應用程序的性能。多線程程序可以通過提高處理器資源的利用率來提升系統的吞吐率。與此同時,在線程的使用...
摘要:于是抽時間看了看以后各個版本的特性,做了一個總結。年和公開版本發布,取名為。此后對應版本就是,。發布,是一個重大版本更新。在此之后,就是每六個月發布一次新版本。以上和參考資料聊了一些關于的歷史,下面我們看看各個版本有那些新特性。 【這是 ZY 第 11 篇原創技術文章】 某天在網上閑逛,突然看到有篇介紹 Java 11 新特性的文章,頓時心里一驚,畢竟我對于 Java 的版本認識...
摘要:中很多特性或者說知識點都是和面向對象編程概念相關的。在多線程中內容有很多,只是簡單說明一下中初步使用多線程需要掌握的知識點,以后有機會單獨再詳細介紹一些高級特性的使用場景。 寫這篇文章的目的是想總結一下自己這么多年來使用java的一些心得體會,主要是和一些java基礎知識點相關的,所以也希望能分享給剛剛入門的Java程序員和打算入Java開發這個行當的準新手們,希望可以給大家一些經...
摘要:標記,表示記錄當前的位置。直接緩沖通過方法分配的緩沖區,此緩沖區建立在物理內存中。直接在兩個空間中開辟內存空間,創建映射文件,去除了在內核地址空間和用戶地址空間中的操作,使得直接通過物理內存傳輸數據。 NIO與IO的區別 IO NIO 阻塞式 非阻塞式、選擇器selectors 面向流:單向流動,直接將數據從一方流向另一方 面向緩存:將數據放到緩存區中進行存取,經通道進行...
摘要:外部存儲器可用于長期保存大量程序和數據,其成本低容量大,但速度較慢。 1_計算機概述(了解) A:什么是計算機?計算機在生活中的應用舉例 計算機(Computer)全稱:電子計算機,俗稱電腦。是一種能夠按照程序運行,自動、高速處理海量數據的現代化智能電子設備。由硬件和軟件所組成,沒有安裝任何軟件的計算機稱為裸機。常見的形式有臺式計算機、筆記本計算機、大型計算機等。 應用舉例 ...
閱讀 1203·2021-11-17 09:33
閱讀 3599·2021-09-28 09:42
閱讀 3326·2021-09-13 10:35
閱讀 2478·2021-09-06 15:00
閱讀 2438·2021-08-27 13:12
閱讀 3609·2021-07-26 23:38
閱讀 1828·2019-08-30 15:55
閱讀 539·2019-08-30 15:53