並行、併發、synchonrized同步的用法
一、多執行緒的併發與並行:
併發:多個執行緒同時都處在執行中的狀態。執行緒之間相互干擾,存在競爭,(CPU,緩衝區),每個執行緒輪流使用CPU,當一個執行緒佔有CPU時,其他執行緒處於掛起狀態,各執行緒斷續推進。
並行:多個執行緒同時執行,但是每個執行緒各自有自己的CPU,不存在CPU資源的競爭,他們之間也可能存在資源的競爭。
併發發生在同一段時間間隔內,並行發生在同一時刻內。併發執行的總時間是每個任務的時間和,而並行則取決於最長任務的時間。
下面看一下A,B兩個任務在並行和併發情況下是怎麼執行的:[不考慮其他資源競爭,只考慮CPU競爭]
A任務:a+b+c,
A任務有三步:a=1+1,b=2+2,c=3+3
B任務:x+y+z,
B任務有三步:x=1*1,y=2*2,z=3*3
A,B並行操作:多核CPU,多工並行。
CPU1:
CPU2:
圖中可以看到A,B操作相互不受影響。
當A任務開始的時候B任務也開始,他們可以同一時刻開始,如果每一個小任務耗時相同,那麼他們可能同時結束。
A,B併發操作:單核cpu,多工併發。
圖中可以看出A,B同時執行的時候,一定有一個先,有一個後,因為CPU只有一個,執行了A就不能執行B,但是為了讓兩個任務能夠“同時完成“,CPU先執行A的一部分,在執行B的一部分,當這個間隔時間非常短的時候我們看到的就是A,B都在執行。
舉個簡單的例子:
左右手同時握住兩支筆,並排點一個點,點出一條線來,這就是並行。只有一隻手握住一支筆,左點一下,右點一下直到畫出兩條線,這兩條線看似同時開始同時結束,實際是有時間差的,這就是併發。
實際操作中並不是嚴格的併發或者是並行,因為CPU有限,而任務是無限的。任務數量超過CPU核心數量的時候,就是併發並行共同存在的時候,不過這不是我們關注的重點,CPU資源的分配問題,是作業系統關心的重點。我們關心的重點是任務之間發生的資源競爭問題。
當多個執行緒對同一個資源進行訪問和操作的時候就會出現資料一致性問題。一致性問題得不到解決多個任務的操作永遠得不到正確的結果,解決一致性問題的方法就是同步機制。
二、synchonrized實現同步:
java每個物件都有一個內建鎖,當用synchonrized修飾方法或者程式碼塊的時候就會呼叫這個鎖來保護方法和程式碼塊。
同步方法:同步靜態方法,同步非靜態方法。
public synchronized void addMeethod2(){
num2++;
}
public static synchronized void addMeethod2(){
num2++;
}
同步程式碼塊:
synchronized(Object){...}:Object表示一個物件,synchronized獲取這個物件的同步鎖,保證執行緒獲取object的同步鎖之後其他執行緒不能訪問object的任何同步方法。但其他執行緒可以訪問非同步的部分。
public void addMeethod3(){
synchronized(this){
num3++;
}
}
public void addMeethod4(){
synchronized(num4){
num4++;
}
}
public void addMeethod5(){
synchronized(Learnlocks.class){
num5++;
}
}
同步當前物件this:public void addMeethod3(){synchronized(this){num3++;}}和同步非靜態方法:public synchronized void addMeethod2(){ num2++;}他們的效果是一樣的都是作用於當前物件,即獲取的object都是當前物件。那麼只有這個執行緒可以操作這個物件所有synchronized修飾的部分,執行完這部分內容才會釋放鎖,其他執行緒才能訪問。
下面看一下示例:模擬三個執行緒分別對不同的同步機制保護的資料進行操作,看他們的具體表現。
import java.util.concurrent.atomic.AtomicInteger;
public class NumAction {
private Integer num1=0;
private Integer num2=0;
private Integer num3=0;
private Integer num4=0;
private Integer num5=0;
private volatile Integer num6=0;
private AtomicInteger num7=new AtomicInteger(0);
public NumAction() {
}
//省略get/set方法public void Initializers(NumAction lk){
lk.num1=0;
lk.num2=0;
lk.num3=0;
lk.num4=0;
lk.num5=0;
lk.num6=0;
lk.num7=new AtomicInteger(0);
}
//----------------------------------------------------------重點部分------------------------------------------------------
public void addMeethod1(){
num1++;
}
public synchronized void addMeethod2(){
num2++;
}
public void addMeethod3(){
synchronized(this){
num3++;
}
}
public void addMeethod4(){
synchronized(num4){
num4++;
}
}
public void addMeethod5(){
synchronized(NumAction.class){
num5++;
}
}
public void addMeethod6(){
num6++;
}
public void addMeethod7(){
num7.incrementAndGet();
}
public void Add100() {
for (int i = 0; i < 100; i++) {
addMeethod1();
addMeethod2();
addMeethod3();
addMeethod4();
addMeethod5();
addMeethod6();
addMeethod7();
}
}
}
NumAction類:有七個屬性,八個方法,前七個方法分別給七個屬性自增一次。第八個方法是呼叫這七個方法100次。下面用多個執行緒來執行這個方法。
package com.eshore.ljcx.locks;
public class Learnlocks2 extends Thread{
private static NumAction numaction=new NumAction();
public void run() {
numaction.Add100();
}
public static void testRun(){
Learnlocks2 l1 = new Learnlocks2();
Learnlocks2 l2 = new Learnlocks2();
Learnlocks2 l3 = new Learnlocks2();
new Thread(l1).start();
new Thread(l2).start();
new Thread(l3).start();
try {
Thread.sleep(7000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(numaction.getNum1()+","+numaction.getNum2()+","+numaction.getNum3()+","+numaction.getNum4()+","+numaction.getNum5()+","+numaction.getNum6()+","+numaction.getNum7());
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
testRun();
numaction.Initializers(numaction);
}
}
}
我們啟動了三個執行緒去執行給每個資料自增一百次的操作,理想狀態下最終每個資料應該是300,再將這個步驟重複10次,看一下是不是一樣的結果。
結果如下:num2,num3,num5,num7是300,其他的資料都不正確。說明這四個資料的同步起到了作用,他們分別是:(同步方法,同步當前物件,同步類物件,Atom原子物件。)而volatitle關鍵字被稱為輕量級的同步機制並沒有起到應有的效果。同步屬性物件num4也並沒有作用。
299,300,300,300,300,299,300
297,300,300,300,300,300,300
292,300,300,297,300,297,300
275,300,300,296,300,294,300
298,300,300,300,300,300,300
283,300,300,300,300,297,300
297,300,300,300,300,300,300
297,300,300,300,300,297,300
299,300,300,300,300,300,300
296,300,300,299,300,298,300
同步num4為什麼沒有起到作用:因為實際上我們上鎖的是物件本身,並不是物件的引用。每次給物件自增,物件已經修改了,那麼我們每次獲取的鎖也不是同一個鎖。自然沒有辦法保持同步。如果我們新增一個不會被修改的屬性物件num0。
private Integer num0 =0;
修改方法四:
public void addMeethod4(){
synchronized(this.num0){
num4++;
}
}
結果如下:發現num4的輸出結果也是預期的300.
296,300,300,300,300,300,300
297,300,300,300,300,300,300
248,300,300,300,300,281,300
255,300,300,300,300,289,300
297,300,300,300,300,300,300
289,300,300,300,300,298,300
298,300,300,300,300,300,300
290,300,300,300,300,300,300
263,300,300,300,300,299,300
296,300,300,300,300,300,300
從num4可以看出來我們獲取num0的物件鎖,但是num4卻可以保持同步。這是因為num0這個物件鎖的程式碼塊是num0物件鎖的作用域,要對num4操作,必須獲取num0物件鎖。很多時候誤區可能在於我們要對那個資料保持同步就要獲取那個資料的鎖,這是很錯誤的理解。首先synchonrized獲取的是物件鎖,資料不是物件就滿足不了條件(個人見解,僅供參考)。
總結下:(synchonrized的用法在示例程式碼部分已經做了展示。)
對於同步方法:
1、所有靜態方法M0,M1。。:鎖定的物件是類
多執行緒用類使用M0,M1會阻塞:M0,M1屬於類,類加鎖,鎖相同
2、所有非靜態方法M2,M3:鎖定的物件是類的例項
多執行緒用不同的物件使用M2不會阻塞:物件不同,鎖不同
多執行緒用相同的物件使用M2會阻塞:物件相同,鎖相同
多執行緒用相同的物件使用M2,M3會阻塞:物件相同,鎖相同
多執行緒用不同的物件使用M2,M3不會阻塞:物件不同,鎖不同
*&*:
多執行緒訪問M1(靜態方法)和M2(非靜態方法)不會阻塞:M1的鎖是類,M2的鎖是物件,鎖不同
對於同步程式碼塊:
1、非靜態方法程式碼塊:鎖定的是指定的物件Object
同步程式碼塊當Object==this的時候,和同步方法效果相同,但是如果只作用了整個方法的一部分程式碼塊,那麼效率會更高。【作用內容大小帶來的差異】
同步程式碼塊當Object==X(X==指定一個不變的物件 final Integer X=0)的時候,和同步方法效果不同,不同點在於此時作用域是所有以X為鎖的程式碼塊,而同步方法會作用於物件下所有的同步方法。這也是同步程式碼塊相較於同步方法效率會更高的原因。【作用方法的數量帶來的差異】
2、靜態方法程式碼塊:鎖定的是指定的物件Object
同步程式碼塊當Object==this的時候,不多說,還是類(靜態方法類呼叫,怎麼也扯不到例項物件上去)
同步程式碼塊當Object==X(X==指定一個不變的物件 static final Integer X=0,X必須是靜態的,因為靜態方法只能使用靜態屬性)的時候,鎖的作用域是所有使用X作為鎖的程式碼塊。而同步靜態方法作用域是所有的同步方法。
併發機制提高任務效率,而同步機制是保證併發的安全,但是同時也會破壞併發效率,所以同步的鎖的粒度把握是個關鍵問題,在保證同步的情況下儘量保證效能才是關鍵。
對於非靜態欄位中可更改的資料,通常使用非靜態方法訪問.
對於靜態欄位中可更改的資料,通常使用靜態方法訪問,也可用非靜態訪問。不過是操作類。
我的總結:為甚麼理想狀態不是300?
因為,在自增的時候,資料不一致,也就是說,Thread1 取出資料此時為2,加上1變成3, 馬上Thread2 也取出資料此時還是2,加上1變成3,為什麼還是2?因為Thread1 來不及改變原來的值,就被Thread2 搶走執行權。最後Thread1 Thread2 都賦值給變數3,這樣就少了一次自增的機會。資料不一致。