並發編程3個包:
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
線程的六種狀態與轉換:
wait()與sleep()的區別:
- 來自不同的類:wait()來自Object類,sleep()來自Thread類
- 有無釋放鎖資源:sleep()不釋放鎖,wait()釋放鎖,
- 使用范圍不同:sleep()可以在任何地方使用,wait()只能在同步方法或同步塊中使用
- 是否需要捕獲異常:sleep()必須捕獲異常,wait()不需要捕獲異常
synchronized與Lock區別:
- synchronized是java內置關鍵字,Lock是類
- synchronized無法判斷是否獲取到鎖,Lock可以
- synchronized會自動釋放鎖,Lock需要手動釋放鎖
- synchronized等待不可中斷,Lock可以中斷
- synchronized是非公平鎖,Lock鎖默認是非公平,可以通過構造方法傳入boolean值true來設置為公平鎖(公平鎖就是按申請鎖的順序分配鎖,非公平鎖就是不按順序分配)
8鎖現象:
1.標准訪問 2.使郵件sleep4秒 都是先調用先執行
public class Lock8 { public static void main(String[] args) throws InterruptedException { Phone phone=new Phone(); new Thread(()->{ try{ phone.sendEmail();; }catch (Exception e){ e.printStackTrace(); } },"A").start(); Thread.sleep(2000); new Thread(()->{ try{ phone.sendSMS(); }catch (Exception e){ e.printStackTrace(); } },"B").start(); } } public class Phone { public synchronized void sendEmail() throws Exception{ System.out.println("SendEmail"); } public synchronized void sendSMS() throws Exception{ System.out.println("sendSMS"); } }
誰先調用誰先執行,因為synchronized修飾的方法,鎖的對象是方法的調用者,因為兩個方法的調用者是同一個,所以連個方法用的是同一個鎖,先調用的先執行
3.未同步的方法與同步方法誰先執行
未同步的方法不受鎖的影響,無需等待先執行
4.兩部手機,誰先執行
兩部手機兩個鎖對象,所以第二部手機無需等待,先執行
5.一個手機兩個靜態同步方法,誰先執行
被sychronized和static修飾的方法鎖的對象是類的class對象,兩個方法用的還是同一把鎖,先調用先執行
6.兩個手機,兩個靜態同步方法,誰先執行
被sychronized和static修飾的方法鎖的對象是類的class對象,兩個方法用的還是同一把鎖,先調用先執行
7.一部手機,一個普通同步方法,一個靜態同步方法,誰先執行
普通同步方法鎖的對象是方法的調用者,靜態同步方法鎖的是類的Class對象,兩個方法用的不是同一個鎖,后調用的方法無需等待先調用的方法
8.兩部手機,一個普通同步方法,一個靜態同步方法,誰先執行
普通同步方法鎖的對象是方法的調用者,靜態同步方法鎖的是類的Class對象,兩個方法用的不是同一個鎖,后調用的方法無需等待先調用的方法
synchronized具體表現:
- 普通同步方法:鎖的是當前實例對象
- 靜態同步方法:鎖的是當前Class對象
- 同步方法塊:鎖的是synchronized括號的配置對象
集合類不安全
在多線程下使用多個線程向List里add值會拋出異常主要原因是add方法沒有加鎖
public class UnSafeList { public static void main(String[] args) { List<String> list=new ArrayList<>(); for(int i=1;i<=30;i++){ new Thread(()->{ list.add("12"); System.out.println(list); },String.valueOf(i)).start(); } } }
使用CopyOnWriteArrayList就不會拋出異常
public class UnSafeList { public static void main(String[] args) { List<String> list=new CopyOnWriteArrayList<>(); for(int i=1;i<=30;i++){ new Thread(()->{ list.add("12"); System.out.println(list); },String.valueOf(i)).start(); } } }
寫入時復制的思想(CopyOnWrite)
當多個調用者同時請求相同資源,只是進行讀取操作時共享該資源;當有調用者要修改資源時,就將該資源的副本給調用者,其他調用者見到的最初資源仍然保存不變,直到修改完成后才將復制的資源賦值給最初資源。
CopyOnWriteArrayList和Vector
Vector是在增刪改查方法上都加了鎖,而CopyOnWriteArrayList只是在增刪改上加了鎖,所以CopyOnWrite在讀方面的性能好於Vector
CopyOnWriteArrayset()與ConcurrentHashMap()同理
Callable接口
實現Callable接口重寫call方法時第三種實現多線程的方式,其與Runnable接口的區別是:
- 是否有返回值
- 是否拋出異常
- 方法不一樣,一個call()一個run()
public class CallableDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { MyCallable myCallable=new MyCallable(); FutureTask futureTask=new FutureTask(myCallable);//適配類 Thread thread=new Thread(futureTask); thread.start(); Integer integer=(Integer)futureTask.get(); System.out.println(integer); } }
public class MyCallable implements Callable { @Override public Integer call() throws Exception { System.out.println("調用call()方法"); return 1024; } }
常用的輔助類
CountDownLatch
public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { // 計數器 CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 1; i <= 6; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName()+"Start"); countDownLatch.countDown(); // 計數器-1 },String.valueOf(i)).start(); } //阻塞等待計數器歸零 countDownLatch.await(); System.out.println(Thread.currentThread().getName()+" End"); } }
CountDownLatch是一個計數器,當線程調用其countDown()時計數器-1;當線程調用await()方法時,線程就會阻塞,等到計數器變為0時,await()阻塞的線程就會被喚醒繼續執行。
CyclicBarrier
柵欄類,阻塞一組線程直到某一事件發生;所有的線程必須同時達到柵欄位置才能繼續執行,構造方法CyclicBarrier(int parties, Runnable barrierAction),parties表示攔截線程數,barrierAction線程都到達后執行事件
public static void main(String[] args) { // CyclicBarrier(int parties, Runnable barrierAction) CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{ System.out.println("召喚神龍成功"); }); for (int i = 1; i <= 7; i++) { final int tempInt = i; new Thread(()->{ System.out.println(Thread.currentThread().getName()+"收集了第"+ tempInt +"顆龍珠"); try { cyclicBarrier.await(); // 等待 } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }).start(); } }
Semaphore
信號量
public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore=new Semaphore(3); for (int i=1;i<=6;i++){ new Thread(()->{ try { semaphore.acquire(); // acquire 得到 System.out.println(Thread.currentThread().getName()+" 搶到了車位"); TimeUnit.SECONDS.sleep(3); // 停3秒鍾 System.out.println(Thread.currentThread().getName()+" 離開了車位"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); // 釋放這個位置 } },String.valueOf(i)).start(); } } }
acquire():線程調用acquire()時。要么成功獲取到了信號量(此時信號量-1),要么一直等待獲取
release():釋放信號量(此時信號量+1),喚醒在等待的線程
讀寫鎖
獨占鎖:此類型的鎖一次只能被一個線程持有,ReentrantLock和Synchronized都是獨占鎖
共享鎖:此類型的鎖可以被多個線程持有
讀寫鎖(ReentrantReadWriteLock):其讀鎖是共享鎖,寫鎖是獨占鎖,保證了在並發讀時的高效,讀鎖readLock();寫鎖writeLock()
阻塞隊列
阻塞隊列是一個隊列,當隊列是空的時,往隊列中獲取元素的操作會被阻塞,當隊列時滿的時,往隊列添加元素的操作會被阻塞。當滿足條件后,被掛起的線程會被自動喚起
常用:ArrayBlockingQueue
線程池
線程池:控制運行的線程數量,將任務放入隊列,在線程創建后啟動這些任務,如果線程數超過了最大數量,超出的線程排隊等候,其他線程執行完畢后再沖隊列中取出任務來執行。主要特點是線程復用,管理線程,控制最大並發數。
線程池是通過Executor框架實現的
三大方法:
- Executors.newFixedThreadPool(int n):創建一個有n個線程的線程池
public class ThreadPoolDemom { public static void main(String[] args) { ExecutorService threadPool=Executors.newFixedThreadPool(5); try { // 模擬有10個顧客過來銀行辦理業務,池子中只有5個工作人員受理業務 for (int i = 1; i <= 10; i++) { threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+" 辦理業務"); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); // 用完記得關閉 } }
- Executors.newSingleThreadExecutor():只有一個線程
public class ThreadPoolDemom { public static void main(String[] args) { ExecutorService threadPool=Executors.newSingleThreadExecutor(); try { // 模擬有10個顧客過來銀行辦理業務,池子中只有5個工作人員受理業務 for (int i = 1; i <= 10; i++) { threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+" 辦理業務"); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); // 用完記得關閉 } } }
- Executors.newCachedThreadPool():根據需要創建線程,再先構建的線程可用時會重用。
public class ThreadPoolDemom { public static void main(String[] args) { ExecutorService threadPool=Executors.newCachedThreadPool(); try { // 模擬有10個顧客過來銀行辦理業務,池子中只有5個工作人員受理業務 for (int i = 1; i <= 10; i++) { threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+" 辦理業務"); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); // 用完記得關閉 } } }
七大參數
- corePoolSize:核心線程數,當線程池中的線程數目達到corePoolSize后,就會把到達的任務放到緩存隊列當 中。
- maxumumPoolSize:最大線程數,線程池中最多能創建的線程數,必須大於等於1
- keepAliveTime:空閑線程保留時間
- TimeUnit:空閑線程保留時間單位
- BlockingQueue workQueue:存儲等待執行的任務
- ThreadFactory threadFactory:線程工廠,用於創建線程
- RejectedExecutionHandler:隊列已滿且任務量大於最大線程數的異常處理策略
ThreadPoolExecutor工作原理
CPU密集型程序(計算為主):線程數等於CPU數最好(頻繁切換線程會消耗時間)
IO密集型(輸入輸出為主):IO任務數等於線程數最好
四大函數式接口
public class FunctionDemo { public static void main(String[] args) { //消費型接口 Consumer<String> consumer=(s)->{ System.out.println(s); }; consumer.accept("dwx"); //供給型接口 Supplier<String> supplier=()->{ return "dwx"; }; System.out.println(supplier.get()); //函數型接口 Function<String,String> function=s->{ return s; }; System.out.println(function.apply("dwx")); //斷定型接口 Predicate<String> predicate=s->{ if(s.equals("dwx")){ return true; }else{ return false; } }; System.out.println(predicate.test("dwx")); } }
Stream流式計算
集合等是用來存儲數據,Stream流用來計算數據
一個流式處理首先調用stream()將其轉換成流,然后流有兩個操作:
- 中間操作:執行對集合的一些操作
- 終端操作:將結果進行封裝,返回需要的形式
類似車間流水線
public class StreamDemo { public static void main(String[] args) { User user1=new User(11,"dwx1",22); User user2=new User(12,"dwx2",23); User user3=new User(13,"dwx3",24); User user4=new User(14,"dwx4",25); List<User> list= Arrays.asList(user1,user2,user3,user4); //1.將list轉換未流list.stream() //2.然后過濾流,過濾符合條件的元素filter(Predicate函數接口) //3.map()實現映射 //4.foeEach()輸出forEach(s->{System.out.println(s);}),等價於forEach(System.out::println) list.stream() .filter(s->{return s.getId()%2==0;}) .map(s->{return s.getUserName().toUpperCase();}) .forEach(System.out::println); } }
分支合並
jdk1.7后,Java提供Fork/Join框架用於並行執行任務,思想是將大任務分割成若干個小任務,最后把各個小任務的結果匯總得到大任務的結果
該模型概念:線程池的每一個線程有自己的工作隊列,當自己隊列中的任務完成后,會去其他線程的工作隊列中偷取任務來執行,這樣就可以充分的利用資源
工作竊取
工作竊取算法是指某個線程自己的任務完成后去他竊取其他線程的任務來執行
線程里的任務隊列采用雙端隊列,來減少竊取任務的線程和被竊取任務線程之間的競爭,被竊取的任務從雙端隊列的頭部拿任務,而竊取任務的隊列從雙端對列的尾部拿任務
異步回調
讓被調用者立即返回一個引用,讓其在后台慢慢處理。此時調用者可以先處理其他任務,在真正需要數據的場合再去獲取數據
JMM(Java內存模型)
談談對volatie的理解
volatile是java虛擬機的輕量級同步機制。有三大特性:
- 保證可見性
- 禁止指令重排
- 不保證原子性
JMM抽象的概率,描述的是一組規則或者規范
JMM規定了內存主要分為主內存和工作內存,主內存是硬件的物理地址,工作內存是寄存器和高速緩存
Java線程在每次讀取和寫入操作都去訪問主內存會影響性能,因此JMM規定每條線程擁有各自的工作內存,工作內存中的變量是主內存變量中的拷貝。線程對工作內存的操作其他線程不可見,為了保證線程間同步,使用Volatile關鍵字即可,實現了一下兩個規則:
- 線程對變量進行修改后要立刻寫入到主內存中
- 線程對變量讀取時要從主內存去讀
內存交互操作
內存交互操作有8種,每個操作都是原子的,不可再分的
- lock鎖定:作用於主內存的變量,把一個變量標識為線程獨占狀態
- unlock解鎖:作用於主內存的變量,把鎖定的變量解鎖
- read讀取:作用於主內存的變量,把一個變量的值從主內存傳輸到工作內存
- load載入:作用於工作內存的變量,把read操作的變量從主內存放入工作內存
- use使用:作用於工作內存的變量,把工作內存中的變量傳輸個執行引擎
- assigin賦值:作用於工作內存的變量,它把一個從執行引擎中接受到的值放入工作內存的變量副本中
- store存儲:作用於與主內存的變量,將工作內存中的變量值傳輸到主內存
- write:作用於主內存的變量,將store操作的變量放入到主內存的變量中
JMM對這八種指令制定了如下規則
- read和load,store和write必須成對使用
- 線程不能丟棄最近的assign操作,工作內存變量數據改變了必須告知主內存
- 不允許線程將沒有assigin的數據從工作內存同步到主內存
- 不允許工作變量使用未初始化的變量。新的變量必須在主內存中誕生
- 一個變量同一時間只有一個線程對其lock
- 對一個變量進行lock操作會清空所有工作內存的此變量的值,在執行引擎要使用該變量前,必須重寫load或assign
- 一個變量沒被lock就不能進行unlock
- 對一個變量進行unlock操作之前,必須把此變量同步回內存中
volatile
volatile保證可見性
public class VolatileDemo { //不加volatile就會陷入死循環 private volatile static int num=0; public static void main(String[] args) throws InterruptedException { new Thread(()->{ while(num==0){ } }).start(); Thread.sleep(1000); num=1; System.out.println(num); } }
volatile不保證原子性
public class VolatileDemo { private volatile static int num=0; public static void add(){ num++; } public static void main(String[] args) throws InterruptedException { for(int i=1;i<=20;i++){ new Thread(()->{ for(int j=1;j<=1000;j++){ add(); } },String.valueOf(i)).start(); } while(Thread.activeCount()>2){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+""+num); } }
一部分值被覆蓋了
使用atomic包下的原子類實現原子性,num++是不安全的,使用getAndIncrement()替代
public class VolatileDemo { private volatile static AtomicInteger num=new AtomicInteger(); public static void add(){ num.getAndIncrement(); } public static void main(String[] args) throws InterruptedException { for(int i=1;i<=20;i++){ new Thread(()->{ for(int j=1;j<=1000;j++){ add(); } },String.valueOf(i)).start(); } while(Thread.activeCount()>2){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+""+num); } }
指令重排
計算機在執行程序時,為了提高性能,編譯器和處理器常常會對指令重排
在多線程環境中,由於線程是交替執行的,編譯器優化的指令重排會使兩個線程中使用的變量是否保證一致性無法確定,結果無法預測
volatile實現了禁止指令重排優化
內存屏障(內存柵欄)是一個CPU指令,有兩個作用:
- 保證特定操作的執行順序
- 保證某些變量的內存可見性(volatile就是利用了該特性)
在指令鍵插入一條內存屏障指令就會告訴CPU和編譯器不管什么指令都不能和這條內存屏障指令重排。即禁止內存屏障前后的指令執行重排優化。該指令還會強制刷出CPU緩存數據,所以線程讀取到的都是最新值
基於保守策略的內存屏障策略:
- 在每個volatile寫操作的前面插入一個StoreStore屏障。
- 在每個volatile寫操作的后面插入一個StoreLoad屏障。
- 在每個volatile讀操作的前面插入一個LoadLoad屏障。
- 在每個volatile讀操作的后面插入一個LoadStore屏障。
單例模式
餓漢式
public class Hungry { private int[] data1=new int[10]; private int[] data2=new int[10]; privare Hungry(){} private final static Hungry hungry=new Hungry(); public static Hungry getInstance(){ return hungry; } }
當代碼一運行就會生成一個Hungry實例,並且data1和data2會被放入內存,如果長時間不用就會造成內存浪費
懶漢式
public class Lazy { private Lazy(){} private static Lazy lazy; public static Lazy getInstance(){ if (lazy == null) { lazy = new Lazy(); } return lazy; } }
在多線程下,一些線程的單例會失效,且由於指令重排,會發生一些錯誤導致單例不完整。
使用DCL單例模式加volalite,可以避免問題
public class Lazy { private Lazy(){} private volatile static Lazy lazy; public static Lazy getInstance(){ if (lazy == null) { synchronized(Lazy.class) { if(lazy==null) { lazy = new Lazy(); } } } return lazy; } }
CAS
CAS比較並交換
public class CASDemo { public static void main(String[] args) { AtomicInteger atomicInteger=new AtomicInteger(5); //第一個參數為期望值,第二個參數為修改值,如果期望值=實際值,就將實際值修改,不等於就不改 System.out.println(atomicInteger.compareAndSet(5,2021)); System.out.println(atomicInteger.get()); System.out.println(atomicInteger.compareAndSet(12,2020)); System.out.println(atomicInteger.get()); } }
UnSafe類
UnSafe類是java的核心類,類中所有方法都是native修飾,即其方法都是直接調用操作系統底層資源執行任務的。
CAS(Compare And Swap)
CAS是一條CPU並發語句,用來判斷內存的某個位置的值是否是期望值,如果是則將其更改為新的值,該過程是原子的
CAS有三個操作數,內存值V,預期值A,和修改值B,當預期值A與內存值V相同時就將內存V修改為B,否則不斷重試或放棄。
CAS缺點
- 時間開銷大,while循環
- 只能保證一個共享變量的原子操作
- 導致ABA問題(ABA問題:另一個線程修改了內存值V為V1,然后又將其改回原來的值V,當前線程無法分辨內存值是否發生過改變)
解決ABA問題使用原子版本號引用AtomicStampedReference,相當於添加了一個版本號,當修改內存值是版本號就會發生改變,類似樂觀鎖
Java鎖的類別
公平鎖和非公平鎖
公平鎖:多個線程按照申請鎖的順序來獲取鎖,即排隊
非公平鎖:多個線程不是按照申請鎖的順序獲取鎖
可重入鎖
可重入鎖(遞歸鎖):程序外層獲取了鎖,內層也可以獲得該鎖(避免死鎖)ReentrantLock和Synchronized都是可重入鎖
自旋鎖
自旋鎖:嘗試獲取鎖的線程不會立即阻塞,而是采用循環的方式嘗試獲取鎖,減少線程上下文切換消耗