設計模式
1.單例模式
????????1.1餓漢模式
????????1.2懶漢模式(單線程版)
????????1.3懶漢模式(多線程版本)
? ? ? ? 1.4懶漢模式(多線程版本進階版)
2.阻塞隊列
3.定時器
4.線程池
1.單例模式
????????設計模式是"軟性約束",不是強制的,可以遵守也可以不遵守,按照設計模式寫代碼使代碼不會太差
? ? ? ? 框架是"硬性約束",必須遵守
? ? ? ? 單例模式能保證某個類在程序中只保存唯一一個實例,而不會創建多個
? ? ? ? 單例模式具體的實現方式有很多,最常見的是"餓漢"和"懶漢"兩種
1.1餓漢模式
餓漢模式的餓代表急迫,一開始就把實例加載到內存中了
class singleton{private singleton(){};private static singleton instance=new singleton();public static singleton getInstance(){return instance;}
}
public class demo17 {public static void main(String[] args) {singleton s1=singleton.getInstance();singleton s2=singleton.getInstance();System.out.println(s1==s2);}
}
餓漢模式屬于線程安全的,不涉及修改操作,只有讀取操作
注釋:1.private singleton(){};將構造方法設為私有,可以使得這個類無法new出來,更安全
????????2.只有一個對外的接口,getInstance(){},更安全
????????3.這個實例在程序加載的時候就被初始化了,屬于類對象
1.2懶漢模式(單線程版)
懶漢模式的懶標識懶加載,提高效率
class singletonLazy{private singletonLazy(){};private static singletonLazy instance =null;public static singletonLazy getInstance(){if(instance==null){instance=new singletonLazy();}return instance;}}
public class demo18 {public static void main(String[] args) {singletonLazy s1=singletonLazy.getInstance();singletonLazy s2=singletonLazy.getInstance();System.out.println(s1==s2);}
}
這里是單線程版本的懶漢模式,屬于線程不安全
if(instance==null){
instance=new singletonLazy();
}
涉及到判定和修改操作,這個操作因為不是原子性的,所以會導致出現問題
此處實例的初始化并不是程序啟動時,而是第一次調用方法的時候初始化實例
?會提升性能,減少加載時間?? ? ? ? ? ? ? ? ? ? ?
單例模式只能避免別人失誤,無法應對別人的故意攻擊(如:序列化反序列化,反射)
先判定后修改是常見的線程不安全問題
1.3懶漢模式(多線程版)
class singletonLazy{//多線程版本private singletonLazy(){};private static Object Locker=new Object();private static singletonLazy instance =null;public static singletonLazy getInstance(){synchronized (Locker){if(instance==null){instance=new singletonLazy();}}return instance;}}
public class demo18 {public static void main(String[] args) {singletonLazy s1=singletonLazy.getInstance();singletonLazy s2=singletonLazy.getInstance();System.out.println(s1==s2);}
}
多線程版本需要考慮到線程的安全性,剛剛提到先修改后判定是不安全的,所以我們給它加上synchronize
把if和new包裹到一起
1.4懶漢模式(多線程版本進階)
僅僅是上述代碼依舊無法解決線程安全問題
上完鎖后也可能會造成阻塞狀態
會造成性能問題,所以可以在所外進行應該判定if(instance==null)
同時修改操作有可能造成內存可見性問題以及指令重排序問題,所以需要加上volatile關鍵字
class singletonLazy{//多線程版本進階版private singletonLazy(){};private static Object Locker=new Object();private static volatile singletonLazy instance =null;public static singletonLazy getInstance(){if(instance ==null){synchronized (Locker){if(instance==null){instance=new singletonLazy();}}}return instance;}}
public class demo18 {public static void main(String[] args) {singletonLazy s1=singletonLazy.getInstance();singletonLazy s2=singletonLazy.getInstance();System.out.println(s1==s2);}
}
2.阻塞隊列
阻塞隊列是一種特殊的隊列,遵守"先進先出的"原則(線程安全+阻塞特性)
阻塞隊列是一種線程安全的數據結構,并且具有以下的特點
1.當隊列滿的時候,繼續入隊列就會產生阻塞, 直到有其他線程從隊列中取走元素
2.當隊列為空的時候,繼續出隊列也會產生阻塞,直到有其他的線程從隊列里插入元素
阻塞隊列的一個典型的應用場景就是"生產者消費者模型",這是一種非常經典的開發模型
通常提到的"阻塞隊列"是代碼中的一個數據結構
但由于這個東西太好用了,以至于把這樣的數據結構單獨的封裝成一個服務器程序
并且在單獨的服務器上進行部署
此時,這樣的阻塞隊列,有了一個新的名字,"消息隊列"(Message Queue,MQ)
A只和隊列通信,B也只和隊列通信
A不知道B的存在,代碼中更沒有B的影子
B也不知道A的存在,代碼中沒有A的影子
A的數據量激增的情況下,A往隊列中寫入的數據變快了,但是B任然可以按照原有的速度來消費數據,阻塞隊列抗下這樣的壓力
生產者/消費者模型????????
? ? ? ? 這個模型的好處:
1.服務器之間的"解耦合"
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 模塊之間的關聯程度/影響程度
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 更希望見到的是低耦合
????????
2.阻塞隊列就相當于一個緩沖區,平衡了生產者和消費者的處理能力(削峰填谷)? ?
問題:
1).為啥一個服務器,收到的請求更多,就可能會掛(可能會崩潰)
一臺服務器就是一臺電腦,上面提供了一些硬件資源(包括但不限于,CPU,內存,硬盤,網絡帶寬...)
就算你這個機器,配置的再好,硬件資源也是有限的
服務器每次收到一個請求,處理這個請求的過程都需要執行一系列的代碼,在執行這些代碼過程中,就會需要消耗一定的硬件資源"CPU,內存,硬盤,網絡帶寬"
這些請求消耗的總的硬件資源的量,超過了機器能提供的上限,那么此時機器就會出現問題(卡死,程序直接崩潰等)
2).為什么A和消息隊列沒有掛,只有B會掛
A的角色是一個"網關服務器",收到客戶端的請求再把請求轉發給其他服務器
這樣的服務器里面的代碼,做的工作比較簡單(單純的數據轉發),消耗的硬件資源通常是更少
處理一個請求消耗的資源更少,同樣的配置下,就能支持更多的請求來處理
同理,隊列其實也是比較簡單的程序,單位請求消耗的硬件資源也是比較少的
B這個服務器,是真正干活的服務器,要真正的完成一系列的業務邏輯
這一系列工作,代碼量是非常的龐大,消耗的時間也是很多的,消耗的系統硬件資源也是更多的
類似的,MySQL這樣的數據庫處理每個請求的時候,做的工作就是比較多的,消耗的硬件資源也是比較多的,因此MySQL也是后端系統重容易掛的部分
對應的,像Redis這種內存數據庫,處理請求做的工作遠遠少于MySQL做的工作,消耗的資源更少,Redis就比MySQL更不容易掛
代價:
1)需要更多的機器
2)A和B之間的通信的延時會變長
標準庫中的阻塞隊列
在java標準庫中內置了阻塞隊列,如果我們需要在一些程序中使用阻塞隊列,直接使用標準庫中的即可
BlockingQueue是一個接口,真正實現的類是Linked/Array/priorityBlockingQueue.
put方法用于阻塞式的入隊列,take用于阻塞式的出隊列
BlockingQueue也有offer,poll,peek等方法,但是這些方法不帶有阻塞性質
生產者消費者模型
public class demo20 {//生產者消費者模型public static void main(String[] args) throws InterruptedException {BlockingQueue<Integer> queue =new ArrayBlockingQueue<>(1000);Thread thread1=new Thread(()->{//生產者int i=0;while(true){try {System.out.println("生產元素"+i);queue.put(i);i++;} catch (InterruptedException e) {e.printStackTrace();}}});Thread thread2=new Thread(()->{//消費者while(true){try {int num=queue.take();System.out.println ("消費元素"+num);Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});thread1.start();thread2.start();thread1.join();thread2.join();}
}
阻塞隊列的實現
1.通過"循環隊列"的方式來實現
2.使用synchronized進行加鎖控制
3.put插入元素的時候,判定如果隊列滿了,就進行wait.(注意,要在循環中進行wait,被喚醒時候不一定隊列就不滿了,因為同時是可能喚醒多個線程),以后只要出現wait,就使用while!!
4.take取出元素的時候,判定如果隊列為空,就進行wait(也是循環wait)
class MyBlockingQueue{private String[] data=null;private volatile int head=0;private volatile int tail=0;private volatile int size=0;public MyBlockingQueue(int capacity){data=new String[capacity];}public void put(String s) throws InterruptedException {//生產者synchronized (this){while(size==data.length){this.wait(); //滿了就不能再放了}data[tail]=s;size++;tail++;if(tail>=data.length){tail=0;}this.notify();}}public String take() throws InterruptedException {//消費者String s="";synchronized (this){while(data.length==0){this.wait();//如果沒有內容的時候就阻塞}s=data[head];head++;size--;if(head>=data.length){head=0;}this.notify();}return s;}
}
public class demo21 {public static void main(String[] args) throws InterruptedException {MyBlockingQueue queue =new MyBlockingQueue(1000);Thread thread1=new Thread(()->{//生產者int i=0;while(true){try {System.out.println("生產元素"+i);queue.put(""+i);i++;} catch (InterruptedException e) {e.printStackTrace();}}});Thread thread2=new Thread(()->{//消費者while(true){try {String num=queue.take();System.out.println ("消費元素"+num);Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});thread1.start();thread2.start();thread1.join();thread2.join();}
}
3.定時器(Timer)
????????定義:定時器也是軟件開發的一個重要組件,類似于一個"鬧鐘",達到一個設定的時間之后,就執行某個制定好的代碼
? ? ? ? 定時器是一種實際開發中非常常用的組件
? ? ? ? 比如網絡通信中,如果對方500ms內沒有返回數據,則會斷開連接嘗試重連
比如一個Map,希望里面的KEY在3s之后過期(自動刪除)
類似于這樣的場景就需要使用到定時器
標準庫中的定時器
標準庫中提供了Timer類,Timer類的核心方法是schedule
schedule 包含兩個參數,第一個參數指定即將要執行的任務代碼,第二個參數指定多長時間之后執行(單位為毫秒)
import java.util.Timer;
import java.util.TimerTask;public class demo24 {public static void main(String[] args) {//使用定時器Timer timer=new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello world3");}},3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello world2");}},2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello world1");}},1000);}
}
結果:
模擬實現定時器
class MyTimerTask implements Comparable<MyTimerTask>{Runnable runnable;long time;public MyTimerTask(Runnable runnable,long delay){this.runnable=runnable;this.time=delay+System.currentTimeMillis();}public long getTime(){return time;}public void run(){runnable.run();}@Overridepublic int compareTo(MyTimerTask o) {return (int)(this.time-o.time);}
}
Task類用來描述一個任務(作為Timer的內部類),里面包含一個runnable和一個time(毫秒時間戳)
這個對象需要放在優先級隊列中,因此需要實現Comparable接口
class MyTimer{private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();private Object Locker=new Object();MyTimer(){Thread thread=new Thread(()->{try {while(true){synchronized (Locker){while(queue.isEmpty()){//如果為空的時候就進入阻塞Locker.wait();}MyTimerTask currentmyTimeTask=queue.poll();if(System.currentTimeMillis()>=currentmyTimeTask.getTime()){currentmyTimeTask.run();}else{Locker.wait(currentmyTimeTask.getTime()-System.currentTimeMillis());queue.offer(currentmyTimeTask);}}}} catch (InterruptedException e) {e.printStackTrace();}});thread.start();}public void schedule(Runnable runnable,long delay){synchronized (Locker){MyTimerTask myTimerTask=new MyTimerTask(runnable,delay);queue.offer(myTimerTask);Locker.notify();}}
}
Timer實例中,通過priorityQueue來組織若干個Task對象
通過schedule來往隊列中插入一個個Task對象
Timer類中存在一個worker線程,一直不停的掃描隊首元素,看看是否能執行這個任務
全部代碼:
import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;class MyTimerTask implements Comparable<MyTimerTask>{Runnable runnable;long time;public MyTimerTask(Runnable runnable,long delay){this.runnable=runnable;this.time=delay+System.currentTimeMillis();}public long getTime(){return time;}public void run(){runnable.run();}@Overridepublic int compareTo(MyTimerTask o) {return (int)(this.time-o.time);}
}
class MyTimer{private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();private Object Locker=new Object();MyTimer(){Thread thread=new Thread(()->{try {while(true){synchronized (Locker){while(queue.isEmpty()){//如果為空的時候就進入阻塞Locker.wait();}MyTimerTask currentmyTimeTask=queue.poll();if(System.currentTimeMillis()>=currentmyTimeTask.getTime()){currentmyTimeTask.run();}else{Locker.wait(currentmyTimeTask.getTime()-System.currentTimeMillis());queue.offer(currentmyTimeTask);}}}} catch (InterruptedException e) {e.printStackTrace();}});thread.start();}public void schedule(Runnable runnable,long delay){synchronized (Locker){MyTimerTask myTimerTask=new MyTimerTask(runnable,delay);queue.offer(myTimerTask);Locker.notify();}}
}
public class demo25 {//模擬實現定時器public static void main(String[] args) {MyTimer myTimer=new MyTimer();myTimer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello world3");}},3000);myTimer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello world2");}},2000);myTimer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello world1");}},1000);}
}
?定時器的構成:
1.一個帶優先級隊列(不要使用PriorityBlockingQueue,容易死鎖)
2.隊列中每一個元素是一個Task對象\
3.Task中帶有一個時間屬性,隊首元素就是要執行的任務
4.同時有一個worker線程一直掃描隊首元素,看隊首元素是否需要執行
業界實現定時器,除了基于優先級隊列的方法之外,還有一個經典的實現方式,"時間輪" ,它也是一個巧妙設計的數據結構
定時器這個東西特別重要,特別常用,尤其是后端開發,和"阻塞隊列"類似,也會有專門的服務器,就是用來在分布式系統中實現定時器這樣的效果
hashmap? => redis
阻塞隊列? => 消息隊列
定時器? => 定時任務
4.線程池(ThreadPoolExecutor)
最初引入線程是因為線程太重了,頻繁的創建銷毀進程,開銷大
隨著業務上對于性能的要求越來越高,線程創建/銷毀的頻次越來越多,此時線程創建銷毀的開銷變的比較明顯,無法忽略不計了
線程池就是解決上述問題的常見方案
線程池就是把線程提前從系統中申請好,放到一個地方
后面需要使用線程的時候,直接從這個地方來取,而不是系統重新申請
線程用完了之后,也是還是回到剛才的地方
"池"---->本質上是為了提高程序的效率
內核態&用戶態
操作系統=操作系統內核態+操作系統配套的應用程序
操作系統內核-->操作系統的核心功能部分,負責完成一個操作系統的核心工作(管理)
包括為軟件提供穩定的運行環境和管理硬件
對應的,執行的很多代碼邏輯都是要用戶態的代碼和內核態的代碼配合完成的
應用程序有很多,這些應用程序都是由內核統一負責管理和服務
內核里的工作十分的繁忙--->所以提交給內核要做的任務可能是不可控的
????????從系統創建線程,就相當于讓銀行的人給我銀錢->這樣的邏輯就是調用系統api,由系統內核執行一系列邏輯來完成這個過程
????????直接從線程池里取,這就相當于是自助復印,整個過程都是純用戶態代碼,都是自己控制的,整個過程更可控,效率更高
因此通常認為,純用戶態操作,就比經過內核態操作的效率更高
java標準庫中提供了現成的線程池
標準庫提供了類ThreadPoolExcutor(構造方法有很多的參數)
帶入的包是java.util.concurrent? ?concurrent的意思是并發
它的構造方法中有7個參數
1.corePoolSize:核心線程數-------正式員工的數量(正式員工,一旦錄用,永不辭退)(最少有多少個)
2.maximunPoolSize:最大線程數---正式員工+臨時員工的數量(臨時工:一段時間不干活,就會被辭退)
最大線程數=核心線程數+非核心線程數
如果核心線程數和最大線程數一樣的話,將不會自動擴容
核心線程會始終存在于線程池內部
非核心線程會在繁忙的時候被創建出來,不繁忙了,就會把這些線程真正的釋放掉
3.keepAliveTimer:臨時工允許的空閑時間
4.unit:keepAliveTime的時間單位,可以是分,是秒,或者是其他
5.workQueue:工作隊列--傳遞任務的阻塞隊列,可以傳遞你的任務Runnable
6.threadFactory:創造線程的工廠,參與具體的線程創建工作,通過不同線程工廠創建出的線程相當于對一些屬性進行了不同的初始化設置
"工廠"是指"工廠設計模式"也是一種常見的設計模式
工廠設計模式是一種在創建類的實力時使用的設計模式,由于構造方法有"坑",通過工廠設計模式來填坑
就是Thread類的工廠類,通過這個類完成Thread的實例創建和初始化操作,此時ThreadFactory就可以針對線程池里的線程進行批量的設置屬性,此處一般不會進行調整,就使用標準庫提供的ThreadFactory的默認值就可以了---->ThreadFactory threadFactory
7.RejectedExecutionHandler:拒絕策略,如果任務量超過公司的負荷該如何處理
?????????7.1)AbortPolicy:超過負荷直接拋出異常
????????7.2)CallerRunsPolicy():調用者負責處理多出來的任務
????????7.3)DiscardOldestPolicy():丟棄隊列中最老的任務
????????7.4)DiscardPolicy():丟棄新來的任務
標準庫中的線程池
1.使用Executors.newFixedThreadPool(10)能創建出固定包含十個線程的線程池
2.返回值類型為ExecutorService?
3.通過ExecutorService.submit 可以注冊一個任務到線程池中
public class demo22 {public static void main1(String[] args) {ExecutorService service= Executors.newFixedThreadPool(4); // Executors的本質是ThreadPoolExecutor類的封裝//返回的類型是ExecutorServiceservice.submit(new Runnable() {//此處也可以使用lambda@Overridepublic void run() {System.out.println("Hello World!");}});service.shutdown();//service是前端線程,得主動結束}public static void main(String[] args) {ExecutorService service= Executors.newFixedThreadPool(4); // Executors的本質是ThreadPoolExecutor類的封裝//返回的類型是ExecutorServiceservice.submit(()->{//此處可以使用lambdaSystem.out.println("Hello World!");});service.shutdown();//service是前端線程,得主動結束}
}
Executors創建線程池的幾種方式
1.newFixedThreadPool:創建固定線程數的線程池
核心線程數和最大線程數一樣,所以不會擴容
2.newCachedThreadPool:創建線程數目動態增長的線程池
設置了非常大的線程數,自動擴容
3.newSingleThreadExcutor:創建只包含單個線程的線程池
4.newScheduledThreadPool:設定延遲時間后執行命令,或定期執行命令,是進階版的Timer
Executors的本質上是ThreadPoolExecutor類的封裝
這里的i報錯了,涉及的知識點是變量捕獲,只能捕獲final或者事實final
可以
創建一個新的變量id,來得到i的值,并且id是不會改變的,這樣即可
問題:使用線程池的時候,需要指定線程個數,線程個數如何指定,指定多少合適?
????????實際開發中,更建議的做法是通過實驗的方式找到一個合適的線程池的個數的值
? ? ? ? 給線程池設置不同的線程數,分別進行性能測試是,關注響應時間/消耗的資源指標,挑選一個比較合適的數值
線程池的模擬實現
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;class myThreadPool{BlockingQueue<Runnable> queue=new ArrayBlockingQueue<>(1000);public myThreadPool(int n){for(int i=0;i<n;i++){Thread thread=new Thread(()->{while(true){try {Runnable runnable =queue.take();runnable.run();} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();}}public void sunbmit(Runnable runnable){try {queue.put(runnable);} catch (InterruptedException e) {e.printStackTrace();}}
}
public class demo23 {public static void main(String[] args) {myThreadPool myThreadPool=new myThreadPool( 4);for(int i=0;i<1000;i++){int id=i;myThreadPool.sunbmit(()->{System.out.println("執行任務"+id+", "+Thread.currentThread().getName());});}}
}