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