安順網(wǎng)站開發(fā)網(wǎng)站推廣公司大家好
一)計(jì)算機(jī)是如何工作的?
指令是如何執(zhí)行的?CPU基本工作過程?
假設(shè)上面有一些指令表,假設(shè)CPU上面有兩個寄存器A的編號是00,B的編號是01
1)第一個指令0010 1010,這個指令的意思就是說把1010地址上面的數(shù)據(jù)給他讀取到A寄存器里面
2)第二個指令0001 1111,這個指令的意思是說把1111內(nèi)存地址上面的數(shù)據(jù)給他讀到寄存器B里面
3)第三個指令0100,1000,這個指令的意思是把A寄存器里面的內(nèi)容值寫到并保存到內(nèi)存地址1000的地方上面
4)1000 0100這個操作的意思就是將00寄存器和01寄存器的數(shù)值進(jìn)行相加,結(jié)果放到00寄存器里面
接下來我們來進(jìn)行查看一下,這些指令是怎么進(jìn)行工作的: 我們的上面的每一個地址的數(shù)據(jù)就是8bit,一個字節(jié)
1)假設(shè)從0號地址的位置開始進(jìn)行執(zhí)行,首先CPU中的CU就會從0號地址讀取內(nèi)存中的指令,來進(jìn)行解析,發(fā)現(xiàn)指令是00101110,根據(jù)上面的指令表可以看出我們此時執(zhí)行的指令就是00101110,我們就會把14號地址的數(shù)據(jù)加載到寄存器A里面,發(fā)現(xiàn)14號地址上面的數(shù)據(jù)就是00000011,此時數(shù)據(jù)就是3,此時我們就在CPU中有一個寄存器A來存放3的值
2)加下來我們從1號地址位置的地址處開始進(jìn)行執(zhí)行,00011111,我們的意思就是說,執(zhí)行l(wèi)oadB操作,把內(nèi)存地址是15上面的數(shù)據(jù)加載到寄存器B里面,內(nèi)存地址是15的數(shù)據(jù),去查發(fā)現(xiàn)數(shù)據(jù)是14,但是我們的計(jì)算機(jī)在1s內(nèi)可以執(zhí)行15條這樣的指令
3)第三條指令就是:10000100,就是將01和00寄存器中的數(shù)據(jù)取出來進(jìn)行相加,放到00這個寄存器里面,在上面說了01就是B,00就是A,加起來就是17,放到A里面
4)01001101,這個命令就是說把A寄存器的這個數(shù)據(jù)寫到1101這個內(nèi)存地址上面,1101翻譯成10進(jìn)制就是13,所以說我們就把A寄存器上面的這個17寫到了13這個內(nèi)存地址上面,17的二進(jìn)制就是00010001這個數(shù)據(jù)寫到了內(nèi)存地址是13的地方(內(nèi)存地址是13的地方原來是00000000)
CPU的工作流程:?
1)從內(nèi)存中讀取指令
2)解析指令
3)執(zhí)行指令
1)上面這個過程都是通過CU這一個控制單元來進(jìn)行實(shí)現(xiàn)的,這就是說在CPU在執(zhí)行代碼的時候的關(guān)鍵所在,咱們最終要進(jìn)行編寫的程序,最終都會被編譯器給翻譯成CPU所能識別的機(jī)器語言指令,在運(yùn)行程序的時候,操作系統(tǒng)就會把這樣的可執(zhí)行程序加載到內(nèi)存里面,CPU就依靠CU這個控制單元來進(jìn)行讀取,解析和執(zhí)行
2)如果說我們最終要是在配上條件跳轉(zhuǎn),我們就可以實(shí)現(xiàn)條件語句和循環(huán)語句,所以我們這一套完整的邏輯就可以通過二進(jìn)制指令來進(jìn)行表述出來,以上這就是所謂編程的本質(zhì)
3)所以說我們寫下來的每一行代碼,寫下來的每一個類,每一個方法,每一個變量最終都會被編譯器翻譯成CPU所執(zhí)行的指令的?,所以接下來通過CPU執(zhí)行這些指令,就會完成整個程序的工作過程
4)咱們自己電腦上面的idea還有QQ音樂都是依靠上面的工作過程來進(jìn)行執(zhí)行的
咱們CPU執(zhí)行的是工作指令,不同的CPU上面執(zhí)行的是不同的工作指令
1)外掛是一個單獨(dú)的程序, 對于一個游戲來說,源代碼是不會被公開的,雖然沒有源碼,但是具有可執(zhí)行程序,可執(zhí)行程序其實(shí)本質(zhì)上來說就是二進(jìn)制的機(jī)器指令,這里面就包含了一些程序運(yùn)行的時候涉及到的一些邏輯,我們就可以通過研究這里面的機(jī)器指令來進(jìn)行找到其中的一些關(guān)鍵邏輯
2)比如說找到一些,子彈扣血的關(guān)鍵邏輯,本質(zhì)上就是說是一些機(jī)器指令,算術(shù)邏輯加上一些邏輯判斷,我們就可以把這里面的條件給改了,或者把篡改的血量換成0
?二)操作系統(tǒng):操作系統(tǒng)是一個搞管理的軟件,就是一個軟件
操作系統(tǒng)是一個軟件,是計(jì)算機(jī)上面最復(fù)雜,最重要的軟件之一,比如說linux系統(tǒng),windows,mac系統(tǒng),安卓,IOS系統(tǒng),操作系統(tǒng)相當(dāng)于是給硬件設(shè)備和軟件系統(tǒng)相互之間架起了一座橋,軟件和硬件更好的進(jìn)行交互,更好地進(jìn)行相互配合
1)先進(jìn)行描述一個進(jìn)程(明確指出一個進(jìn)程上面的相關(guān)屬性),結(jié)構(gòu)體(PCB)
2)再進(jìn)行組織若干個進(jìn)程,使用一些數(shù)據(jù)結(jié)構(gòu),把很多描述進(jìn)程的信息都放到一起,方便進(jìn)行增刪改查,實(shí)現(xiàn)一個雙向鏈表把每一個進(jìn)程的PCB來進(jìn)行串起來
三)進(jìn)程:叫做任務(wù),process,運(yùn)行的exe文件;
進(jìn)程:把這些運(yùn)行起來的可執(zhí)行文件,就被稱之為進(jìn)程,CMD上面的輸入任務(wù)管理器,進(jìn)程就是跑起來的應(yīng)用程序,咱們電腦上面的exe就是被稱為可執(zhí)行文件(QQ.exe,CCtalk.exe),這些可執(zhí)行文件,都是文件,都是靜靜的躺在硬盤上面的,在你雙擊之前,這些文件都不會對你的系統(tǒng)產(chǎn)生任何影響,但是當(dāng)雙擊執(zhí)行這些exe文件的時候,操作系統(tǒng)就會將這些exe文件給加載到內(nèi)存里面,并開始執(zhí)行exe內(nèi)部的一些指令,這個可執(zhí)行程序就變成了進(jìn)程
這個進(jìn)程此時就和內(nèi)存空間綁定在了一起,讓CPU開始執(zhí)行一些exe內(nèi)部的指令,exe里面就存放了很多和這個程序相關(guān)的二進(jìn)制指令,編譯器生成的,還有一些重要的數(shù)據(jù),CPU可以識別的機(jī)器語言,點(diǎn)擊exe文件,就可以運(yùn)行起來的,exe里面有啥,那么內(nèi)存里面就有啥
這個時候就已經(jīng)把exe文件給執(zhí)行起來了,它就不再是躺平的咸魚了,而是開始進(jìn)行執(zhí)行一些具體的工作,就從靜態(tài)過程成為了動態(tài)過程
進(jìn)程管理:因?yàn)檫M(jìn)程被描述和組織:
四)線程的相關(guān)知識
創(chuàng)建線程注意的事項(xiàng):
創(chuàng)建線程的方式:以下創(chuàng)建線程的方式,本質(zhì)上都是相同的,都要借助Thread類,在內(nèi)核中創(chuàng)建出新的PCB,加入到內(nèi)核中的雙向鏈表中,只不過是描述任務(wù)的主體不一樣;
可以通過Thread類創(chuàng)建線程,最簡單的方法就是說創(chuàng)建一個類繼承于Thread類,并且重寫里面的run方法,本質(zhì)上是在創(chuàng)建繼承于Thread類的這樣的一個實(shí)例
1.1)注意:Thread類是在Java.lang包底下的,而我們的TimeUnit是在juc包底下的,咱們的jconsole是JAVA中自帶的一個調(diào)試工具
1.2)jconsole可以列舉出你系統(tǒng)上面的JAVA進(jìn)程,但是其他進(jìn)程不行,但是他是JDK的調(diào)試工具
前兩個進(jìn)程表示的是JConsole進(jìn)程,下來一個是Main進(jìn)程,下來是Idea進(jìn)程
java進(jìn)程一但進(jìn)行啟動,那么不僅僅是你自己代碼中的線程,還有一些其他的線程
咱們的Java進(jìn)程一旦進(jìn)行啟動,不僅僅有一些自己代碼中的線程,還有一些其他的線程(有的進(jìn)行網(wǎng)絡(luò)連接,有的進(jìn)行垃圾回收,有的進(jìn)行日志打印)
1)run方法里面描述了這個線程內(nèi)部要執(zhí)行那些代碼,每一個線程都是并發(fā)執(zhí)行的,每一個線程都有每一個線程的代碼,是一個完全并發(fā)的關(guān)系,就需要告訴線程你要執(zhí)行的代碼是什么,run方法的邏輯就是在新創(chuàng)建的線程中,要執(zhí)行的代碼
2)在這里并不是定義這個Thread類的子類,一重寫run方法,線程就被創(chuàng)建出來了,相當(dāng)于是老板把活安排好了,工人們還沒有開始干呢
3)需要進(jìn)行創(chuàng)建Thread子類的實(shí)例并且調(diào)用start方法,才真正的進(jìn)行開始執(zhí)行上面的run方法,所以說在進(jìn)行調(diào)用start方法之前,系統(tǒng)中是沒有進(jìn)行創(chuàng)建出線程的
4)因?yàn)閮蓚€進(jìn)程之間是不會相互干擾的,所以我們通過Thread類創(chuàng)建的線程,都是在同一個JAVA進(jìn)程里面的
package com; class MyThread extends Thread{public void run(){System.out.println("執(zhí)行run方法");} } public class Solution{public static void main(String[] args) {MyThread thread=new MyThread();thread.start();} }
Thread t1=new Thread(){@Overridepublic void run() {System.out.println(1);}};t1.start();
import java.sql.Time; import java.util.concurrent.TimeUnit; class MyThread extends Thread{public void run(){try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("我是線程1里面的方法");} } class TestThread extends Thread{public void run(){try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("我是線程2里面的方法");} } public class Solution{public static void main(String[] args) {MyThread t1=new MyThread();t1.start();TestThread t2=new TestThread();t2.start();} }
1)如果說在循環(huán)中不加任何限制,那么循環(huán)就會轉(zhuǎn)得非???#xff0c;就導(dǎo)致我們打印的東西太多了,根本看不過來
2)所以可以加上一個sleep操作,讓這個線程強(qiáng)行進(jìn)行休眠一段時間,這個休眠操作就是讓線程強(qiáng)制進(jìn)入到阻塞狀態(tài),單位是ms,意思就是說在指定的毫秒之內(nèi),這個線程不會到CPU上面執(zhí)行
3)InterruptedException就是說線程被打斷的異常
4)在一個進(jìn)程里面,至少會有一個線程,在一個JAVA進(jìn)程里面,也是說會至少會有一個調(diào)用main方法的線程,這個線程不是你自己寫的,而是你的系統(tǒng)自動搞出來的,自己創(chuàng)建的線程和main線程就是在并發(fā)執(zhí)行的,宏觀上面看起來同時執(zhí)行,這里面的并發(fā)就是指并發(fā)+并行,在宏觀上面是無法進(jìn)行區(qū)分并發(fā)和并行,這完全取決于操作系統(tǒng)的調(diào)度執(zhí)行
4)在的上述代碼中,現(xiàn)在有兩個線程,都是打印個一條,那么就直接休眠1s,但是當(dāng)1s時間到了以后,會先執(zhí)行誰呢?結(jié)論就是不知道,這個順序并不能完全進(jìn)行確定,所以說操作系統(tǒng)內(nèi)部對于線程之間的調(diào)度順序,在宏觀上面可以認(rèn)為是隨機(jī)的,把這個線程以內(nèi)的隨機(jī)調(diào)度,稱之為搶占式執(zhí)行,線程之間在搶,就存在了太多的不確定因素
1)通過顯示創(chuàng)建一個類,實(shí)現(xiàn)Runnable接口,然后再把這個繼承runnable的實(shí)例化對象關(guān)聯(lián)到Thread的實(shí)例上;也可以通過匿名內(nèi)部類的方式來進(jìn)行創(chuàng)建;
static class myrunnable implements Runnable{public void run(){System.out.println("我是線程");}}public static void main(String[] args) throws InterruptedException{Thread t1=new Thread(new myrunnable());t1.start(); }
2)通過runnable匿名內(nèi)部類的方式創(chuàng)建一個線程,重寫run方法,在直接把這個Runnable類創(chuàng)建的對象關(guān)聯(lián)到Thread里面,這種寫法和單獨(dú)創(chuàng)建一個類,再繼承Thread沒有任何區(qū)別;
直接通過Runnable來進(jìn)行描述一個線程執(zhí)行的具體任務(wù),進(jìn)一步的在把描述好的任務(wù)交給Thread實(shí)例
1)Runnable myrunnable=new Runnable(){public void run() {System.out.println("我是一個線程");}};Thread t1=new Thread(myrunnable);t1.start();}} 在這里面主要是說new出來的Runnable接口,創(chuàng)建繼承于runnable接口的類的實(shí)例 同時將new出來的Runnable實(shí)例傳給Thread類的構(gòu)造方法 2)Thread thread =new Thread(new Runnable() {@Overridepublic void run() {System.out.println("我是一個線程");}});thread.start();
3)通過lamda的表達(dá)式的方式創(chuàng)建一個線程,類似于通過匿名內(nèi)部類的方式來進(jìn)行創(chuàng)建,只是通過lamda表達(dá)式來代替Runnable接口?
new Comparator<Integer>(){@Overridepublic int compare(Integer o1, Integer o2) {return o1-o2;}};
public class Hello {public static void main(String[] args) throws InterruptedException{Thread t1=new Thread(()->{System.out.println("我是一個線程");});t1.start(); 函數(shù)式接口}}
1)咱們的匿名內(nèi)部類,其中的Comparable接口,Comparator接口,都是可以寫成匿名內(nèi)部類的方式
2)咱們上面的這個匿名內(nèi)部類的寫法就是說進(jìn)行創(chuàng)建了一個匿名內(nèi)部類,實(shí)現(xiàn)了Comparator接口或者繼承于Thread類,同時我們進(jìn)行重寫了run方法,同時還new出了這個匿名內(nèi)部類的實(shí)例
3)Runnable這一種寫法要更好一些,因?yàn)榭梢宰尵€程和線程執(zhí)行的任務(wù)可以更好地進(jìn)行解耦,所以說在寫代碼的時候,要高內(nèi)聚,低耦合,同類的功能的代碼放在一起,不同的功能模塊之間盡量不要有太多的關(guān)聯(lián)關(guān)系,Runable只是單純的描述了一個任務(wù),至于這段代碼是有一個線程來進(jìn)行執(zhí)行,進(jìn)程,線程池,協(xié)程來進(jìn)行執(zhí)行,Runnable本身并不關(guān)心,Runnable本身的代碼也不會進(jìn)行關(guān)心,以后的代碼改動更小,所以說這種寫法要更好一些
class Hello{public static void main(String[] args) throws InterruptedException{long beg1=System.currentTimeMillis();Thread t1=new Thread(){public void run(){long a=0;for(long i=0;i<1000000000;i++){a++;}}};Thread t2=new Thread(){public void run(){long b=0;for(long j=0;j<1000000000;j++){b++;}}};t1.start();t2.start();t1.join();t2.join();long beg2=System.currentTimeMillis();System.out.println("執(zhí)行時間為");System.out.println(beg2-beg1); 1)此時我們記錄時間是在main線程里面來進(jìn)行記錄的,也是在主線程里面執(zhí)行的,main線程和t1線程和t2線程是一種并發(fā)執(zhí)行的關(guān)系,我們此處就認(rèn)為t1和t2還沒有執(zhí)行完成呢,main線程就進(jìn)行記錄時間,這顯然是不準(zhǔn)確的 2)此時我們的正確做法應(yīng)該是讓我們的main線程等待t1線程和t2線程全部執(zhí)行完成了,再來進(jìn)行執(zhí)行我們的main線程} }
public static void main(String[] args){long beg1=System.currentTimeMillis();int a=0;for(long i=0;i<1000000000;i++){a++;}int b=0;for(int j=0;j<1000000000;j++){b++;}long beg2=System.currentTimeMillis();System.out.println("執(zhí)行時間為");System.out.println(beg2-beg1);} }
上面的join效果就是t1線程和t2線程執(zhí)行完成之后,再來執(zhí)行main線程
1)咱們的join的效果就是等待對應(yīng)線程結(jié)束:t1.join就是讓main線程等待t1線程結(jié)束,t2.join()就是說讓main線程等待t2線程結(jié)束,上述的這兩個線程在我們的底層到底是在并行執(zhí)行還是在并發(fā)執(zhí)行,這是不確定的,只有真正的兩個線程在并行執(zhí)行的時候,效率才會有顯著的提升,但是肯定要比單線程執(zhí)行的更快
2)如果說進(jìn)行計(jì)算的count值太小,那么此時你創(chuàng)建線程本身也是有開銷的呀,你的主要的時間就花在創(chuàng)建線程上面了,光你創(chuàng)建兩個線程就用了50ms,但是你計(jì)算值的過程就使用了10ms,此時肯定是得不償失的,只有你的任務(wù)量太大的時候,多線程才有優(yōu)勢,只有說進(jìn)行創(chuàng)建的任務(wù)量的總時間大于線程創(chuàng)建的時間,才說多線程可以提高效率
1)主線程還是一直向下走,但是新線程會執(zhí)行run方法,對于新線程來說,run方法執(zhí)行完了,新線程就結(jié)束了,對于主線程來說,main方法執(zhí)行完,主線程就結(jié)束了
2)線程之間,是并發(fā)執(zhí)行的關(guān)系,誰先執(zhí)行,誰后執(zhí)行,誰執(zhí)行到哪里讓出CPU,都是不確定的,作為程序員是無法感知的,全權(quán)有操作系統(tǒng)的內(nèi)核負(fù)責(zé),例如當(dāng)創(chuàng)建一個新線程的時候,接下來是主線程先執(zhí)行,還是新線程,是不好保證的;
3)執(zhí)行join方法的時候,該線程會一直阻塞,一直阻塞到對應(yīng)線程結(jié)束后,才會繼續(xù)執(zhí)行,本質(zhì)上來說是為了控制線程執(zhí)行的先后順序,而對于sleep來說,誰調(diào)用誰就會阻塞;
4)主線程把任務(wù)分成幾份,每個線程計(jì)算自己的一份任務(wù),當(dāng)所有的任務(wù)被計(jì)算完畢后,主線程再來匯總(就必須保證主線程是最后執(zhí)行完的線程)
5)獲得當(dāng)前對象的引用 Thread.currentThread()
6)如果線程正在運(yùn)行,執(zhí)行計(jì)算其邏輯,此時就在就緒隊(duì)列排序呢,調(diào)度器就會在就緒隊(duì)列找出合適的PCB讓他在CPU執(zhí)行,如果某個線程調(diào)用Sleep就會讓對應(yīng)的PCB進(jìn)入阻塞隊(duì)列,無法上CPU;
7 對于sleep讓其進(jìn)入阻塞隊(duì)列的時間是有限制的,時間到了之后,就會被系統(tǒng)把PCB那回到原來的就緒隊(duì)列中了;
t1.start(); t1.join(); t2.start(); t2.join(); 在這種情況下:t1,t2是串行執(zhí)行的
8)join被恢復(fù)的條件是對應(yīng)的線程結(jié)束
1)Thread類中的常見用法,Thread類是用于管理線程的一個類,換句話來說,每一個線程都有唯一的Thread類進(jìn)行關(guān)聯(lián)
2)Thread的常見構(gòu)造方法:
Thread() 創(chuàng)建線程對象 Thread(Runnable target) 借助Runnable對象創(chuàng)建線程對象 Thread(String name),創(chuàng)建線程對象,并命名; Thread(Runnable target,String name)通過runnable來進(jìn)行創(chuàng)建線程對象,并且進(jìn)行命名 有名字的構(gòu)造方法就是為了方便調(diào)試
3)給線程起一個名字,本質(zhì)上是為了方便程序員來進(jìn)行調(diào)試,我們起一個啥樣的名字是不會影響線程本身的執(zhí)行的,僅僅只是影響程序員來進(jìn)行調(diào)試,我們可以在工具中看到每一個線程以及名字,這樣就很容易在調(diào)試中對線程進(jìn)行區(qū)分,只是程序調(diào)試的小功能,并不會對代碼本身的功能造成影響
4)我們在C:\Program Files\Java\jdk1.8.0_301\bin中的jconsole.exe就可以羅列出我們系統(tǒng)上面的Java進(jìn)程,jconsole.exe就是一個方便與程序員進(jìn)行調(diào)試的工具
import java.util.concurrent.TimeUnit; class MyRunnableT1 implements Runnable{public void run(){while(true){try {TimeUnit.SECONDS.sleep(1000);System.out.println("我是一個任務(wù)");} catch (InterruptedException e) {e.printStackTrace();}}} } class MyRunnableT2 implements Runnable {public void run() {try {TimeUnit.SECONDS.sleep(1000);System.out.println("我也是一個任務(wù)");} catch (InterruptedException e) {e.printStackTrace();}} } public class Main {public static void main(String[] args) {Thread t1=new Thread(new MyRunnableT1(),"ThreadT1");Thread t2=new Thread(new MyRunnableT2(),"ThreadT2");t1.start();t2.start();} }
線程中斷:讓一個線程停下來,線程停下來的關(guān)鍵,是讓先成對應(yīng)的Run方法執(zhí)行完,在這里面有一種特殊的情況,是對于這個main線程,對于main線程來說,main方法執(zhí)行完成,整個線程才會執(zhí)行完成
1)先把當(dāng)前任務(wù)執(zhí)行完,再來結(jié)束線程
2)任務(wù)還在執(zhí)行,被強(qiáng)制結(jié)束
1)通過自己手動的來進(jìn)行設(shè)置一個標(biāo)志位,這就是自己創(chuàng)建的變量,來進(jìn)行控制線程是否要結(jié)束,搞一個boolean類型的變量或者是int類型的變量,咱們就可以通過標(biāo)志位,在其他代碼中控制這個標(biāo)志位的值,來進(jìn)行決定當(dāng)前線程是否要結(jié)束
static private boolean flag=true;public static void main(String[] args)throws InterruptedException {Thread t1 =new Thread(){public void run(){while(flag==true){System.out.println("正在發(fā)財(cái)");try{Thread.sleep(500);}catch(InterruptedException e){e.printStackTrace();break;}}System.out.println("發(fā)財(cái)結(jié)束");}};t1.start();Thread.sleep(5000);System.out.println("有內(nèi)鬼");flag=false; //我們要是將這個flag改成false,那么此時這個循環(huán)就退出了,進(jìn)一步的,run方法就執(zhí)行完畢了,再進(jìn)一步就是線程結(jié)束了} }
此處因?yàn)槎鄠€線程在共同用同一塊虛擬地址空間,因此main線程修改的flag變量和線程判定的flag變量是同一個值,是同一塊內(nèi)存空間,適合我們最終的線程的特點(diǎn)來進(jìn)行修改這個代碼的,但是在不同的虛擬地址空間里面,這樣的代碼就有可能會失效
使用標(biāo)準(zhǔn)庫中的內(nèi)置的標(biāo)記
一)獲取線程中內(nèi)置的標(biāo)記位:
通過:Thread.interrupted(),這個是一個靜態(tài)的方法
或者是:Thread.currentThread().isInterrupted(),這個是一個實(shí)例方法,其中可以通過Thread.currentThread()來進(jìn)行獲取到當(dāng)前線程的實(shí)例
二:修改線程中內(nèi)置的標(biāo)記位:
線程名字.interrupt()來進(jìn)行執(zhí)行中斷
1)public void? interrupted() 中斷對象關(guān)聯(lián)的線程,如果線程正在阻塞,那么以異常的方式來進(jìn)行通知,否則設(shè)計(jì)標(biāo)志位
2)Thread.currentThread().IsInterrupted()這個方法的判定的標(biāo)志位是Thread的普通成員,每一個實(shí)例都有自己的標(biāo)志位;
關(guān)于線程的實(shí)例.interrupted()方法的時候,可能會導(dǎo)致兩種效果:
1)如果說當(dāng)前這個線程處于就緒狀態(tài),那么會設(shè)置線程的標(biāo)記位設(shè)置成是true;
2)如果當(dāng)前這個線程處于阻塞狀態(tài)(sleep休眠),那么就會觸發(fā)一個InterruptedException異常,讓我們當(dāng)前的線程從阻塞狀態(tài)被喚醒;
3)如果說處于sleep狀態(tài),調(diào)用Interrupt方法,一旦觸發(fā)了異常之后,就進(jìn)行了catch語句塊,在catch中就單純打了個日志,可是咱們的標(biāo)志位并沒有真正的進(jìn)行修改,然后線程就繼續(xù)在循環(huán)里面運(yùn)行了,不能起到線程終止的作用
4)如果代碼中沒有任何sleep或者其他阻塞操作,只是做一個循環(huán)判定就夠了,直接就可以中斷線程,如果說我們當(dāng)前的run方法有阻塞操作(sleep),那么當(dāng)我們的程序執(zhí)行了Interrupt方法,并不會真正的修改標(biāo)志位,只會發(fā)生一個異常,就需要在catch語句塊里面做出進(jìn)一步的處理,才可以使我們的線程進(jìn)行終止;
當(dāng)使用Thread.currentThread().IsInterrupted()當(dāng)作循環(huán)標(biāo)志位的時候:?
現(xiàn)象:剛一開始程序里面會不斷地打印出Hello Thread這樣的代碼日志,但是當(dāng)?shù)闹骶€程調(diào)用interrupt方法的時候,由于當(dāng)前線程處于休眠狀態(tài),當(dāng)前線程就會出現(xiàn)InterruptedException異常,不會修改標(biāo)志位,但是當(dāng)try代碼語句出現(xiàn)異常的時候,就會立即進(jìn)入到catch語句塊里面,打印對應(yīng)的異常調(diào)用棧
1)但是這個代碼絕大部分情況下是在休眠的狀態(tài)下阻塞,所以在代碼中直接改成break就可以了,就可以起到中斷線程的作用
2)此處的中斷,希望是可以立即起到最終的效果,但是如果線程是本身處于阻塞狀態(tài)下,此時我們本身進(jìn)行設(shè)置標(biāo)志位就不可以起到及時喚醒的效果
3)此處我們當(dāng)前的線程處于休眠狀態(tài),那么當(dāng)我們調(diào)用這個interrupted方法的時候,就會使這個sleep觸發(fā)一個異常,那么這就會導(dǎo)致當(dāng)前線程從阻塞狀態(tài)被喚醒,咱們的sleep狀態(tài)被喚醒之后出現(xiàn)異常只是在catch中打印了一個日志就沒有了,直接就進(jìn)入到下一次循環(huán)了printStackTrace只是打印當(dāng)前代碼中出現(xiàn)異常位置的調(diào)用棧,直接就繼續(xù)運(yùn)行了,這個情況是并不科學(xué)的
public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{while(!Thread.currentThread().isInterrupted()){System.out.println("Hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) { //我們應(yīng)該在這個代碼中寫上一句,當(dāng)我們的程序中出現(xiàn)InterruptedException之后,雖然線程從阻塞狀態(tài)到運(yùn)行狀態(tài)之后,我們的線程應(yīng)該終止運(yùn)行e.printStackTrace();}}}); //在我們的主線程中,我們調(diào)用interrupt方法,來進(jìn)行中斷線程,t.interrupt的意思就是讓t線程被中斷t1.start();Thread.sleep(5000);t1.interrupt();}
4)比如說我正在打游戲,我的媽媽讓我去下樓買一瓶醬油,我的處理方式就是如下:
4.1)答應(yīng)一下,好嘞,然后接著打
4.2)答應(yīng)一下,好嘞,立即放下游戲就去
4.3)答應(yīng)一下,好嘞,打完這一局就去
第一種情況就是說處于休眠狀態(tài),但是經(jīng)過打斷之后回到就緒狀態(tài)之后,還是繼續(xù)執(zhí)行程序
第二種情況就是說處于休眠狀態(tài),但是經(jīng)過打斷之后回到就緒狀態(tài)之后,直接break了
第三種情況就是說處于休眠狀態(tài),但是經(jīng)過打斷之后回到就緒狀態(tài)之后,要做一些收尾工作,最好要再finallly語句塊中執(zhí)行一段邏輯,再去打醬油或者是說咱們上述那一種使用標(biāo)志位的方式來進(jìn)行線程的中斷,雖然標(biāo)志位在線程外邊被修改了,但是我們還是要等到當(dāng)前循環(huán)中的邏輯結(jié)束之后再來退出循環(huán),結(jié)束run方法
中斷標(biāo)記想象成是一個boolean值,初始情況為false,如果外部方法調(diào)用interrupt方法,就會把這個終端標(biāo)記設(shè)成true;
但是Thread.currentThread.isInterrupted()方法是屬于Thread實(shí)例的方法,每一個線程都有一個標(biāo)志位,還是說自己判斷自己是否結(jié)束當(dāng)然還是比較好的;對于Thread.currentThread().interrupted()來說,經(jīng)過一次打斷后,會徹底的改成true;
public class Main{public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{while(!Thread.interrupted()){System.out.println("線程1正在執(zhí)行");}System.out.println("線程1終止執(zhí)行");});Thread t2=new Thread(()->{while(!Thread.interrupted()){System.out.println("線程2正在執(zhí)行");}System.out.println("線程2終止執(zhí)行");});t1.start();t2.start();Thread.sleep(2000);t1.interrupt();} }
線程等待:因?yàn)榫€程與線程之間,調(diào)度順序是完全不確定,它取決于操作系統(tǒng)本身調(diào)度器的一個實(shí)現(xiàn),但是有時候我們希望這個順序是可控的,此時的線程等待,就是一種方法,用來控制線程結(jié)束的先后順序;
1)線程之間,調(diào)度順序是不確定的,線程之間的執(zhí)行是按照調(diào)度器來進(jìn)行安排執(zhí)行的,這個過程是無序,隨機(jī)的,有些時候,但是這樣不太好,要控制線程之間的執(zhí)行順序,先進(jìn)行執(zhí)行線程1,再來執(zhí)行線程2,再來執(zhí)行線程3;
2)線程等待就是其中一種控制線程執(zhí)行的先后順序的一種手段,此處我們所說的線程等待,就是我們說的控制線程結(jié)束的先后順序
3)當(dāng)我們進(jìn)行調(diào)用join的時候,哪個線程調(diào)用的join,那個線程就會阻塞等待,直到對應(yīng)的線程執(zhí)行完畢,也就是對應(yīng)線程run方法執(zhí)行完之后,
4)在調(diào)用join之后,對應(yīng)線程就會進(jìn)入阻塞狀態(tài),暫時這個線程無法在CPU上面執(zhí)行,就暫時停下了,不會再繼續(xù)向下執(zhí)行了,就是讓main線程暫時放棄CPU,暫時不往CPU上面調(diào)度,往往會等待到時機(jī)成熟
5)Sleep等待的時機(jī)是時間結(jié)束,而我們的join等待的時機(jī)是當(dāng)我們的(t.join),t線程中的run方法執(zhí)行完了,main方法才會繼續(xù)執(zhí)行
public class Main{public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for(int i=0;i<5;i++){System.out.println("hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t1.start(); //我們在主線程中就可以使用一個等待操作來進(jìn)行等待t線程執(zhí)行結(jié)束try{t1.join();}catch(InterruptedException e){e.printStackTrace();}} }
join執(zhí)行到這一行,就暫時停下了,不會繼續(xù)向下執(zhí)行,當(dāng)前的join方法是main方法調(diào)用的,針對這個t線程對象進(jìn)行調(diào)用的,此時就是讓main等待t,我們這是阻塞狀態(tài),調(diào)用join之后,main方法就會暫時進(jìn)入阻塞狀態(tài)(暫時無法在CPU上執(zhí)行);
join默認(rèn)情況下是死等,只要對應(yīng)的線程不結(jié)束,那么我們就進(jìn)行死等,里面可以傳入一個參數(shù),指定時間,最長可以等待多久,等不到,咱們就撤
Thread.currentThread()表示獲取當(dāng)前對象的實(shí)例
相當(dāng)于在Thread實(shí)例中run方法中直接使用this
操作系統(tǒng)管理線程: 1)描述 2)組織:雙向鏈表 就緒隊(duì)列:隊(duì)列中的PCB有可能隨時被操作系統(tǒng)調(diào)度上CPU執(zhí)行
1)當(dāng)前這幾個狀態(tài),都是Thread類的狀態(tài)和操作系統(tǒng)中的內(nèi)部的PCB的狀態(tài)并不是一致的;
2)然后我們從當(dāng)前join位置啥時候才可以向下執(zhí)行呢?也就是說恢復(fù)成就緒狀態(tài)呢?就是我們需要等待到當(dāng)前t線程執(zhí)行完畢,也就是說t的run方法執(zhí)行完成了,通過線程等待,我們就可以讓t先結(jié)束,main后結(jié)束,一定程度上干預(yù)了這兩個線程的執(zhí)行順序
3)我們此時還是需要注意,優(yōu)先級是咱們系統(tǒng)內(nèi)部,進(jìn)行線程調(diào)度使用的參考量,咱們在用戶代碼層面上是控制不了的,這是屬于操作系統(tǒng)內(nèi)核的內(nèi)部行為,但是我們的join是控制線程代碼結(jié)束之后的先后順序
4)就是說帶有參數(shù)的join方法的時候,就會產(chǎn)生阻塞,但是這個阻塞不會一直執(zhí)行下去,如果說10s之內(nèi),t線程結(jié)束了,此時的join會直接進(jìn)行返回,但是假設(shè)我們此時的10S之后,t線程仍然不結(jié)束,那么join也直接返回,這就是超時時間
五)線程安全問題:
1)咱們說的就緒狀態(tài)和阻塞狀態(tài),其實(shí)是針對系統(tǒng)級別的狀態(tài)(PCB)
2)線程不安全:由于多線程并發(fā)執(zhí)行,導(dǎo)致代碼中出現(xiàn)了BUG,因?yàn)椴僮飨到y(tǒng)在進(jìn)行調(diào)度線程的時候是隨機(jī)的,正是因?yàn)檫@樣的隨機(jī)性,才會導(dǎo)致程序的執(zhí)行會出現(xiàn)BUG
3)如果因?yàn)檫@樣的隨機(jī)性引入了BUG,那么就認(rèn)為代碼是線程不安全的,如果因?yàn)檫@樣的調(diào)度隨機(jī)性,沒有引入BUG,那么就說明代碼是線程安全的,安全不安全,我們在這里面指的是是否出現(xiàn)了BUG
使用兩個線程,對同一個整型變量,進(jìn)行自增操作,每一個線程自增5w次,看看最終的結(jié)果
class Counter{//這個變量就是連各個線程進(jìn)行自增的變量public int count;public void increase(){count++;} } public class Solution {private static Counter counter=new Counter();public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for(int i=0;i<5000;i++) {counter.increase();}});Thread t2=new Thread(()->{for(int i=0;i<5000;i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join(); //讓我們的main線程最后執(zhí)行完,由于我們的線程調(diào)度是隨機(jī)的,咱們也不知道是t1先結(jié)束,還是t2先進(jìn)行結(jié)束System.out.println(counter.count);} }
上面的兩個join操作,是一定要進(jìn)行注意的,這兩個Join誰在前,誰在后面無所謂,但是也可以這么說由于我們的線程調(diào)度是隨機(jī)的,我們也是不知道是t1先結(jié)束還是t2先結(jié)束
1)假設(shè)我們的t1先進(jìn)行結(jié)束,那么就先進(jìn)行執(zhí)行t1.join,然后進(jìn)行等待t1結(jié)束,t1結(jié)束了,那么開始調(diào)用t2.join(),等待t2結(jié)束,t2結(jié)束了,我們t2.join()執(zhí)行完畢,
2)假設(shè)此時的t2線程先進(jìn)行結(jié)束,因?yàn)槲覀兪窍冗M(jìn)行執(zhí)行t1.join(),等到t1結(jié)束,t2結(jié)束了,t1沒有結(jié)束,那么我們此時的main線程仍然阻塞在t1.join()里面,在過了一會,t1結(jié)束了,我們此時再次執(zhí)行t2.join(),因?yàn)橹罢f過,t2已經(jīng)結(jié)束了,t2.join()就會立即返回
3)兩個線程,操作的是同一個變量,變量是什么類型沒有什么關(guān)系,和靜態(tài)不靜態(tài)沒有什么關(guān)系,只要兩個線程操作的是同一個變量,就沒有什么關(guān)系
自增的內(nèi)容分成三步,咱們的變量都是在內(nèi)存里面,一個++操作,就分成了三個指令
假設(shè)兩次自增操作數(shù)據(jù)在不同的CPU上執(zhí)行
1)把內(nèi)存中的count數(shù)據(jù)讀取加載到CPU寄存器里面;load
2)在CPU中的寄存器中把數(shù)據(jù)加1(比其他兩個操作快1000倍);add
3)再把計(jì)算結(jié)果也就是CPU寄存器的值,結(jié)果寫回到內(nèi)存中;save
當(dāng)CPU執(zhí)行到上面三個步驟的任何一個步驟的時候,都隨時可能會被調(diào)度器搶走,讓給其他線程執(zhí)行,正是因?yàn)樵蹅兦懊嬲f的搶占式執(zhí)行,這就導(dǎo)致兩個線程同時執(zhí)行這三條指令的時候,順序上面就充滿了隨機(jī)性,每一種情況都有可能發(fā)生
?
?
上面這兩種情況是不會產(chǎn)生線程安全問題的?
雖然我們執(zhí)行了兩次相加,但是內(nèi)存的數(shù)據(jù)仍然少加了一個1,只有兩次的三條指令,是完全串行執(zhí)行的時候就不會出現(xiàn)問題,如果說這兩種線程的指令出現(xiàn)相互交錯,那么最終的結(jié)果就會出現(xiàn)問題,就會少進(jìn)行相加一次
一:一個線程修改一個變量,線程安全;
二:多個線程同時讀取一個變量,線程安全:讀,只是把內(nèi)存中的數(shù)據(jù)放到CPU中,不管怎么讀,內(nèi)存中的數(shù)據(jù)是始終不會進(jìn)行改變的
三:多個線程修改不同的變量,線程安全(自己修改自己的),兩個線程去自增自己的兩個變量,就不會影響到結(jié)果
一)為什么多次運(yùn)行結(jié)果不是10000?這就涉及到線程安全問題,只要兩個線程同時操作的是一個變量,就會出現(xiàn)問題
1)因?yàn)樵?w對并發(fā)相加過程中,有時候操作可能是串行執(zhí)行的,那么此時就加上2,有的時候可能是交錯的,如果說兩個操作時進(jìn)行交錯的,那么結(jié)果就+1
2)在極端情況下,如果說所有的操作都是串行執(zhí)行的,那么此時結(jié)果就是10W
如果說所有的操作都是進(jìn)行交錯執(zhí)行的,那么此時的結(jié)果就是5W,也是可能出現(xiàn)的,但是是小概率事件,串行多少次和進(jìn)行交替多少次,我們不知道,這都是操作系統(tǒng)進(jìn)行調(diào)度執(zhí)行產(chǎn)生的效果,因此我們就無法預(yù)知最終結(jié)果是多少,只不過是有多少次串行執(zhí)行,這才是最終會影響結(jié)果
二)編譯器的優(yōu)化問題:編譯器不會到內(nèi)存里面讀
眾所周知,編譯器從內(nèi)存里面讀數(shù)據(jù)和把CPU操作過的數(shù)據(jù)放回到內(nèi)存里面,這個過程會比在CPU中操作數(shù)據(jù)要慢的很多,于是編譯器就把這個過程給優(yōu)化了,直接不斷地在CPU里面進(jìn)行操作,這樣就會導(dǎo)致線程安全問題;如果說在單線程環(huán)境下,這樣的操作絕對沒有問題,但是在單線程環(huán)境下,就出大問題了;
兩個線程要是去分別自增兩個變量,就最終不會影響到結(jié)果
要是沒有多個線程進(jìn)行操作同一個資源就不會產(chǎn)生線程安全問題
//1.在方法前面加上synchronized關(guān)鍵字,進(jìn)入到方法就會自動加鎖,離開方法就會自動解鎖public synchronized void increase(){count++;} //2.當(dāng)我們給一個線程加鎖成功的時候,其他線程嘗試進(jìn)行加鎖,此時就會觸發(fā)阻塞等待,此時對應(yīng)的線程就處于Blocked狀態(tài) //3.阻塞會一直持續(xù)到占用鎖的線程把鎖釋放位置
1)操作系統(tǒng)搶占式執(zhí)行,線程調(diào)度隨機(jī),這是萬惡之源,無能為力;
2)多個線程同時修改同一個變量,適當(dāng)調(diào)整代碼結(jié)構(gòu),避免這種情況;
3)針對的變量操作不是原子的,count++本質(zhì)上是三個指令:load,add,save;
4)內(nèi)存可見性:一個線程讀,一個線程寫,直接讀寄存器,不讀內(nèi)存,這也是一種優(yōu)化;
5)指令重排序:編譯器優(yōu)化,CPU自身的執(zhí)行;
線程安全問題產(chǎn)生的原因:
1)線程是搶占式執(zhí)行,這是線程不安全的根本原因,解決方法:沒有,假設(shè)線程之間不是搶占式執(zhí)行的,而是其他的調(diào)度方式,那么就可能沒有線程不安全問題了,也有協(xié)商式執(zhí)行的,雖然這是根本原因,但是我們拿他無可奈何,畢竟這是操作系統(tǒng)自身的機(jī)制,咱們也改變不了,它天然就是充滿隨機(jī)性的,因?yàn)椴僮飨到y(tǒng)的調(diào)度完全由內(nèi)核負(fù)責(zé),線程之間的調(diào)度不可預(yù)期,線程的調(diào)度充滿隨機(jī)性,線程誰先執(zhí)行誰后執(zhí)行,誰后執(zhí)行,到哪里從CPU上下來
都是不確定的;導(dǎo)致兩個線程里面執(zhí)行的操作順序無法確定,隨機(jī)性是導(dǎo)致線程安全問題的根本所在;
2)多個線程針對同一個變量進(jìn)行修改操作,多個線程同時批量執(zhí)行一些不是原子性的操作
2.1)多個線程同時修改同一個變量會發(fā)生線程問題;多個線程針對不同的變量進(jìn)行修改不會發(fā)生線程安全問題,多個線程針對同一個變量讀,不會發(fā)生線程安全問題
2.2)針對變量的操作不是原子性,但是賦值這樣的操作就認(rèn)為是一個原子性的操作,(讀取變量的值,只是對應(yīng)著一條機(jī)器操作指令,這樣的操作本身就可以視為是原子性的操作
2.3)通過加鎖操作,也是把好幾個指令打包成一個原子性的操作了
2.4)自增或者自減操作,本身就不是一個原子性的操作
2.5)什么是原子性的操作?不進(jìn)行拆分,一步就到位的那一種操作,就是原子性的操作
2.6)由于不是原子性的操作,那么就會更容易的在多線程調(diào)度中出現(xiàn)問題,原子性的操作直接一步到位,所以說多個線程讀,就沒有什么問題);
2.7)可以通過調(diào)整代碼結(jié)構(gòu),使不同的線程操作不同的變量,只要不是多個線程同時操作同一個變量,那么就不會有太大的問題
2.8)自增操作不是原子的 ,上面的++操作,本質(zhì)上分成三個步驟,是一個非原子的操作;
2.9)在這里面的加鎖操作,就是將若干個非原子性的操作,打包成原子性的操作
3)內(nèi)存可見性:最終內(nèi)存可見性操作,都是由咱們的JAVA編譯器進(jìn)行代碼優(yōu)化的效果,一個線程在不斷地進(jìn)行讀,一個線程針對這個變量進(jìn)行修改
3.1)原因就是說咱們的JAVA編譯器是不會相信程序員的,編譯器就會假設(shè)程序員就是一個XXXX,寫的代碼就是一坨XXX,編譯器就會對程序員寫的一些代碼做出一些調(diào)整,保證在原有邏輯不變的情況下,程序的執(zhí)行效率可以大大提高,這就叫做編譯器優(yōu)化
3.2)在我們保證原有邏輯不變的情況下,大部分情況下,都是可以保證的,但是在多線程的環(huán)境下,是可能翻車的,因?yàn)槎嗑€程執(zhí)行代碼的不確定性,導(dǎo)致在整個編譯器編譯階段,就很難預(yù)知執(zhí)行行為的,進(jìn)行的優(yōu)化就有可能會發(fā)生誤判,因?yàn)閮?yōu)化的誤判就有可能會發(fā)生線程不安全的問題;
?
基本定義:線程A針對這個變量進(jìn)行讀操作,線程B針對這個變量進(jìn)行修改操作,注意A和B都是對應(yīng)的是操作同一個變量,一個線程進(jìn)行讀操作這是我們進(jìn)行循環(huán)很多次,我們的一個線程進(jìn)行修改操作,在合適的時候執(zhí)行一次;
1)一個線程修改了共享變量的值,其他線程來不及看到最新修改的值 ,和原子性類似,與編譯器優(yōu)化相關(guān);
2)如果有多個線程,針對同一個變量,一個線程t1進(jìn)行讀操作(循環(huán)進(jìn)行很多次),一個線程t2修改數(shù)據(jù)(合適的時候執(zhí)行一次),可能會導(dǎo)致線程不安全;
3)咱們線程在循環(huán)讀取內(nèi)存操作,是一個非常低效的事情,如果說t1線程在頻繁的讀取這里面的內(nèi)存里面的值,就會非常低效,如果說t2線程遲遲不修改,t1線程讀到的值有始終是一個值,因此t1就會產(chǎn)生一個大膽的想法,直接從CPU的寄存器里面讀,不從內(nèi)存里面讀
此時如果說等了很久,t2線程修改了count值,那么t1線程就感知不到了
private static int flag=0; public static void main(String[] args) {Thread t1=new Thread(){public void run(){while (flag == 0) { //加上sleep操作,就不會進(jìn)行優(yōu)化,可能加上了sleep操作之后 //就是說可能不會頻繁的讀取內(nèi)存的數(shù)據(jù)加載到CPU里面了 //t1線程不斷地從快速的從CPU的寄存器里面進(jìn)行讀取數(shù)據(jù)}System.out.println("當(dāng)前循環(huán)結(jié)束,當(dāng)前線程結(jié)束,t1線程退出");}};t1.start();Scanner scanner=new Scanner(System.in);System.out.println("請輸入你要修改的flag的值");flag=scanner.nextInt();System.out.println("當(dāng)前的main線程已經(jīng)執(zhí)行完畢");} } 上面的這個操作就是類似于第一個線程在不斷地進(jìn)行讀操作,第二個線程進(jìn)行針對這同一個變量進(jìn)行修改操作;
解決方法線程安全問題:
1)使用synchronized關(guān)鍵字,synchronized不光可以保證原子性的指令,還可以保證內(nèi)存可見性,指令重排序,被synchronized包裹起來的的代碼,編譯器不敢輕易的做出上面的假設(shè),于是手動的禁止了對編譯器的優(yōu)化,所以說我們使用synchronized的時候禁止編譯器進(jìn)行優(yōu)化操作,會使我們的程序的效率會大大降低
2)使用volatile關(guān)鍵字,是和原子性無關(guān),禁止編譯器優(yōu)化,每一次在循環(huán)條件里面判定是否相等,都會強(qiáng)制刷新內(nèi)存中的內(nèi)容到我們的CPU寄存器里面,能夠保證內(nèi)存可見性
指令重排序:
Java的編譯器在編譯代碼時,也會針對指令進(jìn)行優(yōu)化,調(diào)整指令的先后順序,保證原來指令邏輯不變的情況下,提高程序運(yùn)行效率;
synchronized不光可以保證原子性,同時還可以保證內(nèi)存可見性,還可以禁止指令重排序
1)比如說我媽媽讓我買牛肉,黃瓜,茄子,還有西紅柿,如果我們按照這種順序從進(jìn)入超市之后一個一個找,從進(jìn)入超市門口之后,先去到超市出口買牛肉,再去超市入口賣黃瓜.....
這種效率就非常低?
2)可以針對買的順序的一個指令進(jìn)行優(yōu)化,先買黃瓜,再買西紅柿,這樣我們的走的路程就會少了很多,整個路程也會更短
3)在我們的上面的那一個買菜的例子中,我們?nèi)绻f要是可以能夠調(diào)整一下代碼執(zhí)行的先后順序,最終的執(zhí)行結(jié)果不變,但是我們的程序執(zhí)行效率就大大的提高了
4)咱們以前寫的很多代碼,彼此的順序,誰在前面,誰在后面都是無所謂的,在這里面我們的編譯器就會智能地進(jìn)行調(diào)整代碼執(zhí)行的前后順序,從而提高程序的效率,總而言之,保證邏輯不面的條件下來進(jìn)行優(yōu)化的
1)也就是說編譯器會自動的調(diào)整指令的順序,以便達(dá)到提高執(zhí)行效率的結(jié)果,但是調(diào)整的前提是,保證指令的最終結(jié)果是不變的;
2)編譯器優(yōu)化,是一個非常智能的東西,如果當(dāng)前的判斷邏輯只是在單線程下執(zhí)行編譯器根據(jù)順序來判斷結(jié)果,還是比較容易的;但是如果在多線程情況下,編譯器也可能會進(jìn)行誤判,編譯器判定順序是否會影響結(jié)果,就不一定了;使用synchronized不光可以保證原子性,還可以保證內(nèi)存可見性,保證指令重排序
synchronized關(guān)鍵字的作用:
1)互斥使用
2)避免內(nèi)存可見性,指令重排序
3)可重入鎖
咱們的synchronized的使用是要付出代價(jià)的,代價(jià)就是一旦使用synchronized很容易會導(dǎo)致線程阻塞,一旦線程阻塞(放棄CPU),下一次我們這個線程再次回到CPU就會變得很困難,這個時間是不可控的,如果調(diào)度不回來,可能是滄海桑田,自然對應(yīng)的任務(wù)執(zhí)行時間也就被拖慢了,一旦代碼使用synchronized,就會基本和高性能無緣,volaitile不會導(dǎo)致線程阻塞
1)進(jìn)入increase之后加了一次鎖,進(jìn)入代碼塊之后又加了一鎖,在這里面synchronized在這里進(jìn)行了特殊處理;
2)如果是其他語言的鎖操作,就有可能造成死鎖;
第一次加鎖,加鎖成功
第二次在嘗試對這個對象頭加鎖的時候,此時對象頭的鎖標(biāo)記已經(jīng)是true,按照之前的理解,線程就要進(jìn)行阻塞等待,等待這個所標(biāo)記被改成false,才重新競爭這個鎖,但是此時是在方法內(nèi)部,肯定是無法釋放第一次所加的鎖的,就出現(xiàn)死鎖了;
synchronized public void increase() { ?count++; }synchronized public void increase2() {increase(); }
synchronized public static void run(){synchronized (Student.class) {System.out.println("我叫做run方法");}}
3)在可重入鎖內(nèi)部,會記錄當(dāng)前的鎖是被哪一個線程占用的,同時也會記錄一個加鎖次數(shù);當(dāng)我們的線程A對這個鎖進(jìn)行第一次加鎖的時候,顯然是能加鎖成功的,鎖內(nèi)部就會進(jìn)行記錄,當(dāng)前占用著的線程是A,同時加鎖次數(shù)是1,后續(xù)我們再次針對這個線程A進(jìn)行加鎖的時候,此時就不會真的加鎖,而只是單純的把引用計(jì)數(shù)給自增,加鎖次數(shù)是2,后續(xù)我們進(jìn)行解鎖的時候,再把引用計(jì)數(shù)減1,當(dāng)我們進(jìn)行把引用計(jì)數(shù)減到0的時候,就真的進(jìn)行解鎖,所以說后續(xù)的加鎖操作對這個線程持有這把鎖是沒有本質(zhì)影響的;
3.2)咱們的可重入鎖的意義就是為了降低程序員的負(fù)擔(dān),降低使用成本,提高了開發(fā)效率,但是也帶來了代價(jià),程序中需要有額外的開銷,因?yàn)槲覀円S護(hù)鎖屬于哪一個線程,并且進(jìn)行加減計(jì)數(shù),這個變量還占用空間,降低了運(yùn)行效率,開發(fā)效率最重要
3.3)如果說我們使用的是不可重入鎖,此時我們的開發(fā)效率就降低了,如果我們一不小心,代碼中就出現(xiàn)死鎖了,線上程序出BUG了,就需要修BUG了,如果BUG嚴(yán)重了,年終獎可能會泡湯
4)解決指令重排序,避免亂序執(zhí)行
當(dāng)synchronized修飾方法的時候有以下需要進(jìn)行注意:
1)synchronized關(guān)鍵字不可以被繼承:雖然可以使用synchronized來進(jìn)行修飾方法,但是synchronized并不屬于方法中的一部分,因此synchronized關(guān)鍵字不可以被繼承,如果說你在父類中的某一個方法中使用了synchronized關(guān)鍵字,在子類中重寫了這個方法,在子類中的某一個方法并不是同步的,必須顯示的在子類中加上synchronized關(guān)鍵字才可以
2)定義接口方法中不能使用synchronized關(guān)鍵字
3)在構(gòu)造方法中不能使用synchronized關(guān)鍵字,但是可以使用synchronized同步代碼快來進(jìn)行同步
總結(jié):synchronized修飾普通方法和同步代碼快指定this表示給當(dāng)前對象進(jìn)行加鎖,就是當(dāng)不同線程嘗試訪問一個對象中的synchronized(this)同步代碼快的時候,其他訪問該對象的線程將會被阻塞
class RunnableTask implements Runnable{public void GetCount(){ System.out.println("生命在于運(yùn)動");}public void run() {synchronized (this){try {TimeUnit.SECONDS.sleep(10);System.out.println("我是中國人");} catch (InterruptedException e) {e.printStackTrace();}}} } class Solution {public static void main(String[] args) {RunnableTask task=new RunnableTask();Thread t1=new Thread(task);Thread t2=new Thread(task);t1.start();t2.start();//上面這種情況就會發(fā)生阻塞//但是下面這種情況就不會發(fā)生阻塞,不會產(chǎn)生鎖的競爭RunnableTask task1=new RunnableTask();RunnableTask task2=new RunnableTask();Thread t3=new Thread(task1);Thread t4=new Thread(task2);t3.start();t4.start();} }
1)當(dāng)我們的兩個并發(fā)線程t1和t2在進(jìn)行訪問同一個對象的synchronized(this)修飾的同步代碼快的時候,同一時刻只能有一個線程被執(zhí)行,一個線程被阻塞,必須等待一個線程執(zhí)行玩這個同步代碼快之后另一個線程才可以執(zhí)行這個代碼塊
2)t1和t2是互斥的,因?yàn)樵谖覀儓?zhí)行synchronized代碼塊的時候會進(jìn)行鎖定當(dāng)前的對象,只有執(zhí)行完該同步代碼快的才能釋放該對象的鎖,下一個線程才能執(zhí)行并且鎖定該對象
3)但是被注釋掉的那一片代碼,t3和t4在同時進(jìn)行執(zhí)行,因?yàn)樗麄兪窃L問兩個對象中的被synchronized修飾的方法或者是同步代碼快,這是因?yàn)閟ynchronized只鎖定對象,每一個對象只有一把鎖與之相關(guān)聯(lián)
4)當(dāng)一個線程訪問一個對象的synchronized的同步代碼快的時候,另一個線程仍然可以訪問該對象的非同步代碼快
1)synchronized給靜態(tài)方法加鎖是全局的,也就是說在整個程序運(yùn)行期間,所有調(diào)用這個靜態(tài)方法的對象都是互斥的,而普通方法是對象級別的,不同的對象對應(yīng)著不同的鎖
2)當(dāng)我們修飾靜態(tài)代碼塊的時候,需要指定一個加鎖對象,可以給this來進(jìn)行加鎖,表示給當(dāng)前的對象加鎖,不同的對象對應(yīng)著不同的鎖,但是我們給.class進(jìn)行加鎖的時候,表示給某個類對象進(jìn)行加鎖
?volatile:禁止編譯器進(jìn)行優(yōu)化,保證內(nèi)存可見性,是因?yàn)橛?jì)算機(jī)的硬件結(jié)構(gòu)決定的
JMM內(nèi)存模型就描述了,CPU指令內(nèi)存之間的交互過程和硬件結(jié)構(gòu)用AVA這里面的術(shù)語重新進(jìn)行了封裝?
在這里面,涉及到一個重要的知識點(diǎn),JMM(Java memory model內(nèi)存模型)
1)正常情況下,是想要把內(nèi)存中的數(shù)據(jù)讀到CPU里面進(jìn)行操作,但是實(shí)際上在CPU和內(nèi)存中還會存在這一些緩存,為了提高CPU和內(nèi)存之間的讀寫效率;
2)當(dāng)我們在代碼中讀取變量的時候,不一定是在真的讀內(nèi)存,可能這個數(shù)據(jù)已經(jīng)在CPU或者cathe中緩存著了;
3)這個時候就可能會繞過內(nèi)存,直接從CPU寄存器里面或者cathe中取數(shù)據(jù),咱們的JMM針對計(jì)算機(jī)的硬件結(jié)構(gòu),又進(jìn)行了一層抽象,主要是因?yàn)镴ava是要跨平臺,要能夠支持不同的計(jì)算,有的計(jì)算機(jī)可能沒有catche2內(nèi)存,可能也會有catche3內(nèi)存,當(dāng)這個變量進(jìn)行修改的時候,此時讀的這個線程沒有從內(nèi)存里面讀,而是從catche里面讀,或者CPU中的寄存器里面讀,就會出現(xiàn)內(nèi)存可見性的問題;4)JMM就會把CPU中的寄存器,L1,L2,L3catche,統(tǒng)稱為工作內(nèi)存,把真正的內(nèi)存稱為主內(nèi)存,工作內(nèi)存一般不是真的內(nèi)存,每一個線程都會有自己的工作內(nèi)存,每個線程都有獨(dú)立的上下文,獨(dú)立的上下文就是各自的一組寄存器,catche中的內(nèi)容
5)CPU和內(nèi)存進(jìn)行交互時,經(jīng)常會把主內(nèi)存中的內(nèi)容,拷貝到工作內(nèi)存,然后再進(jìn)行工作,寫會到主內(nèi)存中,這就可能會出現(xiàn)數(shù)據(jù)不一致的情況,這種情況是在編譯器進(jìn)行優(yōu)化的時候特別嚴(yán)重
關(guān)鍵字volitaile和synchronized就可以強(qiáng)制保證接下來的操作是在操作內(nèi)存,在生成的java字節(jié)碼中強(qiáng)制插入一些內(nèi)存屏障的指令,這些指令的效果,就是強(qiáng)制刷新內(nèi)存,同步更新主內(nèi)存和工作內(nèi)存中的內(nèi)容,在犧牲效率的時候,保證了準(zhǔn)確性
這就類似于我現(xiàn)在要出遠(yuǎn)門,要帶行李
內(nèi)存:就是我們家
CPU寄存器:就是我的手
揣個口袋:相當(dāng)于是catche1緩存,這里面存放了我最常用的數(shù)據(jù),比如說身份證,錢財(cái)
背了個背包:相當(dāng)于是catch2緩存,這里面存放了我不太常用的數(shù)據(jù),比如說水杯,牙刷
帶了個行李包:相當(dāng)于是catche2緩存,里面存放了一些最不常用的東西,比如說衣服
1)咱們?nèi)绻f出門忘帶東西了,那么就趕緊回到內(nèi)存中,同步更新工作內(nèi)存的內(nèi)容
從內(nèi)存取東西,從家里面取東西,效率最低,但是直接從緩存中拿數(shù)據(jù),最好是直接從口袋中拿數(shù)據(jù)
2)之前我們進(jìn)行循環(huán)進(jìn)行讀取內(nèi)存,就是類似于你出外面辦事情,發(fā)現(xiàn)用到什么東西就會到家里面取,這樣的開銷實(shí)在是太大了
3)工作內(nèi)存:CPU寄存器+緩存
1)計(jì)算機(jī)想要執(zhí)行一些計(jì)算,就需要把內(nèi)存中的數(shù)據(jù)寫入到CPU的寄存器里面,然后再在寄存器中進(jìn)行計(jì)算,再寫回到內(nèi)存里面,直接讀寄存器,CPU訪問寄存器的速度是要比訪問內(nèi)存的速度要快的多,當(dāng)CPU連續(xù)多次訪問內(nèi)存,發(fā)現(xiàn)結(jié)果都一樣,就不會從內(nèi)存里面讀了,就直接會從CPU的寄存器里面讀就可以了;
2)在Java中,要先把數(shù)據(jù)從主內(nèi)存加載到工作內(nèi)存里面,工作內(nèi)存計(jì)算完畢之后在寫回到主內(nèi)存里面
六)單例模式:
1)單例模式是什么:單例模式是一種常用的軟件設(shè)計(jì)模式,其定義是單例對象的類只能允許一個實(shí)例存在,設(shè)計(jì)模式就是根據(jù)常見場景給出的一些經(jīng)典解決方案;
2)餓漢模式:在類加載的過程中就把實(shí)例創(chuàng)建出來
3)懶漢模式:通過getInstance方法來獲取到首例首次調(diào)用該方法,用了才創(chuàng)建,不用就不創(chuàng)建
4)最大的區(qū)別就是說對于我們代碼中的唯一實(shí)例實(shí)例化的初始時機(jī)是不一樣的
這就類似于吃完飯洗碗:
1)中午這一頓飯,使用了四個碗,吃完之后,就立即把這四個完給洗了,這就是所說的餓漢模式,就是很著急
2)中午這頓飯,使用了四個碗,吃完之后,先不洗,因?yàn)橥砩线@一頓只需要兩個碗,然后直接就洗兩個碗就可以了;就是懶漢模式,我們認(rèn)為這是一個更加高效的操作
我們的餓漢的單例模式是很著急的創(chuàng)建實(shí)例的
我們的懶漢的單例模式是不算太著急的創(chuàng)建實(shí)例的,只是在用的時候創(chuàng)建實(shí)例
?1)餓漢模式
一)使用static創(chuàng)建一個實(shí)例并且立即進(jìn)行實(shí)例化,這個instance實(shí)例就是該類的唯一實(shí)例
二)為了防止在其他地方程序員不小心在其他地方new了一個Singleton,就可以把這個構(gòu)造方法設(shè)置成私有的,你如果想要new只能在類的內(nèi)部new;
三)提供一個公開的static靜態(tài)方法,讓類外可以直接拿到這一個唯一實(shí)例;
//我們通過Singleton這個類來進(jìn)行創(chuàng)建單例模式,來進(jìn)行保證Singleton只有一個唯一的實(shí)例 static class Singleton{private Singleton(){}; //把構(gòu)造方法設(shè)成私有的,防止在類外調(diào)用構(gòu)造方法,也就禁止了調(diào)用者在其他地方創(chuàng)建實(shí)例的機(jī)會private static Singleton instance=new Singleton();public static Singleton getInstance(){return instance;}}public static void main(String[] args) {Singleton s1=Singleton.getInstance();Singleton s2=Singleton.getInstance();System.out.println(s1==s2);} //針對這個唯一實(shí)例的初始化比較著急,類加載階段階段,我們就會直接創(chuàng)建實(shí)例 //程序中使用到了這個類,就會立即進(jìn)行加載
1)這里的打印結(jié)果是true;
2)要創(chuàng)建這個類的實(shí)例只能提供一個靜態(tài)公共方法,此處的getInstance是獲取該類實(shí)例的唯一方式,不應(yīng)該有其他方式來獲取該實(shí)例;
3)static修飾的成員準(zhǔn)確的來說應(yīng)該是類成員,也叫做類屬性或者類方法,不加static修飾的成員,就叫做實(shí)例成員,也稱之為實(shí)例屬性,或者是實(shí)例方法
4)JAVA程序,一個類對象是指只存在一份的,這是JVM來進(jìn)行保證的,進(jìn)一步也就保證了說咱們的類的static成員也是只有一份的,總結(jié)這個類的實(shí)例和獲取這個類的實(shí)例方法都是static的,主要是不想依賴對象的實(shí)例來進(jìn)行調(diào)用,類對象只有一個,但是實(shí)例確實(shí)有很多個
5)對于餓漢模式來說,類加載只有一次,就是在類創(chuàng)建的時候,當(dāng)我們調(diào)用getInstance的時候,getInstance只做了一件事,那就是讀取instance實(shí)例的地址,這就相當(dāng)于多個線程同時讀取同一個變量,不涉及到修改
6)多個線程調(diào)用getinstance就是相當(dāng)于是多個線程同時來讀引用里面的值,返回引用里面的值,是不會有線程安全問題的,只是把的內(nèi)存里面的值加載到我們的CPU的寄存器里面
2)懶漢模式(效率更高),不太著急創(chuàng)建實(shí)例
1)當(dāng)沒有使用到這個實(shí)例的時候,就算我們需要用到這個類的時候,但是也并不會進(jìn)行真正的初始化,只有說什么時候真正的調(diào)用到這個方法的時候,我們才真正的進(jìn)行初始化操作
2)也就是說只有我們真正的用到這個實(shí)例的時候,我們才會真正的創(chuàng)建這個實(shí)例
3)線程是否安全指的是具體是在多線程環(huán)境下,并發(fā)的調(diào)用GetInstance方法,看看是否可能會創(chuàng)建出多個實(shí)例,也就出現(xiàn)了BUG
static class Singleton{private Singleton(){};private static Singleton instance=null; //防止其他程序員在外邊new這個實(shí)例,就可以讓構(gòu)造方法是privatepublic static Singleton getInstance() //只有調(diào)用到getInstance的時候才會創(chuàng)建實(shí)例{if(instance==null){instance=new Singleton();}return instance;}}public static void main(String[] args) {Singleton s1=Singleton.getInstance();Singleton s2=Singleton.getInstance();System.out.println(s1==s2);}
1)在一個Java成員里面,一個類對象只存在一份,進(jìn)一步就保證了類的static成員也是只有一份的,static修飾的成員,叫做類成員,類對象,就是.class文件,被JVM加載到內(nèi)存中的摸樣,類對象里面就有有關(guān)于.class文件的一切屬性,只有基于類對象,我們才能完成反射;
2)一開始的時候,instance指向的內(nèi)容為null,在main函數(shù)里面,s1的時候創(chuàng)建了一個對象,instance就指向了一個對象,當(dāng)?shù)诙握{(diào)用的時候,因?yàn)樵瓉硪呀?jīng)有這個對象了,所以并不會進(jìn)入到這個If語句中,返回的還是上一次s1調(diào)用的實(shí)例,所以兩個變量指向的是同一塊地址,所以返回結(jié)果為true;
3)咱們一直說類加載,類加載,那么類加載到底是什么?就是把,class文件,加載到內(nèi)存里面變成我們的類對象,通過類名.class,我們的類對象就有包含.class文件的一切信息,包括類名是什么,類里面有什么屬性,每一個屬性叫什么名字,每一個屬性是什么類型,每一個屬性是public還是private,基于這些信息,我們才可以使用反射;比如說我們想要獲取到類中某一個名字是value的屬性,先通過類對象找到這個value屬性在我們對象中的偏移量,然后再去對應(yīng)的內(nèi)存空間里面一找,就找到了這個值;
4)類本質(zhì)上就是一個實(shí)例的模板,基于這個模板我們可以創(chuàng)建出很多的對象來;
餓漢模式與懶漢模式
1)餓漢模式:類加載的時候沒有被實(shí)例化,當(dāng)?shù)谝淮握{(diào)用getinstance的時候才真的被實(shí)例化了,要是一整場都沒有調(diào)用getInstance,實(shí)例化的過程也就省略了
2)一般認(rèn)為懶漢模式比餓漢模式效率更高,因?yàn)閼袧h模式很大的時候?qū)嵗貌坏?#xff0c;此時有了節(jié)省實(shí)例化的開銷;
1)餓漢模式:一個典型的場景:像我們的notepad這樣的程序,在我們大開大文件的時候是很慢的,你要想打開這一個G的文件,我們就需要嘗試把這1G的大文件都進(jìn)行加載到內(nèi)存里面
2)懶漢模式:像我們的一些其他的程序,在我們嘗試打開大文件的時候就會進(jìn)行優(yōu)化,比如說我們想要打開1G的文件,但是只能先進(jìn)行加載這一個屏幕中我們能夠顯示的部分,便翻頁便進(jìn)行加載,翻多少,我們就進(jìn)行加載多少,用多少加載多少
對于懶漢模式,線程不安全
1)在的餓漢模式里面,讀取地址里面的值本身就是一條原子性的指令,但是懶漢模式里面,既包含了讀,又包含了修改況且這里面的讀和修改,還是分成兩個步驟的,不是原子的指令,因此就存在線程安全問題
1)讀取instance的內(nèi)容
2)判斷是否為空
3)如果instance為null,就創(chuàng)建一個對象
4)返回instance的值? ? ?
1)下面的這種寫法是一種非常非常錯誤的寫法,因?yàn)橹皇墙o賦值操作進(jìn)行加了鎖,進(jìn)行對我們類的實(shí)例instance判斷是否為空沒有進(jìn)行加鎖
2)假設(shè)現(xiàn)在有兩個線程同時調(diào)用了getInstance()方法,同時進(jìn)行判斷當(dāng)前的instance實(shí)例是否為空?是的呢,然后線程1進(jìn)入到鎖內(nèi)部,然后創(chuàng)建實(shí)例,此時線程2在線程1進(jìn)行創(chuàng)建實(shí)例之后,就不會再進(jìn)行判斷instance是否為空了,因?yàn)橹耙呀?jīng)判斷過了,此時線程2就會獲取到鎖,然后創(chuàng)建實(shí)例
synchronized (Singleton.class) {if(instance==null) {instance = new Singleton();}}return instance; }
這次加了雙重if來優(yōu)化代碼:避免頻繁的觸發(fā)不必要的加鎖操作:
1)當(dāng)前這個代碼,首次調(diào)用getinstance此會發(fā)生線程安全問題,后續(xù)再進(jìn)行操作,就不會涉及到修改操作了也就是new操作了;按照剛才這個寫法,不僅僅使首次調(diào)用會加鎖,后續(xù)進(jìn)行調(diào)用的時候也會涉及到加鎖操作;
2)對于剛才的這個懶漢模式的代碼來說,線程不安全,是發(fā)生在instance被初始化之前的,未進(jìn)行初始化的時候,多線程調(diào)用getInstance,就可能涉及到讀與修改,但是一旦Instance被初始化之后,if判斷語句一定不是null了,if條件一定不成立了,GetInstance也就只剩下兩個讀操作,也就是線程安全的了,這兩個讀操作分別是判斷if語句中的條件是否成立,還有直接進(jìn)行return操作,但是我們的程序中還是對這兩個操作進(jìn)行了加鎖,好像沒有什么必要了;
3)按照上面的加鎖方式,無論是代碼初始化之后,還是初始化之前,每一次調(diào)用GetInstance方法之后都會進(jìn)行加鎖,也就是針對兩個只讀操作進(jìn)行加鎖,也就意味著即使是初始化完成之后,也要進(jìn)行大量的鎖競爭,程序的速度就慢了;
?
1)改進(jìn)思路:首次調(diào)用的時候,進(jìn)行加鎖,后續(xù)進(jìn)行調(diào)用的時候,就不會進(jìn)行加鎖;
2)在上面的代碼中,看起來兩個一樣的if條件是相鄰的,但是實(shí)際上這兩個條件的執(zhí)行實(shí)際差別是很大的,加鎖是由可能會使代碼出現(xiàn)阻塞,外層條件是10:16分進(jìn)行執(zhí)行的,但是里層條件可能是10:30分執(zhí)行的,在這個執(zhí)行差中間,instance也是有可能被其他線程給修改的
3)從上述的代碼中分析不是說出現(xiàn)了多線程就需要進(jìn)行加鎖,從上述情況下可知只有在第一次進(jìn)行初始化的時候才需要進(jìn)行加鎖,后續(xù)線程調(diào)用到這個方法的時候,就不需要進(jìn)行加鎖了,但是我們JAVA標(biāo)準(zhǔn)庫中的集合類,vector,HashTable就是這兩個集合類在無腦加鎖
4)如果去掉里面if操作就變成了剛才那一個典型的錯誤代碼,加鎖沒有進(jìn)行把讀操作修改操作給打包到一起
5)如果說是針對方法來進(jìn)行加鎖,那么此時就是無腦加鎖,顯然就不會提高程序執(zhí)行的效率
class Singleton {private Singleton() {}private static Singleton instance = null;public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
使用volatile來保證內(nèi)存可見性和指令重排序:
正常情況下加上了雙重校驗(yàn)鎖之后,應(yīng)該就是下面這種情況:
?
1)但是加了雙重if后,會出現(xiàn)內(nèi)存可見性的問題,在最開始的時候,如果我們現(xiàn)在有很多線程,都去調(diào)用這里面的GetInstance,就會造成大量的讀instance內(nèi)存的操作,涉及到要讀內(nèi)存,編譯器可能讓這個讀內(nèi)存操作編程優(yōu)化操作,直接讀CPU寄存器或者緩存,一個線程可能都已經(jīng)對instance進(jìn)行了修改,咱們的另一個線程的最外邊的if語句還是再讀CPU寄存器里面的值呢,那么最外邊的條件加不加沒啥意思,因?yàn)槲覀兊膬?nèi)存可見性問題只會引起第一個if語句判定失效,但是對于第二個if語句影響其實(shí)不大,因?yàn)槲覀兊膕ynchronized也是可以保證內(nèi)存可見性的,因此這樣子的內(nèi)存可見性問題,只會影響到第一個條件的誤判,也就是說導(dǎo)致不應(yīng)該加鎖的地方進(jìn)行了加鎖,但是不會影響到第二層if的誤判,不至于說創(chuàng)建多個實(shí)例
2)一旦這里面觸發(fā)了優(yōu)化,那么比如說后續(xù)的第一個線程完成了針對instance的修改操作,那么緊接著后面的線程都感知不到后面的修改操作,仍然把instance當(dāng)成空值,內(nèi)存可見性問題,可能會引起第一個if判定失效,但是當(dāng)進(jìn)入synchronized(保證內(nèi)存可見性)之后,再去判斷if語句,就不會影響,就是最終造成的結(jié)果是引起第一層的誤判,不該加鎖的加鎖了,不至于說創(chuàng)建多個實(shí)例,不會導(dǎo)致單例模式失效,就會導(dǎo)致鎖的競爭變得更激烈了;
3)首批線程進(jìn)入到第一層if,進(jìn)入到鎖階段,并創(chuàng)建好對象之后,這個時候,就相當(dāng)于把instance的實(shí)例設(shè)成非空的值了;但是在后續(xù)線程調(diào)用方法讀取instance的值時,可能就會出現(xiàn)內(nèi)存可見性的問題;
總結(jié):為了保證懶漢模式的線程安全:
1)加鎖 保證線程安全,在這里面使用我們的類作為鎖對象,類對象在程序中只有一份,就能保證getInstance都是針對同一個對象進(jìn)行加鎖
2)雙重if保證效率,避免進(jìn)行不必要的重復(fù)加鎖操作
3)加上volatile保證避免出現(xiàn)內(nèi)存可見性的問題
下面來看一下完整的正確代碼
class Singleton {private 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;}