Java 多線程/併發 Synchronized學習筆記

synchronized關鍵字可以添加在方法的聲明上,也可以添加在代碼塊中
添加在方法上時分兩種情況,當爲靜態方法時,表示的是對該類的.class對象上鎖
當不爲靜態方法時,表示的是對該類的對象上鎖。
添加在代碼塊時,需要指定上鎖的對象。

public class Synchonizedd {
    static Long start,end;
    static
    {
        start = System.currentTimeMillis();
    }
    //鎖定this對象
    public synchronized  void m1()  {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        end =System.currentTimeMillis();
        System.out.println(end-start+" m1");
    }
    //鎖定Synchonizedd.class 對象
    public static synchronized void m2(){
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        end =System.currentTimeMillis();
        System.out.println(end-start+" m2");
    }
    public void m3()  {
    	//鎖定this對象
        synchronized (this)
        {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            end =System.currentTimeMillis();
            System.out.println(end-start+" m3");
        }
    }

    public static void main(String[] args) {
        Synchonizedd synchonizedd = new Synchonizedd();
        new Thread(()->synchonizedd.m1()).start();
        new Thread(()->Synchonizedd.m2()).start();
        new Thread(()->synchonizedd.m3()).start();
    }
}

運行結果:
在這裏插入圖片描述
可以看到,因爲m1和m2方法鎖定的不是同一對象,所以調用m1方法的線程和調用m2方法的線程能夠並行執行。
而m1和m3中的synchronized代碼塊鎖定的是同一對象,調用兩個方法的線程不能並行運行,等到其中一個釋放該對象鎖之後另一個線程纔會運行。
synchnized 操作是原子操作,不可分。
一個synchnized 方法運行中,一個非同步方法是可以運行的

public class T2 {
    static long start,end;
    static
    {
        start = System.currentTimeMillis();
    }
    public synchronized  void m1()
    {
        System.out.println(Thread.currentThread().getName()+" m1");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        end=System.currentTimeMillis();
        System.out.println("當前時間爲:"+(end-start) +" m1 to end");
    }
    public  void m2()
    {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        end=System.currentTimeMillis();
        System.out.println("當前時間爲:"+(end-start)+" "+Thread.currentThread().getName()+" m2");
    }

    public static void main(String[] args) {
        T2 t2 = new T2();
        new Thread(()->t2.m1(),"Thread1").start();
        new Thread(()->t2.m2(),"Thread2").start();
    }
}

運行結果:
在這裏插入圖片描述
上述代碼中,我們開啓了兩個線程,一個線程執行m1方法,另一個執行m2方法,m1方法執行是需要獲得鎖的,m2方法不需要獲得鎖的,所以當m1執行的過程中,m2方法是可以同時執行的。
一個同步方法可以調用另一個同步方法,一個線程已經擁有某個對象的鎖,再次申請時仍然會得到該對象的鎖,也就是說synchronized獲得的鎖是可重入的。

public class T3 {
    public synchronized void m1()
    {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //執行同一個需要獲得鎖的方法
        m2();
        System.out.println(Thread.currentThread().getName()+" m1");
    }
    public synchronized  void m2()
    {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+" m2");
    }

    public static void main(String[] args) {
        T3 t3 = new T3();
        new Thread(()->t3.m1(),"th1").start();
    }
}

運行結果:
在這裏插入圖片描述
上述代碼中,我們開啓了一個線程,這個線程去執行m1方法,m1方法是需要獲得鎖的,m1方法中又需要調用m2方法,而m2方法也需要獲得鎖,這就形成了需要獲得T3類對象的兩次鎖,而實驗結果表明m2方法是可以執行的,這說明synchronized獲得的鎖是可重入的。
子類調用父類的同步方法,鎖定的是同一對象

public class T4 {
    synchronized void m()
    {
        System.out.println("m start");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("鎖定的對象是 "+this);
        System.out.println("m end");
    }

    public static void main(String[] args) {
        TT4 tt4 = new TT4();
        new Thread(()->tt4.m(),"Thread").start();
    }
}
class TT4 extends T4
{
    public synchronized void m()
    {
        System.out.println("child m start");
        super.m();
        System.out.println("鎖定的對象是 "+this);
        System.out.println("child m end");
    }
}

運行結果:
在這裏插入圖片描述
通過運行結果發現,鎖定的確實是同一對象。
程序在執行過程中,如果發現異常,默認情況下鎖是會被釋放。
所以,在併發處理的過程中,有異常要多加小心,不然可能會發生不一致的情況。
比如,在一個web app處理過程中,多個servlet線程共同訪問一個資源,這時如果異常處理不合適,在一個線程中拋出異常,其他線程就會進入同步代碼區,有可能訪問到異常產生時的數據。
因此要非常小心的處理同步業務邏輯中的異常。

public class T5 {
    int count = 0;
    public synchronized void m()
    {
        while (true) {
            count++;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 5) {
                //加上try catch塊則不會釋放鎖
                /*try {
                    int i = 1 / 0;
                }catch (Exception e)
                {
                    e.printStackTrace();
                }*/
                //拋出異常,並釋放鎖
                int i = 1 / 0;
            }
            System.out.println(Thread.currentThread().getName()+" count="+count);
        }
    }

    public static void main(String[] args) {
        T5  t5 = new T5();
        new Thread(()->t5.m(),"t1").start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->t5.m(),"t2").start();
    }
}

當不對異常進行處理時的運行結果(這裏截取出現異常後的運行結果):
在這裏插入圖片描述
當對異常進行trycatch處理時的運行結果:
在這裏插入圖片描述
上述代碼中,當拋出異常時,正在執行的方法會釋放鎖,而用trycatch處理之後,正在執行的方法不會釋放鎖。
synchronize 優化
同步代碼塊中的語句越少越好

public class T10 {
    int count = 0;
    synchronized void m1()
    {
        try {
            Thread.sleep(2000);
            for (int i=0;i<1000000;i++) count++;
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
    void m2()
    {
        try {
            Thread.sleep(2000);
            //業務邏輯中只有這句話需要sync,這時不應該給整個方法上鎖
            //採用細粒度的鎖,可以使線程爭用時間變短,從而提高效率
            synchronized (this)
            {
                for (int i=0;i<1000000;i++) count++;

            }
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

鎖定某個對象o,如果o的屬性發生改變,不影響鎖的使用。
但是如果o引用另外一個對象,則鎖定的對象改變。
應避免將鎖定對象的引用變成另外的對象

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

    void m1()
    {
        // 鎖定o引用的對象
        synchronized (o) {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("運行的線程:"+Thread.currentThread().getName() );
            }
        }
    }

    public static void main(String[] args) {
        T11 t11 = new T11();
        new Thread(()->t11.m1(),"t1").start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread t2= new Thread(()->t11.m1(),"t2");
        // 改變引用對象
        t11.o  = new Object();
        t2.start();
    }
}

運行結果:
在這裏插入圖片描述
上述代碼中,先開啓線程t1,然後t1執行m1方法,之後改變o的引用,然後開啓一個線程t2,可以看到t1和t2同時運行,這是因爲t2和t1獲得的鎖的對象是不一樣的。
就像下面圖片一樣:
在這裏插入圖片描述
不要以字符串常量作爲鎖定對象。
在下面的例子中,m1和m2其實鎖定的是同一個對象。
這種情況還會發生比較隱退的現象,比如你用到一個類庫,在該類庫中代碼鎖定了字符串"Hello"。
但是你讀不到源碼,所以在自己的代碼中也鎖定了"Hello",這時候就有可能發生非常詭異的死鎖阻塞。
因爲你的程序和你用到的類庫不經意間使用了同一把鎖

public class T12 {
    String s1 = "Hello";
    String s2 = "Hello";
     void m1()
    {
        synchronized (s1)
        {
            while(true)
            {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("運行的線程:"+Thread.currentThread().getName());
            }
        }
    }
    void m2()
    {
        synchronized (s2)
        {
            while(true)
            {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("運行的線程:"+Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        T12 t12 = new T12();
        new Thread(()->t12.m1(),"t1").start();
        new Thread(()->t12.m2(),"t2").start();
    }
}

運行結果:
在這裏插入圖片描述
使用Synchronized實現死鎖
死鎖形成的原理就是線程A需要線程B所佔用的鎖來執行程序,而線程B同時也需要線程A鎖佔用的鎖來執行程序,這就導致了兩個線程無休止的等待,等待對方釋放佔用的鎖。

public class DeadLock {

    public static void main(String[] args) {
        D1 d1 = new D1();
        D2 d2 = new D2();
        new Thread(()->d1.m1(d2),"Thread1").start();
        new Thread(()->d2.m1(d1),"Thread2").start();
    }
}
class D1
{

    public synchronized void  m1(D2 d2)
    {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
        d2.m2();
    }
    public synchronized void m2()
    {
        System.out.println("D1 m2");
    }
}
class D2
{

    public synchronized void m1(D1 d1)
    {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
        d1.m2();
    }
    public synchronized  void m2()
    {
        System.out.println("D2 m2");
    }
}

運行結果:
在這裏插入圖片描述
可見線程1和線程2都在等待對方釋放鎖,而自己需要等待對方釋放鎖之後才釋放自己的鎖,這就導致了死鎖的產生。

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