1. 程式人生 > 其它 >Java學習筆記:2022年1月9日(其二)

Java學習筆記:2022年1月9日(其二)

Java學習筆記:2022年1月9日(其二)

摘要:這篇筆記主要記錄了1月9日學習的第四章的類的基礎知識,以及訪問器以及訪問器於多執行緒的意義。

@

目錄

1.多執行緒

​ 多執行緒指的是在一個程式執行過程中,其內部的多個任務被同時執行,這時每個任務都會作為一個執行緒來執行。使用多執行緒的方式可以有效提升執行效率,當CPU的多執行緒能力很強的時候,計算機的多開能力也會得到提升,也就是說你能一次開多個QQ或者多個DNF,很多工作室都會強調計算機的多執行緒能力。CPU是被分為多個核心的,我之前給一個雙核CPU開過核,發現裡邊確確實實有兩個矽晶體晶片,現在的八核十六核乃至幾十個核的CPU不知內部是什麼樣的,可能已經整合到一個晶片上去了,但是,核與核之間仍然是獨立的,每個核在同一時刻都可以執行一個執行緒,而核多了就可以實現多執行緒並行。

​ 但是我們需要注意的是,一個CPU儘管可以實現多執行緒並行,一個CPU中的一個核心可不能,CPU中的一個核心在同一時刻只能執行一個程式,因此當我們使用只有一個核的CPU時,其內部並不能真正的讓執行緒並行,而是使用一種虛擬化的方式,讓程式看上去在併發執行。當然,在系統中的執行緒數量過多時,即使多核CPU也會出現這種問題。

​ 因此當匯流排程數大於CPU核心數時,CPU的每個核心會不得不進行時間片輪轉,在一段時間內分別執行不同的執行緒,進而保證在一段時間以後這些執行緒全部完成,進而讓這些執行緒看上去像是併發執行了。這種讓任務在CPU核心上輪流執行的方式具體是為每個執行緒指定一個合理的時間執行方案,讓他們間隔的在核心上執行,他們會被分配給一些時間片,消耗時間片規定的時間並按照一定的排程規則執行。

​ 當一個執行緒的時間片瀕臨用完,系統會為它儲存當前的執行狀態,並進行執行緒切換,換下一個執行緒來執行,同時系統還會給它分配新的時間片,確保這個執行緒最終能夠被執行完,這個儲存狀態並切換的行為叫做執行緒的切換。執行緒的切換需要一些開銷,如空間開銷,因為當前即將結束的執行緒以後還需要執行,因此係統需要儲存它的被切換時的狀態,以便下次又輪到它時能夠順利接著剛才的狀態啟動,同時還需要一定的時間開銷。

​ 執行緒的切換行為通常來說是在安全點上進行,所謂安全點指的是一個執行緒內的基本方法執行結束之後,下一個方法還沒有執行的時候,執行緒切換會盡量避免線上程的某個方法執行的時候發生,因為線上程的一個方法執行時,往往有著更復雜的系統狀態,而且對於作為一個整體的方法來說,貿然打斷會破壞它的原子性,在某個方法中很可能某些變數正在發生改變,這是發生中斷,很可能會破壞這些變數的改變,因此可能會發生回滾行為,這和資料庫中的原子操作非常相似,因為在有些方法中會對堆區的記憶體進行操作,這個方法的中斷會導致實參發生改變,但是系統又很難標記某個方法的中間執行狀態,只能在下次執行時把這個方法重頭開始,這時整體上就會發生錯誤,因此可能會導致不安全的現象,如變數改變兩次等。因此係統會盡量避免這種情況的發生,一般情況下是在方法與方法之間的安全點中記性執行緒切換的,當然某些特殊情況下,確實也存在在方法執行時直接切換,只不過這種情況很少罷了。

​ 在Java中存在多執行緒的功能,接下來我們結合程式碼來繼續探討多執行緒中的注意事項:

public class Test{
	public static void main(String[] args) {
		Person a = new Person();
		Thread t1 = new Thread(){
			@Override
			public void run(){
				System.out.println("Hello I'm a thread2");
				int[] xx = a.a();
				xx[0] = 123;
			}
		};
		Thread t2 = new Thread(){
			@Override
			public void run(){
				System.out.println("Hello I'm a thread2");	
				int[] xx = a.a();
				System.out.println(we[0]);
			}
		};
		t1.start();
		t2.start();
		//System.out.println("11");
	}
}

​ 如程式碼所示,為建立多個執行緒程式碼並進行多執行緒執行的方式,具體建立方式為使用執行緒構造方法並在其回撥方法中書寫每個執行緒的操作。而讓執行緒執行的方式是對他們的呼叫start方法。

​ start是讓他們執行的方法嗎?實際上不是的,不僅如此,兩個執行緒的執行先後順序和誰先start誰後start無關,在這個程式中,t2是有可能比t1先執行的。這就是因為start方法並不是讓每個執行緒執行的方法,而是讓他們就緒的方法,線上程建立好後,不會瞬間被執行,而是處於一個預設的掛起狀態。在系統中,想要被CPU執行的執行緒們,是位於一個就緒佇列的,只有位於就緒佇列中才會被CPU排程執行,剛被建立好的執行緒物件不位於這個佇列中,而是存在於記憶體中,並不往CPU中輸送,這時這是一個掛起狀態,而使用start方法,就是讓執行緒由掛起狀態進入就緒狀態,也就是送入就緒佇列,這之後就可以被CPU執行了。在就緒佇列中,誰先被執行誰後被執行和誰先進入佇列沒關係,一切要聽CPU的,要看CPU想先執行誰,誰就先被執行。

​ 當執行緒被執行時,Java虛擬機器的執行時會為它們建立各自的新執行緒棧,它們會立即脫離主執行緒自立門戶,擁有一個屬於自己的執行緒棧,每個執行緒棧有一個屬於自己的run方法,這個run方法類似於主執行緒棧的main方法,每個新執行緒的執行緒棧,是一個屬於自己的run方法,我們所書寫的一切執行緒中的程式碼都在這個run方法中執行,和main方法一樣,當我們線上程中呼叫其他方法,也會發生壓棧的情況。執行緒的創立就伴隨著棧區新棧的創立,它們和主執行緒算是同級,這時候就不分誰先誰後運行了,一切由系統指揮。在新執行緒創立好之後,它們和主執行緒就完全沒關係了,可以理解為直接分解,各奔東西了,他們之間不再存在相互制約的先後關係,誰先執行完要看系統怎麼指揮。

​ 然而通常來講,都是主執行緒先執行完,因為在創立新執行緒的時候,主執行緒一定正在執行,它正在被執行,因此作業系統很大概率不會因為其他執行緒而換掉正在執行的主執行緒,而且一般情況下主執行緒也不會特別長,CPU很快就能執行完,因此就不會再臨時切換執行緒,在主執行緒結束之後再執行其他執行緒。執行緒中可以出現同樣名字的變數,因為他們屬於不同的定義域,互不相干。

2.封裝思想

​ 封裝是面向物件的語言的一大重要特性,Java中就很強調封裝這一特性。封裝就是裡邊的原理實現全部封裝,只對外公開怎麼用,也就是對外提供簡單方便的介面,遮蔽內部複雜繁瑣的原理。在Java中,要相對物件中的屬性進行封裝,則必須使用private字首來標識屬性,這樣一來屬性就不能通過物件直接訪問,但是我們有時也需要使用物件中的屬性,怎麼辦呢?這時候我們要在類中新增修改器和訪問器,訪問器就是得到這個屬性值的方法,修改器就是修改這個屬性值的方法,很多ide都可以實現修改器訪問器直接生成。

public class Person{
    private int[] arr = {23,45,55,66}; 
    public String name;
    public static int flag;
    public static void m1(){}
    public int[] get_a(){
        int[] arr2 = new int[arr.length];
        for(int i = 0;i < arr.length;i++){
            arr2[i] = arr[i];
        }
        return arr2;
    }
}

​ 在以上程式碼中,對於私有屬性arr,get_a方法,實際上就是一個訪問器,當我們將一個屬性設定為私有屬性後,便無法通過物件直接使用這個屬性,但是我們通過類中提供的公共方法可以間接的訪問或者修改這個屬性,我們可以給這個方法設定一些特殊操作,進而使得對於這個屬性的操作更加安全,同時使用者無需瞭解這個方法內部是什麼,只需呼叫並填寫自己的引數,就可以完成對這個屬性的訪問或者修改,這樣能夠很好的保證整個系統的安全性以及外界操作的方便性,這就是封裝帶來的好處。

​ 在上面的程式碼中,我們為在arr屬性的訪問器中加入了一個深拷貝,也就是說我們通過它的訪問器獲得的arr陣列,實際上是一個和arr陣列一樣的,但是實際上不是同一個,在記憶體上位於不同地址的陣列,只是一個替身,這種深拷貝操作的好處實際上是有利於多執行緒中對同一資源訪問時的安全性。當多個執行緒同時訪問arr並進行一些修改操作時,就可能會導致執行緒之間的操作互相影響並導致最終執行錯誤,使用深拷貝操作的訪問器,可以直接遮蔽掉執行緒與執行緒之間對同一資源進行操作時的影響,實際上,他們操作的根本不是同一個資源,同時這也會讓執行緒們的操作無法影響原物件中的arr屬性值,因為他們操作的只是一個替身,這對系統中的資訊保安性有很大意義,這種深拷貝操作在多執行緒中也是會經常用到的一種操作,非常有效好用。

3.筆記原文

	引用型別的=為:控制代碼等於值的地址,引用型別賦值就是把右側的值的地址交給左側的控制代碼。
	控制代碼 = 值的地址(堆中地址),值和基本型別並不對應
	C裡邊的指標也是這麼用的
	引用地址是棧中變數的地址,或者說是棧中變數的真實地址,變數是有自己地址的
	這個過程實際上就是非常類似C語言中的指標,也就是地址之中存地址,進行這種連續的對映,棧的地址上對映上值的地址,值的地址對映到堆上面,其中棧也有自己的地址,這個地址並不對使用者開放,是封裝的。
	棧中有棧,函式棧內有變數棧。
	傳參時,形參會拷貝實參的值。用 = 為引用型別拷貝的就是地址,叫淺拷貝
	java中引用型別進函式傳參,形參被賦值,被賦予的是實參的指向,而不是被賦予的地址,在a方法裡面無論寫任何程式碼都沒辦法改變b方	法裡邊宣告變數的指向。這個和C語言不太一樣,因為傳入的終究不是地址。a方法中的引用變數的值是地址,他們無論如何交換地址是無法影響	到外邊的地址對映的,他們是值傳遞,值操作,但是這個地址是真的,我們真的獲得了堆地址,對堆地址進行操作真的可以產生影響。
	如果我們改變堆地址裡的東西,就真的會產生影響。在java操作中用淺拷貝並不明智,當我們改值的時候,如果修改了公共部分,就會對函式外造成影響,如果沒有改公共部分,那就沒什麼影響。用淺拷貝我們真正獲得的是堆地址上的共享部分,我們可以用形參對堆地址部分進行真正的影響,這就導致了對外部變數產生的真實影響。指標也是這個原理

	person a
	person b
	然後我們在方法中設定形參x = a,y = b。這樣我們讓形參獲得的東西是對地址的指向,也就是說x指向了堆地址中的某個部分,y指向了堆地址中的某個部分。
	堆地址是x和y的值。x和y和a,b完全沒有關係,二者的值無論如何對映,如何改變,對外界都沒關係,因為此時修改的是二者的值,但是通過二者的值我們可以真切的改變堆中資訊的狀態,因為外部的ab指向的也是堆中的地址,因此我們可以通過xy改變堆中地址,進而修改外部ab的資訊。xy可以作為邏輯理解輔助符號
	需要我們注意的東西就是,.就是地址訪問符號。基本型別的值是直接拷貝,引用型別是改變指向。基本型別的值和控制代碼在一起,基本型別賦值,就直接拷貝來了。
	作業系統中的棧都是用陣列實現的,不是鏈式儲存,都是順序儲存,因此他們確實是連續的,引用地址就是棧的地址,然後在棧的地址上邊就是會直接存放控制代碼,然後基本型別就是真的放在這裡,引用型別的控制代碼放在這裡,控制代碼指向值的地址,對於棧的地址,就被稱為引用地址。棧的地址上面直接儲存的就是相關變數資訊。
第四章很多面試要點,注意棧的地址被稱為引用地址,這裡是一個關鍵資訊。
	OOP是面向物件
	封裝 繼承 多型 抽象,這是面向物件的四個特徵
	封裝就是裡邊的原理實現全部封裝,只對外公開怎麼用,也就是對外提供簡單方便的介面,遮蔽內部複雜繁瑣的原理。
	變數的地址看在哪宣告,如果在類中宣告,就是在堆中,在方法中就在棧中
	靜態型別的物件,控制代碼在方法區,值仍然在堆中,靜態的變數型別,就是直接在方法區的靜態資源區

	字元陣列之類的東西,他們在字串常量池中,我們可以理解這個字串常量池也在堆區。總之對於一些引用型別的東西,他們通常都儲存在堆區,並且改他們的的值一般都是改引用指向,字串是比較基本的引用型別了,組成很單一,多半是字元陣列,因此很少對他們進行字元上的改動。
	我們要改變引用型別是,單純的改指向是沒用的,這和C語言也是一樣的,C語言中傳進來的就是地址,但是Java傳進來的其實是變數,我剛才這裡有點隱約不清,但是想來實際上就是這樣的。但儘管如此,Java中可以使用變數訪問到真正的地址,這就是二者的差異之處,二者儘管函式方法宣告呼叫很像,但實際上這裡存在微小的差別,很容易混淆,Java中的類和C語言中的結構體很類似,C中有普通型別的指標,Java中沒有。
	不要把C中普通型別的指標和Java中的物件弄混,物件很像結構體型別指標,傳進來的都是控制代碼,用.來進行訪問,在C語言中還能用->訪問
直接輸出物件可以輸出它的型別以及地址
	對於更改訪問私有屬性的方法,就是更改器和訪問器
	更改器和訪問器旨在保證安全,這個安全和黑客沒有關係,這個是防止多執行緒下的資料相互干擾問題。能夠保障多執行緒下,多程序下的資料相互干擾問題。簡單的寫法沒有任何防禦功能,必須加上一些處理才能有防禦功能。
	有了這種更改器和訪問器,我們就可以把一個數據控制為只讀或者只寫,乃至直接封裝起來,只用於內部輔助。
字串安全問題?這裡沒聽。
	基本型別的等於號都是深拷貝,訪問器裡邊必須加上深賦值處理才能保證多執行緒安全,否則不管用,我們可以更改訪問的方式,如果沒有這個處理,訪問器沒有作用。
	這裡一會再聽一遍吧。
	在類中,基本型別和字串型別無論如何都是安全的,首先基本型別是深複製,而字串型別是不可變型別,他們的訪問都是安全的。
	CPU的一個核心在同一時刻只能進行同一任務,也就是上邊每個時刻只能執行一個執行緒,多個執行緒併發在上邊輪流執行。多工並行實際上是任務的快速切換。
	執行緒進行切換要保持上個執行緒的狀態,這樣一來這個狀態開始時可以繼續這個執行緒的狀態,這種切換是有開銷的,有時間開銷,也有空間開銷,因為狀態的記錄與儲存需要記憶體。
	切換執行緒的時候是在安全點進行切換,一般來說安全點是在某個基礎的方法結束之後,有些方法中間也會切換,但總體上來,一般是在空擋之間確認安全點。
	因此執行緒立即執行,不是一個安全的做法,執行緒的具體執行方案是系統指定的,start方法是進入就緒態。作業系統中存在一個就緒佇列的,佇列中都是有待執行的任務,一個任務準備就緒之後作業系統才會排程他。所以我們可以解釋,進入就緒狀態的操作並不能決定執行的順序,只能是聽系統的。
	進入就緒態的意思就是告訴作業系統說我可以被執行了。其實上就是正式進入就緒佇列,就緒佇列中程序都是等待排程的程序。
線上程進入就緒態之後,執行緒的執行是從run方法開始的,只要執行的話,他們會在棧中建立兩個執行緒棧,每個執行緒棧裡都是各自的run方法,各自的run方法中呼叫其他方法,其實就是相當於拷貝這些被呼叫的方法並壓入自己的執行緒棧中。
	也就是說,執行緒其實就是在棧裡,執行緒的創立就伴隨著棧的創立。我們自己在就java中建立的執行緒,他們會獨自開闢自己的執行緒棧,他們屬於獨立的執行緒。
	兩個執行緒本身是物件,在呼叫的時候,會額外啟動執行緒的執行緒棧。
	程式能一直開著,是因為裡邊有個死迴圈。
	//執行緒的執行互不等待
	sleep是讓執行緒沉睡,裡邊的引數是沉睡的毫秒數
	有主方法的就叫主執行緒,在主執行緒中建立新執行緒後,會立刻構建一個新棧併成為一個新的執行緒,主子執行緒就沒關係了,剛出生就分家。執行緒就是剛出生就分家。執行緒在建立好後,三個執行緒誰都不等誰,三者就沒關係了。
	線上程建立好後,他們就沒有順序關係了,執行緒們不會相互等待,和在程式碼裡寫的start順序沒有關係。
主執行緒屬於當前正在執行執行緒,因此比其他執行緒的執行一般靠前,它有比較大的概率不會被CPU換掉,同時當主執行緒不長,很容易執行完,很可	能在當前時間片就能立刻執行完。
	程式碼中出現主執行緒總是先執行完的原因,這是因為主執行緒正在執行,如果它比較短,可能來不及切換就被執行完了,新執行緒的建立是需要時間的。
	這種情況就是,在激活了兩個執行緒之後,再進行一個主執行緒的操作,這個主執行緒的操作雖然在後,但是往往先執行完,這是因為主執行緒已經存在了,正在執行,且主執行緒較短,因此主執行緒可能沒有觸發執行緒切換,就已經被執行完了。執行緒是一直在棧中的,只有執行結束後才會釋放。


	附錄1:圖片

	執行緒中可以出現同名變數,因為作用域不同
	java中的棧區的不同棧就是執行緒的實體,系統中的執行緒全部都是由陣列構成的,他們都是連續的陣列。到時候再好好整理一下吧。

​ 附錄1: