天天看點

Java中的線程安全出現的原因和解決辦法(多個線程共享資源,臨界區)

線程安全出現的原因

這裡我們讓多個線程共享同一個賣票資源:

Java中的線程安全出現的原因和解決辦法(多個線程共享資源,臨界區)
public class RunnableImpl implements Runnable {
    //總的票得數量
    int tickets = 20;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                //為系統性能考慮,每個線程稍微休息一下
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "的第" + tickets + "張票");
                tickets--;
            }
        }
    }
}
           
public class DemoThreadSafe {
    public static void main(String[] args) {
        //建立Runnable接口的實作類對象
        RunnableImpl run = new RunnableImpl();
        //建立多個Thread類對象
        Thread r0 = new Thread(run);
        Thread r1 = new Thread(run);
        Thread r2 = new Thread(run);
        //用start方法開啟線程
        r0.start();
        r1.start();
        r2.start();
    }
}
           

輸出:

Thread-1的第20張票
Thread-2的第20張票
Thread-0的第20張票
Thread-2的第17張票
Thread-0的第16張票
Thread-1的第16張票
Thread-2的第14張票
Thread-0的第13張票
Thread-1的第12張票
Thread-2的第11張票
Thread-0的第11張票
Thread-1的第11張票
Thread-0的第8張票
Thread-2的第8張票
Thread-1的第6張票
Thread-2的第5張票
Thread-0的第4張票
Thread-1的第3張票
Thread-2的第2張票
Thread-0的第2張票
Thread-1的第0張票
Thread-2的第-1張票
           

從上面的例子可以看到, 有被重複賣出去的票,還有第-1,0張票,出現了線程安全問題。因為線程賣出了重複的票和不存在的票。

出現這個問題的原因是,我們建立的每個線程執行到

sleep()

語句之後就放棄了cpu的使用權。等到它睡醒之後,它照樣還是會往下執行, 不會管其它票的數量已經減少了。

Java中的線程安全出現的原因和解決辦法(多個線程共享資源,臨界區)

線程安全的解決辦法

1. 線程同步(synchronized)

同步代碼塊

  • 同步代碼塊synchronized

    關鍵字可以用于方法中的某個區塊中,表示對這個區塊的互斥通路
synchronized(同步鎖){
	需要同步互斥操作的代碼
}
           

注意:

  1. 通過代碼塊中的鎖對象,可以使用任意的對象
  2. 但是必須保證多個線程使用的鎖對象是同一個
  3. 鎖對象作用:

    把同步代碼塊鎖住,隻讓一個線程在同步代碼塊中執行

public class RunnableImpl implements Runnable {
    //總的票得數量
    int tickets = 20;

    //建立一個鎖對象
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            //同步代碼塊
           synchronized (obj){
               if (tickets > 0) {
                   //為系統性能考慮,每個線程稍微休息一下
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName() + "的第" + tickets + "張票");
                   tickets--;
               }
           }
        }
    }
}
           
Java中的線程安全出現的原因和解決辦法(多個線程共享資源,臨界區)

線程類中建立的obj鎖對象相當于作業系統中的pv操作,每個線程運作到synchronized的時候判斷鎖是否還有,有就拿來執行,沒有就等待别的線程歸還鎖。

2. 同步方法

  • 使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其它線程隻能在方法外等候
public synchronized void method(){
	可能會産生線程安全問題的代碼
}
           

同步鎖是誰?

對于非static方法,同步鎖就是this

對于static方法,是我們使用目前方法所在類的位元組碼對象(類名.class)

public class RunnableImpl implements Runnable {
    //總的票得數量
    int tickets = 20;

    @Override
    public void run() {
        while (true) {
            //同步代碼塊
           sellTickets();
        }
    }

    public synchronized void sellTickets(){
        if (tickets > 0) {
            //為系統性能考慮,每個線程稍微休息一下
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "的第" + tickets + "張票");
            tickets--;
        }
    }
}
           

Lock鎖

Java util.concurrent.locks.lock

機制提供了比

Synchronized代碼塊

synchronized方法

更加廣泛的鎖定操作,同步代碼塊、同步方法具有的功能Lock都有,除此之外更強大,更展現面向對象。

Lock鎖也稱作同步鎖,加鎖和釋放鎖方法化了:

  • public void lock()

    :加同步鎖
  • public void unlock()

    :釋放同步鎖
使用步驟:
  1. 在成員位置建立一個

    ReentrantLock對象

  2. 在可能會出現安全問題的代碼前調用

    Lock接口

    中的方法

    lock擷取

  3. 在可能會出現安全問題的代碼後調用

    Lock接口

    中的方法

    unlock釋放鎖

public class RunnableImpl implements Runnable {
    //總的票得數量
    int tickets = 20;

    //1. 在成員位置建立一個`ReentrantLock對象`
    Lock l = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //2. 在可能會出現安全問題的代碼前調用`Lock接口`中的方法`lock擷取`
            l.lock();
            if (tickets > 0) {
                //為系統性能考慮,每個線程稍微休息一下
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "的第" + tickets + "張票");
                tickets--;
            }
            //3. 在可能會出現安全問題的代碼後調用`Lock接口`中的方法`unlock釋放鎖`
            l.unlock();
        }
    }
}
           

為了確定鎖必須被釋放,推薦采用以下的寫法,将unlock()放在finally塊代碼語句中,無論是否出現異常,鎖都會被釋放。

public class RunnableImpl implements Runnable {
    //總的票得數量
    int tickets = 20;

    //1. 在成員位置建立一個`ReentrantLock對象`
    Lock l = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //2. 在可能會出現安全問題的代碼前調用`Lock接口`中的方法`lock擷取`
            l.lock();
            if (tickets > 0) {
                //為系統性能考慮,每個線程稍微休息一下
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + "的第" + tickets + "張票");
                    tickets--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //3. 在可能會出現安全問題的代碼後調用`Lock接口`中的方法`unlock釋放鎖`
                    l.unlock();
                }
            }
        }
    }
}