?博主主頁: 33的博客?
??文章專欄分類:JavaEE??
🚚我的代碼倉庫: 33的代碼倉庫🚚
🫵🫵🫵關注我帶你了解更多線程知識
目錄
- 1.前言
- 2.單例模式
- 2.1餓漢方式
- 2.2餓漢方式
- 3.阻塞隊列
- 3.1概念
- 3.2實現
- 4.定時器
- 4.1概念
- 4.2實現
- 5.線程池
- 5.1概念
- 5.2實現
- 6.總結
1.前言
在開發過程中,我們會遇到很多經典的場景,針對這些經典場景,大佬們就提出了一些解決方案,我們只需要按照這個解決方案來進行代碼的編寫,這樣就不會寫得很差。
2.單例模式
我們先了解什么是設計模式,在開發過程中,我們會遇到很多經典的場景,針對這些經典場景,大佬們就提出了一些解決方案,按照這個方式進行編程,代碼就不會很差,就例如下期中的棋譜,如果按照棋譜下棋也是不會下很爛的。
所謂單例就是單個實例(對象),那么怎么保證一個類只有一個對象呢?我們需要通過一些編程技巧來達成這樣的效果。
在一個類的內部提供一個實例,把構造方法設置為private避免再構造出新的實例
2.1餓漢方式
餓漢方式:
class Singleton{private static Singleton instance=new Singleton();public static Singleton getsingleton(){return instance;}private Singleton(){}
}
public class Demo17 {public static void main(String[] args) {Singleton s1=Singleton.getsingleton();//Singleton s2=new Singleton();}
}
上述代碼中,創建一個實例的時候在類加載的時候,太早就創建了,那可不可以晚一點呢?
2.2餓漢方式
懶漢方式:
class Singleton2{private static Singleton2 instance2=null;public static Singleton2 getInstance2(){if(instance2==null){instance2=new Singleton2();}return instance2;}private Singleton2(){};
}
但懶漢模式是不安全的,如下:
這樣就創建了兩個實例,所以我們需要對讀取和修改進行加鎖操作
class Singleton3{private static Singleton3 instance3=null;Object lock=new Object();public static Singleton3 getInstance2(){synchronized (lock){if(instance3==null){instance3=new Singleton3();}}return instance3;}
}
這樣就只有在調用getInstance2方法才會去創建對象,但是又引出了新的問題,其實是否創建對象,只需要在第一次調用這個方法的時候判斷,一旦創建好,以后都不用在再去判斷了,可這樣寫,每次調用這個方法都會去判斷,這樣就消耗不少資源。我們進行優化:
class Singleton3{private static Singleton3 instance3=null;Object lock=new Object();public static Singleton3 getInstance2(){if(instance3==null){synchronized (lock){if(instance3==null){instance3=new Singleton3();}}}return instance3;}private Singleton3(){};
}
大家以為到這兒,代碼完美了嗎?其實并不是,在new的時候可能會引起指令重排序問題,那么什么是指令重排序問題呢?指令重排序也是編譯器為了提高執行效率,做出的優化,在保持邏輯不變的前提下,可能對編譯器做出優化.
例如我們要去一個水果超市買香蕉、蘋果、火龍果、獼猴桃四種水果但它們在不同的展區。
優化前:
優化后
在通常情況下,在單線程中,指令重排序,就能夠保證邏輯不變的情況下,把程序的效率提高,但在多線程中就不一定了,可能會誤判。
new操作是可能觸發指令重排序
new可以分為3步:
1.申請內存空間
2.在內存空間上構造對象(構造方法)
3.把內存地址復制給instance引用。
如果內存進程優化:
1.申請內存空間
2.把內存地址復制給instance引用。
3.在內存空間上構造對象(構造方法)
那么該怎么解決這個問題呢?可以使用volatile讓其修飾instanse就可以保證,在修改instanse的時候就不會出現指令重排序問題。
class singleton{private static volatile singleton instance=null;public static singleton getinstance(){if (instance==null){synchronized (singleton.class){if (instance==null){instance=new singleton();}}}return instance;}private singleton(){}
}
public class Demo21 {public static void main(String[] args) {singleton s1=singleton.getinstance();}
}
這個時候才算真正的完成一個單例模式。
3.阻塞隊列
3.1概念
阻塞隊列也是多線程編程中比較常見的一種數據結構,它是一種特殊的隊列,它具有線程安全的特點,并且帶有阻塞特性。
阻塞隊列最大的意義就是用來實現“生產者消費者模型”
例如:一家人在一起包餃子,但是搟面杖只有一個,那么就指定一定人搟餃子皮就稱它為生產者,每搟一張皮,就放入圓盤中,其余人都包餃子稱為消費者,如果圓盤中沒有餃子皮了,消費者就要等待生產,如果圓盤中放滿了就要等待生產者就要等待消費。
那么為啥要引入“生產者消費者模型”呢?隊我們又有什么好處呢?
1.解耦合:就是降低兩個代碼塊的緊密程度
2.削峰填谷
那么在Java中,怎么實現阻塞隊列呢?在標準庫里,以及提供了線程的
public static void main(String[] args) throws InterruptedException {BlockingDeque<Integer> deque=new LinkedBlockingDeque<>();deque.put(1);deque.put(2);deque.put(3);System.out.println(deque.take());System.out.println(deque.take());System.out.println(deque.take());System.out.println(deque.take());}
最后一次出隊列,隊列已經空了,所以就會阻塞:
3.2實現
既然我們已經會使用阻塞隊列了,那我們能不能自己實現一下呢?我們底層可以采用循環數組來實現。
public class MyBlockingqueue {String[] arr=new String[20];private volatile int head=0;//后續中既會讀又會改,為了避免內存可見性+volatileprivate volatile int end=0;private volatile int size=0;public void put(String elem) throws InterruptedException {synchronized (this){if(end==arr.length){this.wait();return;}arr[end]=elem;end++;size++;this.notify();//喚醒因為隊列空導致的阻塞if(end==arr.length){end=0;}}}public String tack() throws InterruptedException {synchronized (this){if (size==0){this.wait();}String ret=arr[head];head++;size--;this.notify();//喚醒因為隊列滿導致的阻塞if(head==arr.length){head=0;}return ret;}}
}
4.定時器
4.1概念
定時器也是日常開發中常見的組件,約定一個時間,時間到達后就會執行某個邏輯。在Java標準庫中,有一個線程的標準庫。
public static void main(String[] args) {Timer timer=new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("3000");}},3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("2000");}},2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("1000");}},1000);System.out.println("定時器打開");
}
主線程執行schdule方法時,此時就把任務放到了timer中,此時timer中也也包含一個線程,叫做掃描線程,一旦時間到達就會給改線程安排任務。那么我們能不能自己實現呢?
4.2實現
1.需要定義一個類來描述一個任務,這個任務需要包含時間,和實際任務。
2.需要有一個數據結構,把任務全部存到數據庫中
3.Timer中需要一個線程來描述任務是否到達時間
一個任務類:
lass MyTimerTask implements Comparable<MyTimerTask>{private Runnable runnable;private long time;public MyTimerTask(Runnable runnable,long delay){this.runnable=runnable;this.time=System.currentTimeMillis()+delay;}public long gettime(){return time;}public Runnable getrun(){return runnable;}@Overridepublic int compareTo(MyTimerTask o) {return (int) (this.time-o.time);}
}
一個Timer類:
public class MyTimer {private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();Object locker=new Object();public void schedule(Runnable runnable,long time ){synchronized (locker){queue.offer(new MyTimerTask(runnable,time));locker.notify();}}public MyTimer(){Thread t=new Thread(()->{while (true){try {synchronized (locker){while (queue.isEmpty()){locker.wait();}MyTimerTask task= queue.peek();long curenttime=System.currentTimeMillis();if (curenttime>=task.gettime()){task.getrun().run();queue.poll();}else {locker.wait(task.gettime()-curenttime);}}}catch (InterruptedException e){e.printStackTrace();}}});t.start();}
}
測試類:
class M{public static void main(String[] args) {MyTimer myTimer=new MyTimer();myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("3000");}},3000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("2000");}},2000);System.out.println("開始");}
}
5.線程池
5.1概念
池,這個詞,是計算機中一種比較重要的思想方法,很多地方都涉及到,比如內存池,進程池,連接池等等。
線程池,就是指在使用第一個線程的時候就把其他線程線程一并創建好,后續如果想要使用這個其他線程,就不必再重新創建新的線程,直接從線程池中回去即可。
那么為啥線程創建好放在池子里后續再從池子中取,比新建線程的效率更高呢?
從池子中取是純用戶態的操作,而創建線程是用戶態+內核態相互配合完成的。如果一段程序是在系統內核中執行的就是叫內核態,否則為用戶態。當創建線程時,就需要調用系統api,進入內核進入一系列操作,但操作系統內核不僅僅是給該線程提供服務,也要給其他線程提供服務,那么這個效率就是非常低的了。
在Java標準庫中,提供了寫好的線程池,直接用即可。
public static void main(String[] args) {ExecutorService service= Executors.newCachedThreadPool();}
線程池對象并不是直接new的,而是調用一個方法返回線程池對象Executors.newCachedThreadPool()稱為工廠模式。
通常情況下創建一個對象需要用new關鍵字,new關鍵字會觸發類的構造方法,但構造方法具有局限性,例如:在一個類中,我即能用笛卡爾坐標系來表示一個點,又能有極坐標的方法表示一個:
class point{//笛卡爾坐標public point(double x,double y){}//極坐標public point(double a,double b){}
}
但如果要在一個類中實現多個構造方法,那么就要保證構造方法的參數不同,或者是類型不同。為了解決構造方法的局限性,我們就使用工廠設計模式。
工廠設計模式就是指,用一個單獨的類,再使用靜態普通方法代替構造方法做的事情。
class PointFactory{public static Point MackXY(){Point p=new Point();.......return p;}public static Point MackAB(){Point p=new Point();.......return p;}
}
在構造線程池中也有多種方法:
public static void main(String[] args) {//線程池是動態的,cache緩存用了之后不立即釋放ExecutorService service= Executors.newCachedThreadPool();//固定創建幾個線程ExecutorService service1=Executors.newFixedThreadPool(3);//相當于定時器,但不是一個掃描線程進程操作而是多個線程了ExecutorService service2=Executors.newScheduledThreadPool(4);//固定只有一個線程ExecutorService service3=Executors.newSingleThreadExecutor();}
上述多種方法都是對ThreadExecutor進行的封裝,這個類非常豐富,提供了很多參數,標準庫中上述多種方法實際給這個類填寫了不同的參數來構造線程。
具體看最后一種構造方法,因為包含了前面三種
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
int corePoolSize:核心線程數
int maximumPoolSize:最大線程數
long keepAliveTime:非核心線程在終止之前等待新任務的最長時間
TimeUnit unit:時間單位
BlockingQueue workQueue:阻塞隊列,存放線程池的任務
ThreadFactory threadFactory:用于創建新線程的工廠。
RejectedExecutionHandler handler:線程拒絕策略
RejectedExecutionHandler handler:線程拒絕策略
一個線程池中,能容納的線程數目已經達到最大上限,繼續再添加將有不同的效果:有以下4種效果
1.ThreadPoolExecutor.AbortPolicy:線程池直接拋出異常
2ThreadPoolExecutor.CallerRunsPolicy:新添加的任務由添加任務的線程自己執行
3.ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列中最老的任務
4.ThreadPoolExecutor.DiscardPolicy:丟棄當前新加的任務
5.2實現
public class MyThreadPool {//設置任務隊列BlockingDeque<Runnable> queue=new LinkedBlockingDeque<>();//任務放到隊列public void submit(Runnable runnable) throws InterruptedException {queue.put(runnable);}//線程執行public MyThreadPool(int n) throws InterruptedException {for (int i=0;i<n;i++){Thread t=new Thread(()->{try {Runnable runnable = queue.take();runnable.run();} catch (InterruptedException e) {e.printStackTrace();}});t.start();}}
}
class M{public static void main(String[] args) throws InterruptedException {MyThreadPool myThreadPool=new MyThreadPool(4);for (int i=0;i<100;i++){int id=i;myThreadPool.submit(new Runnable() {@Overridepublic void run() {System.out.println("i="+id);}});}}
}
6.總結
單例模式,阻塞隊列,定時器,線程池,是一些常用的多線程代碼,希望同學們能夠熟練掌握它們得使用方法,感興趣的同學也可以自己實現一下。
下期預告:多線程進階