多執行緒具體實現

炒燜煎糖板栗發表於2021-03-06

Java記憶體模型

image-20210306120236833

image-20210306120600523

執行緒同步

執行緒同步機制是一套適用於協調執行緒之間的資料訪問機制,該機制可以保障執行緒安全

java平臺提供的執行緒同步機制包括:鎖、volatile關鍵字、final關鍵字,static關鍵字、以及相關API如object.wait/object.notify

鎖概述

執行緒安全問題的產生前提是多個執行緒併發訪問共享資料,將多個資料對共享資料的併發訪問,轉化為序列訪問,即共享資料只能被一個執行緒訪問,鎖就是這種思路。

執行緒訪問資料時必須先獲得鎖,獲得鎖的執行緒稱為鎖的持有執行緒,一個鎖一次只能被一個執行緒持有,持有執行緒在獲得鎖之後和釋放鎖之前鎖執行的程式碼稱之為臨界區。

鎖具有排它性(Exclisive),即一個鎖只能被一個執行緒持有,這種鎖稱為排它鎖或者互斥鎖。

image-20210306122331678

JVM部分把鎖分為內部鎖和顯示鎖,內部鎖通過Synchronized關鍵字實現,顯示鎖通過java.concurrent.locks.Lock介面實現類實現的。

鎖的作用

鎖能夠實現對共享資料的安全,保障執行緒的原子性,可見性與有序性。

鎖是通過互斥保障原子性,一個鎖只能被一個執行緒持有,這就保證了臨界區的程式碼一次只能被一個執行緒執行,使得臨界區程式碼所執行的的操作自然而然的具有不可分割的特性,既具備了原子性。

好比一條路段所有車輛都在跑,併發執行,在經過某一個路段的時候,多車道變為一車道,一次只能通過一輛車,由併發執行改為序列執行。

可見性是通過寫執行緒沖刷處理器的快取和讀執行緒重新整理處理器快取這兩個動作,鎖的獲得隱含著重新整理處理器快取的動作,鎖的釋放隱含著沖刷處理器快取的動作。

鎖能夠保障有序性,寫執行緒在臨界區所執行的臨界區看來像是完全按照原始碼順序執行的。

鎖的相關概念

可重入性:一個執行緒持有該鎖的時候能夠再次/多次申請該鎖

如果一個執行緒持有一個鎖的時候,還沒有釋放,但還能夠繼續成功申請該鎖,稱該鎖可重入,反之。

鎖的爭用與排程

java中內部鎖屬於非公平鎖,顯示鎖支援非公平鎖和公平鎖

鎖的粒度

一個所可以保護的共享資料的數量大小稱為鎖的粒度。

鎖保護共享資料量大,稱為鎖粒度粗,否則稱為粒度細。

鎖的粒度過粗會導致執行緒在申請鎖時會進行不必要的等待,鎖粒度過細會增加鎖排程的開銷。

比如銀行有一個櫃檯一個員工可以辦理開卡、銷戶、取現、貸款那麼所有人都只能去這個櫃檯辦理業務,會需要很長的等待時間。但是如果把業務細分,一個業務一個櫃檯,這時候增加了銀行的開銷,需要三個員工。

內部鎖:Synchronized

Java中每一個物件都有一個與之關聯的內部鎖,這種鎖也叫監視器,是一種排它鎖,可以保障原子性、可見性、排它性。

Synchronized(物件鎖)
{
  同步程式碼塊,可以在同步程式碼塊中訪問共享資料
}

修飾例項方法稱為同步例項方法,修飾靜態方法稱為同步靜態方法。

Synchronized同步程式碼塊
public class SynchronizedLock {
    public static void main(String[] args) {
        SynchronizedLock synchronizedLock=new SynchronizedLock();
        for (int i = 0; i <2 ; i++) {
            new Thread(new RunnableThread())
            {
                @Override
                public void run() {
                    synchronizedLock.mm();
                }
            }.start();
        }
    }
    public  void  mm()
    {
        for (int i = 0; i <100 ; i++) {
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
    }
}

兩個執行緒的程式碼都在併發執行

image-20210306133725123

現在要列印的時候進行同步,同步的原理執行緒在執行的時候要先要獲得鎖

public class SynchronizedLock {
    public static void main(String[] args) {
        SynchronizedLock synchronizedLock=new SynchronizedLock();
        for (int i = 0; i <2 ; i++) {
            new Thread(new RunnableThread())
            {
                @Override
                public void run() {
                    synchronizedLock.mm();//使用鎖的物件是synchronizedLock物件
                }
            }.start();
        }
    }
    public  void  mm()
    {
        synchronized (this)//this作為當前物件
        {
            for (int i = 0; i <100 ; i++) {
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
        }
    }
}

image-20210306135337173

因為Synchronized內部鎖是排它鎖,一次只能被一個執行緒持有,現在是Thread-0先取得鎖物件,Thread-1在等待區等待Thread-0執行完畢釋放鎖,Thread-1獲得鎖再執行。

鎖物件不同不能實現同步

public class SynchronizedLock {
    public static void main(String[] args) {
        SynchronizedLock synchronizedLock=new SynchronizedLock();
        SynchronizedLock synchronizedLock2=new SynchronizedLock();
            new Thread(new RunnableThread())
            {
                @Override
                public void run() {
                    synchronizedLock.mm();//使用鎖的物件是synchronizedLock物件
                }
            }.start();
        new Thread(new RunnableThread())
        {
            @Override
            public void run() {
                synchronizedLock2.mm();//使用鎖的物件是synchronizedLock物件
            }
        }.start();
    }
    public  void  mm()
    {
        synchronized (this)//this作為當前物件
        {
            for (int i = 0; i <100 ; i++) {
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
        }
    }
}

image-20210306143528897

因此想要同步必須使用同一個鎖物件

使用常量作為鎖物件

public class SynchronizedLock {
    public static void main(String[] args) {
        SynchronizedLock synchronizedLock=new SynchronizedLock();
        SynchronizedLock synchronizedLock2=new SynchronizedLock();
            new Thread(new RunnableThread())
            {
                @Override
                public void run() {
                    synchronizedLock.mm();//使用鎖的物件是synchronizedLock物件
                }
            }.start();
        new Thread(new RunnableThread())
        {
            @Override
            public void run() {
                synchronizedLock.mm();//使用鎖的物件是synchronizedLock物件
            }
        }.start();
    }
    public  static  final  Object obj=new Object();
    public  void  mm()
    {
        synchronized (obj)//常量作為當前物件
        {
            for (int i = 0; i <100 ; i++) {
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
        }
    }
}
同步例項方法

使用synchronized修飾例項方法,同步例項方法,預設使用this作為鎖物件

public class SynchronizedLock {
   public static void main(String[] args) {
        SynchronizedLock synchronizedLock=new SynchronizedLock();
            new Thread(new RunnableThread())
            {
                @Override
                public void run() {
                    synchronizedLock.mm();
                }
            }.start();
        new Thread(new RunnableThread())
        {
            @Override
            public void run() { 
                synchronizedLock.mm2();
            }
        }.start();
    }
    //同步例項方法
    public  synchronized void  mm()
    {
            for (int i = 0; i <100 ; i++) {
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
    }
    public  void  mm2()
    {
        synchronized (this)//常量作為當前物件
        {
            for (int i = 0; i <100 ; i++) {
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
        }
    }
}
同步靜態方法

使用synchronized修飾靜態方法,同步靜態方法,預設執行時使用SynchronizedLock class作為鎖物件

public class SynchronizedLock {
    public static void main(String[] args) {
        SynchronizedLock synchronizedLock=new SynchronizedLock();
        new Thread(new RunnableThread())
        {
            @Override
            public void run() {
                synchronizedLock.mm2();
            }
        }.start();
        new Thread(new RunnableThread())
        {
            @Override
            public void run() {
                SynchronizedLock.mm();//使用鎖的物件是SynchronizedLock.class
            }
        }.start();
    }
    //同步靜態方法
    public  synchronized static void  mm()
    {
        for (int i = 0; i <100 ; i++) {
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
    }
    public  void  mm2()
    {
        synchronized (SynchronizedLock.class)//常量作為當前物件
        {
            for (int i = 0; i <100 ; i++) {
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
        }
    }
}
同步程式碼塊和同步方法如何選擇
public class SynchronizedLock {
    public static void main(String[] args) {
        SynchronizedLock synchronizedLock=new SynchronizedLock();
        new Thread(new RunnableThread())
        {
            @Override
            public void run() {
                try {
                    synchronizedLock.mm2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
        new Thread(new RunnableThread())
        {
            @Override
            public void run() {
                try {
                    synchronizedLock.mm2();//使用鎖的物件是SynchronizedLock.class
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
    //同步例項方法 鎖的粒度粗 執行效率低
    public  synchronized  void  mm() throws InterruptedException {
        long starttime= System.currentTimeMillis();
        System.out.println("start");
        Thread.sleep(3000);
        for (int i = 0; i <100 ; i++) {
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
        System.out.println("end");
        long Endtime= System.currentTimeMillis();
        System.out.println(Endtime-starttime);
    }
    //同步程式碼塊 鎖的粒度細 併發效率高
    public  void  mm2() throws InterruptedException {
        System.out.println("start");
        Thread.sleep(3000);
        synchronized (this)//常量作為當前物件
        {
            for (int i = 0; i <100 ; i++) {
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
        }
        System.out.println("end");
    }
}

在執行同步方法的時候,兩次執行緒呼叫每次都需要休眠三秒,而同步程式碼塊同時啟動執行緒都先準備三秒,效率比較高

髒讀
public class Test06 {
    public static void main(String[] args) throws InterruptedException {
        User user=new User();
        SubThread subThread=new SubThread(user);
        subThread.start();
        user.GetName();
    }
    static  class SubThread extends Thread
    {
        public User user;

        public SubThread(User user)
        {
            this.user=user;
        }

        @Override
        public void run() {
            user.SetValue("ww","456");
        }
    }
    static  class  User
    {
        private  String name="ylc";
        private  String pwd="123";
        public  void  GetName()
        {
            System.out.println(Thread.currentThread().getName()+"==>"+name+"密碼"+pwd);
        }
        public  void  SetValue(String name,String pwd)
        {
            System.out.println("原來為為name="+this.name+",pwd="+this.pwd);
            this.name=name;
            this.pwd=pwd;
            System.out.println("更新為name="+name+",pwd="+pwd);
        }

    }
}

image-20210306164026440

在修改資料還沒有完成的時候,就讀取到了原來的資料,而不是修改之後的

出現髒讀的原因是對共享資料的修改和讀取不同步引起的

解決辦法是對修改和讀取的方法進行同步方法上加上synchronized關鍵字

image-20210306164520617

執行緒出現異常釋放鎖

假如在同步方法中,一個執行緒出現了異常,會不會沒有釋放鎖,其他在等待的執行緒就在一直等待,論證:

public class SynchronizedLock {
    public static void main(String[] args) {
        SynchronizedLock synchronizedLock=new SynchronizedLock();
        new Thread(new RunnableThread())
        {
            @Override
            public void run() {
                synchronizedLock.mm();
            }
        }.start();
        new Thread(new RunnableThread())
        {
            @Override
            public void run() {
                synchronizedLock.mm2();
            }
        }.start();
    }
    //同步例項方法
    public  synchronized void  mm()
    {
        for (int i = 0; i <100 ; i++) {
            if(i==50)
            {
                Integer.parseInt("abc");//異常設定
            }
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
    }
    public  void  mm2()
    {
        synchronized (this)
        {
            for (int i = 0; i <100 ; i++) {
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
        }
    }
}

image-20210306170036211

同步過程中執行緒出現異常,會自動釋放鎖物件,以供下一個執行緒繼續執行

死鎖

多執行緒中可能需要使用多個鎖,如果獲取鎖的順序不一致,可能導致死鎖。

public class Text06_5 {
    public static void main(String[] args) {
        SubThread subThread=new SubThread();
        SubThread subThread2=new SubThread();
        subThread.setName("a"); subThread2.setName("b");
        subThread.start();subThread2.start();
    }
    static  class SubThread  extends  Thread
    {
        private  static  final Object lock1=new Object();
        private  static  final Object lock2=new Object();

        @Override
        public void run() {
            if("a".equals(Thread.currentThread().getName()))
            {
                synchronized (lock1)
                {
                    System.out.println("a 執行緒 lock1獲得了鎖,再需要獲得lock2");
                    synchronized (lock2)
                    {
                        System.out.println("a 執行緒 lock2獲得了鎖");
                    }
                }
            }

            if("b".equals(Thread.currentThread().getName()))
            {
                synchronized (lock2)
                {
                    System.out.println("b 執行緒 lock2獲得了鎖,再需要獲得lock1");
                    synchronized (lock1)
                    {
                        System.out.println(" b 執行緒 lock1獲得了鎖");
                    }
                }
            }
        }
    }
}

image-20210306171550782

程式還在執行,卻進入了卡死狀態,a執行緒得到了lock1,要想把該執行緒釋放的執行下面的程式碼獲取lock2,而lock2被b執行緒獲取無法釋放,出現了鷸蚌相爭的情況。

避免死鎖:當需要獲得鎖時,所有執行緒獲得鎖的順序一致,a執行緒先鎖lock1,再鎖lock2,b執行緒同理,就不會出現死鎖了。

相關文章