java高並發編程(一)


讀馬士兵java高並發編程,引用他的代碼,做個記錄。

一、分析下面程序輸出:  

/**
 * 分析一下這個程序的輸出
 * @author mashibing
 */

package yxxy.c_005;

public class T implements Runnable {

    private int count = 10;
    
    public synchronized void run() { 
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    
    public static void main(String[] args) {
        T t = new T();
        for(int i=0; i<5; i++) {
            new Thread(t, "THREAD" + i).start();
        }
    }
    
}
THREAD0 count = 9
THREAD4 count = 8
THREAD1 count = 7
THREAD3 count = 6
THREAD2 count = 5

分析:

啟動了5個線程,thread0先拿到這把鎖,開始執行,thread1-4都在等待准備搶這把鎖;thread0執行完之后,釋放鎖;thread4率先搶到了這把鎖,開始執行;執行完之后thread1又搶到了這把鎖,開始執行....;
所以看到每次線程訪問一次,count-1;而且thread執行的先后順序每次執行的結果不同,因為你不知道哪個線程先執行了;
 
二、對比上一個程序,分析這個程序的輸出:
/**
 * 對比上面一個小程序,分析一下這個程序的輸出
 * @author mashibing
 */

package yxxy.c_006;

public class T implements Runnable {

    private int count = 10;
    
    public synchronized void run() { 
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    
    public static void main(String[] args) {
        
        for(int i=0; i<5; i++) {
            T t = new T();
            new Thread(t, "THREAD" + i).start();
        }
    }
    
}
View Code
THREAD0 count = 9
THREAD4 count = 9
THREAD3 count = 9
THREAD1 count = 9
THREAD2 count = 9

分析:

啟動了5個線程,因為每次都是new了一個t,每個線程都能鎖住t,一共有5個t,5個count;所以這里5個線程執行完,count都是9;
但是因為不知道哪個線程先被cpu執行,所以thread名字的順序是隨機的;
 
三、同步和非同步方法是否可以同時調用?
/**
 * 同步和非同步方法是否可以同時調用?
 * @author mashibing
 */

package yxxy.c_007;

public class T {

    public synchronized void m1() { 
        System.out.println(Thread.currentThread().getName() + " m1 start...");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m1 end");
    }
    
    public void m2() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m2 ");
    }
    
    public static void main(String[] args) {
        T t = new T();
        
        new Thread(()->t.m1(), "t1").start();
        new Thread(()->t.m2(), "t2").start();
    }
    
}
t1 m1 start...
t2 m2 
t1 m1 end
分析:
t1線程執行m1方法,開始睡10s,在這過程之中,t2線程執行m2方法,5s之后打印了m2;由此可見在m1執行的過程之中,m2是可以運行的。
同步方法的執行過程中,非同步方法是可以執行的。只有synchronized這樣的方法在運行時候才需要申請那把鎖,而別的方法是不需要申請那把鎖的。
new Thread(()->t.m1())這個寫法是java8里面的Lambda表達式,一種簡寫,還可以寫成這樣:
public static void main(String[] args) {
        T t = new T();

        new Thread(t::m1, "t1").start();
        new Thread(t::m2, "t2").start();*/
    
}
就是之前最原始的寫法:
public static void main(String[] args) {
        T t = new T();
        
        new Thread(new Runnable(){
            @Override
            public void run() {
                t.m1();
            }
        }, "t1").start();
        
        new Thread(new Runnable(){
            @Override
            public void run() {
                t.m2();
            }
        }, "t2").start();
    }
View Code

 

四、對業務寫方法加鎖,對業務讀方法不加鎖,容易產生臟讀問題(dirtyRead)

/**
 * 對業務寫方法加鎖
 * 對業務讀方法不加鎖
 * 容易產生臟讀問題(dirtyRead)
 */

package yxxy.c_008;

import java.util.concurrent.TimeUnit;

public class Account {
    String name;
    double balance;
    
    public synchronized void set(String name, double balance) {
        this.name = name;
        
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        this.balance = balance;
    }
    
    public double getBalance(String name) {
        return this.balance;
    }
    
    
    public static void main(String[] args) {
        Account a = new Account();
        new Thread(()->a.set("zhangsan", 100.0)).start();
        
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println(a.getBalance("zhangsan"));
        
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println(a.getBalance("zhangsan"));
    }
}
View Code
0.0
100.0

分析:

主線程里面第一次讀zhangsan里面的錢是0.0,第二次讀是100.0;原因是set修改錢的時候過程中,sleep了2s鍾;為什么sleep 2s就是放大了在線程的執行過程之中的時間差,set錢方法里面this.name=name和this.balance=balance之間可能是會被別的程序執行的;
在線程的執行過程set錢之中,盡管寫的這個方法set加上了synchronized鎖定了這個對象,鎖定這個對象過程之中,它仍然有可能被那些非鎖定的方法/非同步方法訪問的;
盡管對寫進行了加鎖,但是由於沒有對讀加鎖,那么有可能會讀到在寫的過程中還沒有完成的數據,產生了臟讀問題;
 
解決:
對讀方法枷鎖:
public synchronized double getBalance(String name) {
        return this.balance;
    }
View Code

 

五、一個同步方法可以調用另外一個同步方法:

一個線程已經擁有某個對象的鎖,再次申請的時候仍然會得到該對象的鎖.
/**
 * 一個同步方法可以調用另外一個同步方法,一個線程已經擁有某個對象的鎖,再次申請的時候仍然會得到該對象的鎖.
 * 也就是說synchronized獲得的鎖是可重入的.(可重入的意思就是獲得鎖之后還可以再獲得一遍)
 * @author mashibing
 */
package yxxy.c_009;

import java.util.concurrent.TimeUnit;

public class T {
    synchronized void m1() {
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();
    }
    
    synchronized void m2() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2");
    }
}
View Code

分析:

對t執行m1的時候,需要在t上面加把鎖,拿到這個鎖了,開始執行,執行鎖定的過程之中,調用了m2();
調用m2的過程中,發現m2也是需要申請一把鎖,而申請的這把鎖就是當前自己已經持有的這把鎖;
嚴格來講,這把鎖m1已經持有了,m2還能持有嗎?由於是在同一個線程里面,這個是沒關系的。它可以再去申請我自己已經擁有的這把鎖,實際上就在這把鎖上加個數字,從1變成2,鎖定了2次。總而言之,再去申請當前持有的這把鎖沒問題,仍然會得到該對象的鎖。

 

六、重入鎖的另外一種情形,繼承中子類的同步方法調用父類的同步方法

/**
 * 一個同步方法可以調用另外一個同步方法,一個線程已經擁有某個對象的鎖,再次申請的時候仍然會得到該對象的鎖.
 * 也就是說synchronized獲得的鎖是可重入的
 * 這里是繼承中有可能發生的情形,子類調用父類的同步方法
 * @author mashibing
 */
package yxxy.c_010;

import java.util.concurrent.TimeUnit;

public class T {
    synchronized void m() {
        System.out.println("m start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m end");
    }
    
    public static void main(String[] args) {
        new TT().m();
    }
    
}

class TT extends T {
    @Override
    synchronized void m() {
        System.out.println("child m start");
        super.m();
        System.out.println("child m end");
    }
}
View Code
child m start
m start
m end
child m end
View Code

 

七、synchronized同步方法如果遇到異常,鎖就會被釋放

/**
 * 程序在執行過程中,如果出現異常,默認情況鎖會被釋放
 * 所以,在並發處理的過程中,有異常要多加小心,不然可能會發生不一致的情況。
 * 比如,在一個web app處理過程中,多個servlet線程共同訪問同一個資源,這時如果異常處理不合適,
 * 在第一個線程中拋出異常,其他線程就會進入同步代碼區,有可能會訪問到異常產生時的數據。
 * 因此要非常小心的處理同步業務邏輯中的異常
 * @author mashibing
 */
package yxxy.c_011;

import java.util.concurrent.TimeUnit;

public class T {
    int count = 0;
    synchronized void m() {
        System.out.println(Thread.currentThread().getName() + " start");
        while(true) {
            count ++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
            try {
                TimeUnit.SECONDS.sleep(1);
                
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            if(count == 5) {
                int i = 1/0; //此處拋出異常,鎖將被釋放,要想不被釋放,可以在這里進行catch,然后讓循環繼續
            }
        }
    }
    
    public static void main(String[] args) {
        T t = new T();
        
        new Thread(()->t.m(), "t1").start();
        
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        new Thread(()->t.m(), "t2").start();
    }
    
}
View Code
執行結果:
t1 start
t1 count = 1
t1 count = 2
t1 count = 3
t1 count = 4
t1 count = 5
t2 start
t2 count = 6
Exception in thread "t1" java.lang.ArithmeticException: / by zero
    at yxxy.c_011.T.m(T.java:28)
    at yxxy.c_011.T.lambda$0(T.java:36)
    at java.lang.Thread.run(Thread.java:745)
t2 count = 7
t2 count = 8
t2 count = 9
View Code
分析:
t1線程啟動后,如果int i=1/0這里拋了異常后,鎖不被釋放的話,t2線程就永遠啟動不起來,永遠執行不了;
但是拋出異常之后,鎖被釋放了,t2得到了執行;
 
解決:
處理異常,鎖不被釋放,循環繼續,t2線程永遠執行不了:
/**
 * 程序在執行過程中,如果出現異常,默認情況鎖會被釋放
 * 所以,在並發處理的過程中,有異常要多加小心,不然可能會發生不一致的情況。
 * 比如,在一個web app處理過程中,多個servlet線程共同訪問同一個資源,這時如果異常處理不合適,
 * 在第一個線程中拋出異常,其他線程就會進入同步代碼區,有可能會訪問到異常產生時的數據。
 * 因此要非常小心的處理同步業務邏輯中的異常
 * @author mashibing
 */
package yxxy.c_011;

import java.util.concurrent.TimeUnit;

public class T {
    int count = 0;
    synchronized void m() {
        System.out.println(Thread.currentThread().getName() + " start");
        while(true) {
            count ++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
            try {
                TimeUnit.SECONDS.sleep(1);
                
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            if(count == 5) {
                try{
                    int i = 1/0; //此處拋出異常,鎖將被釋放,要想不被釋放,可以在這里進行catch,然后讓循環繼續
                }catch(Exception e){
                    System.out.println(e.getMessage());
                }
            }
        }
    }
    
    public static void main(String[] args) {
        T t = new T();
        
        new Thread(()->t.m(), "t1").start();
        
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        new Thread(()->t.m(), "t2").start();
    }
    
}
View Code
t1 start
t1 count = 1
t1 count = 2
t1 count = 3
t1 count = 4
t1 count = 5
/ by zero
t1 count = 6
t1 count = 7
t1 count = 8
t1 count = 9
t1 count = 10
t1 count = 11
t1 count = 12
View Code

 

八、volatile關鍵字              

/**
 * volatile 關鍵字,使一個變量在多個線程間可見
 * A B線程都用到一個變量,java默認是A線程中保留一份copy,這樣如果B線程修改了該變量,則A線程未必知道
 * 使用volatile關鍵字,會讓所有線程都會讀到變量的修改值
 * 
 * 在下面的代碼中,running是存在於堆內存的t對象中
 * 當線程t1開始運行的時候,會把running值從內存中讀到t1線程的工作區,在運行過程中直接使用這個copy,並不會每次都去
 * 讀取堆內存,這樣,當主線程修改running的值之后,t1線程感知不到,所以不會停止運行
 * 
 * 使用volatile,將會強制所有線程都去堆內存中讀取running的值
 * 
 * 可以閱讀這篇文章進行更深入的理解
 * http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
 * 
 * volatile並不能保證多個線程共同修改running變量時所帶來的不一致問題,也就是說volatile不能替代synchronized
 * @author mashibing
 */
package yxxy.c_012;

import java.util.concurrent.TimeUnit;

public class T {
    volatile boolean running = true; //對比一下有無volatile的情況下,整個程序運行結果的區別
    void m() {
        System.out.println("m start");
        while(running) {
        }
        System.out.println("m end!");
    }
    
    public static void main(String[] args) {
        T t = new T();
        
        new Thread(t::m, "t1").start();
        
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        t.running = false;
    }
    
}
View Code

分析:

不加volatile是不行的,線程1沒法結束,那么volatile到底是干嘛的?
線程之間要讓running這個值進行可見,這里要涉及到java的內存模型,java對於線程處理的內存模型;
在jmm(java memory model)里面有個內存它叫主內存,我們所熟識的棧內存,堆內存都可以認為是主內存;每一個線程在執行的過程之中,它有一個線程自己的一塊內存,(實際上不能認為這塊是內存,有可能它是內存,還有cpu上的緩沖區,是一個統稱,就是線程存放它自己變量的一塊內存),如果兩個cpu在運行不同線程的話,每個線程上都有自己的一塊緩沖區,緩沖區就是把主內存JMM里面的內容讀過來在緩沖區里面進行修改,如果+1,+1加了好多次再寫回去;
現在有個running在主內存里面,值是true,占一個字節;
第一個線程啟動的時候會把這個字節copy到自己的緩沖區里面,cpu在處理的過程之中就不再去主內存里面讀了;它在運行這個線程的過程之中,由於這個cpu非常的忙,在while(running)里面,沒空再去主線程里面去刷一下running值了;它一直讀自己緩存里面的內容,running永遠是true;
第二個主線程里面,它首先也是把running讀到它自己的緩沖區,然后把running改成false,發現running已經改了那就把running寫回到主內存里面去;寫回到主內存之后,但是第一個線程它沒有在主內存重新讀啊,所以第一個線程永遠結束不了;
 
加了volatile,第一個線程運行中,不是要求你每次while(running)循環的時候都要到主內存里面讀一次running的值,而是說一旦主內存running這個值發生改變后會通知別的線程,說你們的緩沖區里面內容過期了請重新讀一下,第一個線程再去讀的時候running已經改了,所以線程結束了。
加了volatile的意思就是當running改了后會通知其他的所有線程的緩沖區,說你們那邊的值已經過期了,請你們再去主內存里面重新讀一下。
而並不是通知所有的線程cpu執行的時候每次用的時候都要去主內存讀一下,不是,是寫完之后進行緩存過期通知。
 
要保證線程之間的可見性,那么需要對兩個線程共同訪問的變量加上volatile;如果不想加volatile那只能用synchronized;但volatile的效率要比synchronized高的多;所以在很多高並發的框架里面好多的volatile關鍵字都在用;比如JDK的並發容器的源碼;能用volatile的時候就不要加鎖,程序的並發性就要提高很多;

 圖:

 

九、volatile並不能保證多個線程共同修改running變量時所帶來的不一致問題,也就是說volatile不能替代synchronized

/**
 * volatile並不能保證多個線程共同修改running變量時所帶來的不一致問題,也就是說volatile不能替代synchronized
 * 運行下面的程序,並分析結果
 * @author mashibing
 */
package yxxy.c_013;

import java.util.ArrayList;
import java.util.List;

public class T {
    volatile int count = 0; 
    void m() {
        for(int i=0; i<10000; i++) count++;
    }
    
    public static void main(String[] args) {
        T t = new T();
        
        List<Thread> threads = new ArrayList<Thread>();
        
        for(int i=0; i<10; i++) {
            threads.add(new Thread(t::m, "thread-"+i));
        }
        
        threads.forEach((o)->o.start());
        
        threads.forEach((o)->{
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        System.out.println(t.count);
        
    }
    
}
View Code

volatile和synchronized區別?

volatile只保證可見性,並不保證原子性;
synchronized既保證可見性,又保證原子性;但效率要比volatile低不少;
如果只需要保證可見性的時候,使用volatile,不要使用synchronized;
 
Thread.join()方法解釋見:https://www.cnblogs.com/huangzejun/p/7908898.html
 
十、對比上一個程序,可以用synchronized解決
/**
 * 對比上一個程序,可以用synchronized解決,synchronized可以保證可見性和原子性,volatile只能保證可見性
 * @author mashibing
 */
package yxxy.c_014;

import java.util.ArrayList;
import java.util.List;

public class T {
    int count = 0;
    synchronized void m() { 
        for (int i = 0; i < 10000; i++) count++;
    }

    public static void main(String[] args) {
        T t = new T();

        List<Thread> threads = new ArrayList<Thread>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }

        threads.forEach((o) -> o.start());

        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);
    }
}
View Code

運行結果:100000

 

如果你只是處理這種++,--,這一類的操作,只是涉及到一些簡單的數字運算,在java里面提供了一些原子類;見下面十一:
 

十一、使用AtomXXX類                                       

/**
 * 解決同樣的問題的更高效的方法,使用AtomXXX類
 * AtomXXX類本身方法都是原子性的,但不能保證多個方法連續調用是原子性的
 * @author mashibing
 */
package yxxy.c_015;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class T {
    AtomicInteger count = new AtomicInteger(0); 

    void m() {
        for (int i = 0; i < 10000; i++)
            count.incrementAndGet();  //count++
    }

    public static void main(String[] args) {
        T t = new T();

        List<Thread> threads = new ArrayList<Thread>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }

        threads.forEach((o) -> o.start());

        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);
    }
}
View Code

運行結果:100000

AtomicInteger:原子性操作的int類型;
incrementAndGet(): 原子方法,你可以認為它是加了synchronized的,當然它內部實現不是用synchronized的而是用系統相當底層的實現來去完成的;它的效率要比synchronized高很多;
 
 
 
十二、synchronized優化            
/**
 * synchronized優化
 * 同步代碼塊中的語句越少越好
 * 比較m1和m2
 * @author mashibing
 */
package yxxy.c_016;

import java.util.concurrent.TimeUnit;

public class T {
    
    int count = 0;

    synchronized void m1() {
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //業務邏輯中只有下面這句需要sync,這時不應該給整個方法上鎖
        count ++;
        
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    void m2() {
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //業務邏輯中只有下面這句需要sync,這時不應該給整個方法上鎖
        //采用細粒度的鎖,可以使線程爭用時間變短,從而提高效率
        synchronized(this) {
            count ++;
        }
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
View Code

分析:

m2()的並發效率要比m1()高不少;細粒度的鎖執行效率要比粗粒度的鎖執行效率要高不少;
 
 
 
 
十三、避免將鎖定對象的引用變成另外的對象,例子:
/**
 * 鎖定某對象o,如果o的屬性發生改變,不影響鎖的使用
 * 但是如果o變成另外一個對象,則鎖定的對象發生改變
 * 應該避免將鎖定對象的引用變成另外的對象
 * @author mashibing
 */
package yxxy.c_017;

import java.util.concurrent.TimeUnit;

public class T {
    Object o = new Object();

    void m() {
        synchronized(o) {
            while(true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }
    }
    
    public static void main(String[] args) {
        T t = new T();
        //啟動第一個線程
        new Thread(t::m, "t1").start();
        
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //創建第二個線程
        Thread t2 = new Thread(t::m, "t2");
        
        t.o = new Object(); //鎖對象發生改變,所以t2線程得以執行,如果注釋掉這句話,線程2將永遠得不到執行機會
        
        t2.start();
    }
}
View Code

運行結果:

t1
t1
t1
t2
t1
t2
t1
t2
t1
t2
t1
t2
t1
t2
t1
t2
t1
t2
t1
...

分析:

t.o = new Object();鎖的對象發生改變,就不需要鎖原來的對象,直接鎖新對象就行了;而新對象還沒有鎖的,所以t2線程就被執行了;
所以,這就證明這個鎖是鎖在什么地方?是鎖在堆內存里new出來的對象上,不是鎖在棧內存里頭o的引用,不是鎖的引用,而是鎖new出來的真正的對象;
鎖的信息是記錄在堆內存里的。
 
 
 
 
十四、不要以字符串常量作為鎖定對象
/**
 * 不要以字符串常量作為鎖定對象
 * 在下面的例子中,m1和m2其實鎖定的是同一個對象
 * 這種情況還會發生比較詭異的現象,比如你用到了一個類庫,在該類庫中代碼鎖定了字符串“Hello”,
 * 但是你讀不到源碼,所以你在自己的代碼中也鎖定了"Hello",這時候就有可能發生非常詭異的死鎖阻塞,
 * 因為你的程序和你用到的類庫不經意間使用了同一把鎖
 * 
 * jetty
 * 
 * @author mashibing
 */
package yxxy.c_018;

public class T {
    
    String s1 = "Hello";
    String s2 = "Hello";

    void m1() {
        synchronized(s1) {
            
        }
    }
    
    void m2() {
        synchronized(s2) {
            
        }
    }
}
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM