Java 多線程 02

產生線程安全問題的原因:

線程的working memory是cpu的寄存器和高速緩存的抽象描述:現在的計算機,cpu在計算的時候,並不總是從內存讀取數據,它的數據讀取順序優先級 是:寄存器-高速緩存-內存。線程耗費的是CPU,線程計算的時候,原始的數據來自內存,在計算過程中,有些數據可能被頻繁讀取,這些數據被存儲在寄存器和高速緩存中,當線程計算完後,這些緩存的數據在適當的時候應該寫回內存。當多個線程同時讀寫某個內存數據時,就會產生多線程併發問題

 

同步和異步

同步:多個線程共享內存,因此需要一個等待機制。多個需要同時訪問某個對象的線程需要進入這個對象的等待池形成隊列,並且對象有鎖的機制。形象的來說,就是一堆人排隊上廁所,有隊列,有鎖。

異步:每個線程都包含了運行時自身所需要的數據和方法,不必關心其他線程在幹什麼。

 

幾種線程不安全的例子:

 買票的案例:

public class UnsafeTicket implements Runnable{

    private int ticketNum=15;
    private volatile boolean stop=false;

    @Override
    public void run() {
        while (!stop)
        {
            buy();
        }
    }

    public static void main(String[] args) {
        UnsafeTicket unsafeTicket = new UnsafeTicket();
        new Thread(unsafeTicket,"thread1").start();
        new Thread(unsafeTicket,"thread2").start();
        new Thread(unsafeTicket,"thread3").start();
    }

    private void buy()
    {
        if(ticketNum<=0)
        {
            System.out.println("票賣完了,現在還有" + ticketNum + "張票");
            stop=true;
            return;
        }
        else
        {
            System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum + "張票");
            ticketNum--;
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

不安全的List

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

public class UnsafeList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
               list.add(Thread.currentThread().getName());
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        System.out.println(list.size());
    }
}

雖然有10000個線程進行了添加,但是最終輸出結果List大小隻有9133.這是因爲線程不安全,有的線程添加時直接覆蓋了其他線程已經添加的位置。

 

使用synchronized方法或者synchronized代碼塊實現同步

類似於  private synchronized void buy(){}

把需要同步的操作放到這個同步方法中,這個同步方法在同一時刻只有一個線程能訪問。實際上這時鎖住的是this這個對象。會大大影響效率,特別是方法體裏面東西特別多的時候。

 

synchronized代碼塊則比較方便,鎖住的是synObject

synchronized(synObject)

{

//操作synObject

}

注意鎖不住Integer等對象:
https://blog.csdn.net/mononoke111/article/details/88742903

下面的例子只是示範一下同步代碼塊怎麼用

public class UnsafeTicket implements Runnable{

    Num num= new Num(15);
    private volatile boolean stop=false;

    @Override
    public void run() {
        while (!stop)
        {
            buy();
        }
    }

    public static void main(String[] args) {
        UnsafeTicket unsafeTicket = new UnsafeTicket();
        new Thread(unsafeTicket,"thread1").start();
        new Thread(unsafeTicket,"thread2").start();
        new Thread(unsafeTicket,"thread3").start();
    }

    private void buy()
    {
        synchronized (num)
        {
            if(num.getTickNum()<=0)
            {
                System.out.println("票賣完了,現在還有" + num.getTickNum() + "張票");
                stop=true;
                return;
            }
            else
            {
                System.out.println(Thread.currentThread().getName() + "拿到了第" + num.getTickNum() + "張票");
                num.setTickNum(num.getTickNum()-1);
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

class Num{
    private int tickNum;

    Num(int tickNum) {
        this.tickNum = tickNum;
    }

    public int getTickNum() {
        return tickNum;
    }

    public void setTickNum(int tickNum) {
        this.tickNum = tickNum;
    }
}

 

說到幾種鎖的分類,其實也不是很嚴格的分類,很多的時候是不同的使用思想:

https://zhuanlan.zhihu.com/p/147920568

 

 

死鎖

多個線程各自佔有一些資源,並且互相等待其他其他線程釋放資源。即相互等待對方資源。

public class MakeUp extends Thread{
    public static final LipStick lipStick=new LipStick();
    public static final Mirror mirror=new Mirror();

    private int choice;
    private String name;

    public MakeUp(String name, int choice) {
        this.choice = choice;
        this.name = name;
    }


    @Override
    public void run() {
        if (choice==0)
        {
            synchronized (mirror)
            {
                System.out.println(this.name+"拿到了鏡子");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lipStick)
                {
                    System.out.println(this.name+"拿到了口紅");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        }
        else
        {
            synchronized (lipStick)
            {
                System.out.println(this.name+"拿到了口紅");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (mirror)
                {
                    System.out.println(this.name+"拿到了鏡子");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        }
    }

    public static void main(String[] args) {
        new MakeUp("thread1", 1).start();
        new MakeUp("thread1", 0).start();

    }




}

class LipStick{

}

class Mirror{

}

這個例子很好的解釋了,抱着鎖然後等待其他鎖的例子。

這個例子中,分開就行了。

死鎖的四個必要條件:

 

這也是解決死鎖問題的四種方法:

1、破壞不剝奪條件:讓對面的司機放棄了自己已有的資源。

2、破壞請求與保持條件:在自己需要的材料缺少時,主動放棄自己持有的資源,防止出現互相等待。

3、破壞循環等待條件:由於筷子指定了編號和獲取規則,所以每個鎖定狀態都將按照順序執行,於是便杜絕了環路等待條件。

4、破壞互斥條件:由於每次使用時都拷貝一份,所以一個資源可以被多個進程使用。

事實上,使用預先拷貝資源解決死鎖問題的方案一般並不常用。這是由於拷貝的成本往往很大,並且影響效率。實際工作中較常採用的是第三種方案,通過控制加鎖順序解決死鎖:

  • 加鎖順序:當多個線程需要相同的一些鎖,但是按照不同的順序加鎖,死鎖就很容易發生。如果能確保所有的線程都是按照相同的順序獲得鎖,那麼死鎖就不會發生。當然這種方式需要你事先知道所有可能會用到的鎖,然而總有些時候是無法預知的。

除此之外,我們還可以通過設置加鎖時限或添加死鎖檢測避免死鎖:

  • 加鎖時限:加上一個超時時間,若一個線程沒有在給定的時限內成功獲得所有需要的鎖,則會進行回退並釋放所有已經獲得的鎖,然後等待一段隨機的時間再重試。但是如果有非常多的線程同一時間去競爭同一批資源,就算有超時和回退機制,還是可能會導致這些線程重複地嘗試但卻始終得不到鎖。
  • 死鎖檢測:死鎖檢測即每當一個線程獲得了鎖,會在線程和鎖相關的數據結構中( map 、 graph 等)將其記下。除此之外,每當有線程請求鎖,也需要記錄在這個數據結構中。死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖並且鎖超時也不可行的場景。

其中,死鎖檢測最出名的算法是由艾茲格·迪傑斯特拉在 1965 年設計的銀行家算法,通過記錄系統中的資源向量、最大需求矩陣、分配矩陣、需求矩陣,以保證系統只在安全狀態下進行資源分配,由此來避免死鎖,對於面算法崗的同學一定要對其有所瞭解。

 

避免死鎖可以概括成三種方法:

    • 固定加鎖的順序(針對鎖順序死鎖,鎖對象的hashCode進行排序,銀行家算法)
    • 開放調用(針對對象之間協作造成的死鎖)
    • 使用定時鎖-->tryLock()
      • 如果等待獲取鎖時間超時,則拋出異常而不是一直等待!

 

 

可重入鎖

顯式定義鎖,鎖的是自己創建的Lock對象。

ReentrantLock可重入鎖,實現了Lock接口,一般就用這個來鎖了。

 

 

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class UnsafeTicket implements Runnable{

    private int num=15;
    private volatile boolean stop=false;
    private final Lock lock= new ReentrantLock();

    @Override
    public void run() {
        while (!stop)
        {
            buy();
        }
    }

    public static void main(String[] args) {
        UnsafeTicket unsafeTicket = new UnsafeTicket();
        new Thread(unsafeTicket,"thread1").start();
        new Thread(unsafeTicket,"thread2").start();
        new Thread(unsafeTicket,"thread3").start();
    }

    private void buy()
    {
        try {
            lock.lock();
            if(num>=0)
            {
                System.out.println(Thread.currentThread().getName() + "拿到了第" + num + "張票");
                num--;
                Thread.sleep(1000);
            }
            else
            {
                System.out.println("票賣完了");
                stop=true;
                return;
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            lock.unlock();
        }
    }
}

 

 

線程池

https://zhuanlan.zhihu.com/p/132748927

https://zhuanlan.zhihu.com/p/123328822

 

 

線程通信

提到了生產者消費者模式

兩種方式:管程法(緩存區法),信號燈法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章