1. 程式人生 > >實現同步的幾種方式

實現同步的幾種方式

1.同步方法
即有synchronized關鍵字修飾的方法;
由於Java的每個物件都有一個內建鎖,當用此關鍵字修飾方法時,內建鎖會保護整個方法。在呼叫該方法之前,需要獲取內建鎖,否則就處於阻塞狀態。

2.同步程式碼塊
即有synchronized關鍵字修飾的語句塊;
程式碼如:
synchronized(object) {

}
注:同步是一種高開銷的操作,因此應該儘量減少同步的內容。通常沒有必要同步整個方法,使用synchronized程式碼塊同步關鍵程式碼即可。

3.使用volatile實現執行緒同步
(1)volatile關鍵字為域變數的訪問提供了一種免鎖機制;
(2)使用volatile修飾域,相當於告訴JVM,該域可能會被其它執行緒更新,因此每次使用該域時就要重新計算,而不是使用暫存器中的值;
(3)volatile不會提供任何原子操作,也不能用來修飾final型別的變數。

例項:如果一個銀行賬戶同時被兩個執行緒操作,一個取100塊,一個存錢100塊。假設賬戶原本有0塊,如果取錢執行緒和存錢執行緒同時發生,會出現什麼結果呢?
程式碼示例:
public class Test {

    private volatile static int count = 0; //賬戶餘額
    public static void main(String[] args) throws InterruptedException {
        new TAdd().start();
        new TSub().start();
    }
    
    public static void addMoney(int money) { //存錢
        synchronized(Test.class) {
            count+=money;
        }
        System.out.println(System.currentTimeMillis() + "存進" + money);
    }
    public static void subMoney(int money) { //取錢
        synchronized(Test.class) {
            if(count < money) {
                System.out.println("餘額不足");
                return;
            }
            count -= money;
        }
        System.out.println(System.currentTimeMillis() + "取出" + money);
    }
    public static void lookMoney() {
        System.out.println("賬戶餘額:"+count);
    }
    static class TAdd extends Thread {
        
        public void run() {
            while(true) {
                try {
                    Thread.sleep(1000);
                    addMoney(100);
                    lookMoney();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    static class TSub extends Thread {
        public void run() {
            while(true) {
                try {
                    Thread.sleep(1000);
                    subMoney(100);
                    lookMoney();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
執行結果:
1545704383398存進100
1545704383398取出100
賬戶餘額:0
賬戶餘額:0
餘額不足
賬戶餘額:100
1545704384399存進100
賬戶餘額:100(出錯了)
。。。。。。。。
出錯原因是輸出語句並沒有被synchronized同步。另外,volatile不能保證原子性,因此volatile並不能代替synchronized,而在這個例子中,通過synchronized將count包裹起來解決了原子性的問題。volatile的原理是在每次執行緒要訪問volatile修飾的變數時都是從記憶體中讀取,而不是從快取中讀取,因此每個執行緒訪問到的變數值都是一樣的。

4.使用重入鎖實現執行緒同步
在Java5中新增了java.util.concurrent包來支援同步。ReentrantLock類是可重入、互斥、實現了Lock介面的鎖,它與使用synchronized方法和塊具有相同的基本行為和語義,並且擴充套件了其能力。
ReentrantLock類的常用方法有:
ReentrantLock():建立一個ReentrantLock例項;
lock():獲得鎖;
unlock():釋放鎖;
注:ReentrantLock還有一個可以建立公平鎖的構造方法,但由於會大幅降低程式執行效率,不推薦使用。

程式碼示例:
public class Test {

    private volatile static int count = 0; //賬戶餘額
    
    private static Lock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        new TAdd().start();
        new TSub().start();
    }
    
    public static void addMoney(int money) { //存錢
        lock.lock();
        try {
            count+=money;
            System.out.println(System.currentTimeMillis() + "存進" + money);
        } finally {
            lock.unlock();
        }
    }
    public static void subMoney(int money) { //取錢
        lock.lock();
        try {
            if(count < money) {
                System.out.println("餘額不足");
                return;
            }
            count -= money;
            System.out.println(System.currentTimeMillis() + "取出" + money);
        } finally {
            lock.unlock();
        }
        
    }
    public static void lookMoney() {
        System.out.println("賬戶餘額:"+count);
    }
    static class TAdd extends Thread {
        
        public void run() {
            while(true) {
                try {
                    Thread.sleep(1000);
                    addMoney(100);
                    lookMoney();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    static class TSub extends Thread {
        public void run() {
            while(true) {
                try {
                    Thread.sleep(1000);
                    subMoney(100);
                    lookMoney();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
執行結果正常。
注:關於Lock物件和synchronized關鍵字的選擇
(1)如果synchronized能滿足使用者需求,就用synchronized,因為它能簡化程式碼;
(2)如果需要更高階的功能,就用ReentrantLock,此時需要注意及時釋放鎖,否則會出現死鎖,通常在finally中釋放鎖。

5.使用ThreadLocal實現執行緒同步
public class Test {
    
    private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
        
        @Override
        protected Integer initialValue() {

            return 0;
        }
    };
    public static void main(String[] args) throws InterruptedException {
        new TAdd().start();
        new TSub().start();
    }
    
    public static void addMoney(int money) { //存錢
            count.set(count.get() + money);
            System.out.println(System.currentTimeMillis() + "存進" + money);
        
    }
    public static void subMoney(int money) { //取錢
            if(count.get() < money) {
                System.out.println("餘額不足");
                return;
            }
            count.set(count.get() - money);
            System.out.println(System.currentTimeMillis() + "取出" + money);
        
        
    }
    public static void lookMoney() {
        System.out.println("賬戶餘額:"+count.get());
    }
    static class TAdd extends Thread {
        
        public void run() {
            while(true) {
                try {
                    Thread.sleep(1000);
                    addMoney(100);
                    lookMoney();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    static class TSub extends Thread {
        public void run() {
            while(true) {
                try {
                    Thread.sleep(1000);
                    subMoney(100);
                    lookMoney();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
執行結果:
餘額不足
賬戶餘額:0
1545709620876存進100
賬戶餘額:100
餘額不足
賬戶餘額:0
1545709621876存進100
賬戶餘額:200
餘額不足
賬戶餘額:0
1545709622876存進100
賬戶餘額:300
餘額不足
賬戶餘額:0
1545709623878存進100
賬戶餘額:400

如果使用ThreadLocal管理變數,則每個使用該變數的執行緒都獲得該變數的一個副本,副本之間相互獨立,這樣每個執行緒都可以隨意更改自己的變數副本,而不會對其它執行緒產生影響。
ThreadLocal類的常用方法:
ThreadLocal():建立一個執行緒本地變數;
get():返回此執行緒區域性變數的當前執行緒副本中的值;
initialValue():返回此執行緒區域性變數的當前執行緒的初始值;
set(T value):將此執行緒區域性變數的當前執行緒副本中的值設定為value。