2 java並行基礎

咱們認真研究如何才能構建一個正確、健壯而且高效的並行系統。java

進程與線程

進程(Process):是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操做系統結構的基礎。數據庫

進程是線程的容器。程序是指令、數據和其組織形式的描述,進程是程序的實體。進程中能夠容納若干個線程。安全

進程和線程的關係:線程就是輕量級的進程,是程序執行的最小單位。爲何咱們使用多線程而不是多進程?由於線程間的切換調度成本遠遠小於進程,因此咱們使用多線程而不是多進程。多線程

線程的生命週期併發

線程的全部狀態都在Thread中的State枚舉中定義。ide

public enum State{
  NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
}

NEW狀態表示剛剛建立的線程,這種線程還沒開始執行。start()方法調用時,線程開始執行。當線程執行時,處於RUNABLE狀態,表示線程所需的一切資源都已經準備好了。函數

若是線程在執行過程當中遇到了synchronized同步塊,就會進入BLOCKED阻塞狀態,這時線程就會暫停執行,直到得到請求的鎖。優化

waitingtime_waiting都表示等待狀態,它們的區別是waiting會進入一個無時間限制的等待,time_waiting會進行有時間限制的等待狀態。通常說,waiting的線程是在等待一些特殊的事件。好比,經過wait()方法等待的線程在等待notify()方法,而經過join()方法等待的線程則會等待目標線程的終止。一旦等到了指望的事件,線程就會再次執行,進入runnable狀態。當線程執行完畢後,則進入terminated狀態,表示結束。this

注意:從new狀態出發後,線程不能再回到NEW狀態,同理,處以TERMINATED的線程也不能再回到RUNNABLE狀態。操作系統

初始線程:線程的基本操做

這節瞭解一下java爲線程操做提供的一些API。

新建線程

新建線程很簡單,一種可使用繼承Thread,重載run()方法來自定義線程,下面是匿名內部類,也是重載了run()方法:

Thread t1 = new Thread(){
     @Override
     public void run() {
       System.out.println("Hello, I am t1");
     }
};
t1.start();

start()後,線程Thread,有一個run()方法,start()方法會新建一個線程並讓這個線程執行run()方法。

t1.start()和t1.run()兩個方法的區別:start()會開啓新的線程,並調用run()執行線程;直接調用run()方法也能經過編譯,卻不能新建線程,而是在當前線程中調用run()方法(不要嘗試這樣開啓新線程,它只會在當前線程中,串行執行run()中的代碼)。

Thread t1 = new Thread();
t1.run();

第二種是使用Runnable接口來實現一樣的操做。這種方法解決了java單繼承實現多線程的缺點:

public class CreateThread implements Runnable {
   
    @Override
    public void run() {
        System.out.println("Hi!I am Runnable");
    }

    public static void main(String args[]) {
        Thread thread = new Thread(new CreateThread());
        thread.start();
    }
}

終止線程

通常,線程在執行完畢就會結束,無需手動關閉。特殊狀況,須要手動關閉。

使用stop關閉

可使用stop關閉,可是不推薦,爲何?緣由是stop()太過於暴力,強行把執行到一半的線程終止,可能會引發一些數據不一致的問題。舉個例子:

記錄1:ID=1,name=小明
記錄2:ID=2,name=小王

上面數據庫中要麼是存記錄1,要麼存記錄2,不然說明數據被損壞了,在單線程中不會出現這種狀況,單在多線程中則會出現這樣的狀況。

Thread.stop()方法在結束線程時,會直接終止線程,而且會當即釋放這個線程所持有的鎖。而此時,線程寫到一半便終止了。因爲鎖被釋放,等待該鎖的讀線程也能夠讀到這個不一致的數據。以下圖:

代碼模擬:

public class StopThreadUnsafe {
    public static User user = new User();
    public static class User {
        private int id;
        private String name;
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public User() {
            id = 0;
            name = "0";
        }
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", id=" + id +
                    '}';
        }
    }
    public static class ChangeObjectThread extends Thread {
        public void run() {
            while (true) {
                synchronized (user) {
                    int v = (int) (System.currentTimeMillis() / 1000);
                    user.setId(v);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    user.setName(v + "");
                }
                Thread.yield();
            }
        }
    }
    public static class ReadObjectThread extends Thread {
        public void run() {
            while (true) {
                synchronized (user) {
                    if (user.getId() != Integer.parseInt(user.getName())) {
                        System.out.println(user.toString());
                    }
                }
                Thread.yield();
            }
        }
    }
    public static void main(String args[]) throws InterruptedException {
        new ReadObjectThread().start();
        while (true) {
            Thread thread = new ChangeObjectThread();
            thread.start();
            Thread.sleep(150);
            thread.stop();
        }
    }
}

上面程序原本應該輸出id和name的值都相同,可是卻輸出以下的錯誤數據,這種錯誤沒有報錯,很難查找。

User{name='1565947644', id=1565947645}
User{name='1565947644', id=1565947645}

如何解決上面的問題?須要由咱們自行決定線程什麼時候退出。仍然用本例說明,只須要將ChangeObjectThread線程增長一個stopMe()便可:

public static class ChangeObjectThread extends Thread {
    volatile boolean stopme = false;

    public void stopMe(){
        stopme = true;
    }

    public void run() {
        while (true) {
            //手動中止線程
            if (stopme){
                System.out.println("exit by stop me");
                break;
            }
            synchronized (user) {
                int v = (int) (System.currentTimeMillis() / 1000);
                user.setId(v);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                user.setName(v + "");
            }
            Thread.yield();
        }
    }
}

線程中斷

線程中斷能夠和stop()同樣起到退出線程的做用,可是它不會當即退出,而是給線程發送一個通知,告知目標線程,有人但願你退出啦!至因而否退出由目標線程自行決定。

與線程中斷的三個方法:

public void interrupt()                     //中斷線程
public boolean Thread.isInterrupted()       //判斷是否被中斷
public static boolean Thread.interrupted()  //判斷是否被中斷,並清除當前中斷狀態

若是不手動加入中斷處理的邏輯,即便對線程中斷,這個中斷也不會起任何做用。

public class T1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                while(true){
                    if (Thread.currentThread().isInterrupted()){
                        System.out.println("Interruted!");
                        break;
                    }
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        System.out.println("Interrupted when Sleep");
                        Thread.currentThread().interrupt();     //1
                    }
                    Thread.yield();

                }
            }
        };

        t1.start();
        Thread.sleep(1000);
        t1.interrupt();
    }
}

若是去掉上例代碼run方法中的Thread.sleep(2000),那麼看起來和以前的stopme的方案很類似,可是中斷的功能更強大。若是在循環體中,相似於wait()或者sleep()這樣的操做,只能經過中斷來識別。

若在線程休眠期間發生中斷,它會拋出一個InterruptedException中斷異常,而且清除中斷標記。在上例代碼中,1處是在捕獲異常後(此時已清除了中斷標記)從新設置中斷標誌,使其在下一次循環進入if語句中斷循環。

等待(wait)和通知(notify)

public final void wait() throws InterruptedException
public final native void notify()

當一個對象實例上調用wait()方法後,當前線程就會在這個對象上等待。直到等到其餘線程調用了obj.notify爲止。顯然,這個對象成爲了多個線程之間的有效通訊手段。

wait()和notiry()如何工做?

若是一個線程調用了object.wait(),那麼它就會進入object對象的等待隊列。當object.notify()被調用時,它就會從這個等待隊列中,隨機選擇一個線程將其喚醒。這個選擇徹底是隨機的。而object.notifyAll()會將這個等待隊列中全部等待的線程喚醒,而不會隨機一個。

Object.wait()方法不是隨便調用的,必須包含在對應synchronzied語句中,不管是wait()或者notify()都須要首先得到目標對象的一個監視器。下圖展現了wait()和notify()的工做流程。

下面代碼簡單的使用了wait()和notify(),T1執行了object.wait()方法,這時立馬釋放對象鎖。此時正在等待對象鎖的T2捕獲到後,在2處執行object.notify()方法,可是此時和object.wait()方法不一樣,不立馬釋放,而是執行完synchronized塊的代碼後才釋放。T2釋放後,T1再次捕獲,執行T2接下來的程序。

public class SimpleWN {
   final static Object object = new Object();
   public static class T1 extends Thread{
        public void run()
        {
            synchronized (object) {
                System.out.println(System.currentTimeMillis()+":T1 start! ");
                try {
                   System.out.println(System.currentTimeMillis()+":T1 wait for object ");
                   object.wait();  //1 wait()後,立刻釋放對象鎖
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis()+":T1 end!");
            }
        }
   }
   public static class T2 extends Thread{
        public void run()
        {
            synchronized (object) {
                System.out.println(System.currentTimeMillis()+":T2 start! notify one thread");
                object.notify();    //2 notify()後,沒有立刻釋放對象鎖,而是執行完synchronized塊的代碼後釋放
                System.out.println(System.currentTimeMillis()+":T2 end!");
                try {
               Thread.sleep(3000);
            } catch (InterruptedException e) {
            }
                System.out.println(System.currentTimeMillis()+":T2 after sleep!");
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println(System.currentTimeMillis()+":T2 after synchronized!");
        }
   }
   public static void main(String[] args) {
        Thread t1 = new T1() ;
        Thread t2 = new T2() ;
//        Thread t1_1 = new T1() ;
//        t1_1.start();
        t1.start();
        t2.start();
   }
} /**
1566280793124:T1 start! 
1566280793125:T1 wait for object 
1566280793127:T2 start! notify one thread
1566280793127:T2 end!
1566280796127:T2 after sleep!
1566280796128:T1 end!
1566280797127:T2 after synchronized!
*/

Object.wait()Thread.sleep()的區別:Object.wait()和Thread.sleep()方法均可以讓線程等待若干時間。除了wait()能夠被喚醒外,另一個主要區別就是wait()方法會釋放對象的鎖,而Thread.sleep不會。

掛起(suspend)和繼續執行(resume)線程

被掛起的線程,必需要等待resume()後,才能繼續執行。

這對方法已經不推薦使用了。不推薦的緣由是由於suspend()在致使線程暫停的同時,並不會釋聽任何鎖資源,直到等到resume()才釋放。若是resume()操做意外出如今suspend()前面,就可能致使永久掛起。。此時,任何其餘線程想要訪問被它佔用的鎖時,都會受到牽連。甚至整個系統運行不正常。

public class BadSuspend {
   public static Object u = new Object();
   static ChangeObjectThread t1 = new ChangeObjectThread("t1");
   static ChangeObjectThread t2 = new ChangeObjectThread("t2");

   public static class ChangeObjectThread extends Thread {
      public ChangeObjectThread(String name){
         super.setName(name);
      }
      @Override
      public void run() {
         synchronized (u) {
            System.out.println("in "+getName());
            Thread.currentThread().suspend();   //1
         }
      }
   }

   public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        t1.resume();
        System.out.println("t1 resume!");
        t2.resume();
        System.out.println("t2 resume!");
        t1.join();
        t2.join();
    }
}/**
in t1
t1 resume!
t2 resume!
in t2
*/

上面程序中,t1.start()後線程走到1處被掛起,此時不釋放對象鎖。t2.start()後,t2須要等待t1釋放的對象鎖。在t2等待鎖的過程當中,t2.resume()已經發生了(經過打印結果能夠看出),隨後才發生t2的Thread.suspend()。這時,t2被永久掛起。

那若是須要一個比較可靠的suspend()函數,該怎麼作呢?能夠利用wait()和notify(),在應用層面實現suspend()和resume():

public class GoodSuspend {
    public static Object u = new Object();
    public static class ChangeObjectThread extends Thread {
        // 標記變量,表示當前線程是否被掛起
        volatile boolean suspendme = false;     //1
        // 掛起線程
        public void suspendMe(){
            suspendme = true;
        }
        // 繼續執行線程
        public void resumeMe(){
            suspendme = false;
            synchronized (this){
                notify();
            }
        }
        @Override
        public void run(){
            while (true){
                synchronized (this){    //2
                    while (suspendme){
                        try {
                            wait();
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                }
                synchronized (u){
                    System.out.println("in ChangeObjectThread");
                }
                Thread.yield();
            }
        }
    }
    public static class ReadObjectThread extends Thread{
        @Override
        public void run(){
            while (true){
                synchronized (u){
                    System.out.println("in ReadObjectThread");
                }
                Thread.yield();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ChangeObjectThread t1 = new ChangeObjectThread();
        ReadObjectThread t2 = new ReadObjectThread();
        t1.start();
        t2.start();
        Thread.sleep(1000);

        t1.suspendMe();
        System.out.println("suspend t1 2 sec");
        Thread.sleep(2000);

        System.out.println("resume t1");
        t1.resumeMe();
    }
}

在1處,給出了一個標記suspendme,表示當前線程是否被掛起,同時,增長了suspendMe()(經過執行wait()方法實現掛起)和resumeMe()(經過執行notify()通知繼續執行,並清除掛起標記),注意,2處給本身加鎖。

等待線程結束(join)和謙讓(yield)

join

在多線程中,一個線程的輸入可能很是依賴於另一個或者多個線程的輸出,此時,這個線程就須要等待依賴線程執行完畢,才能繼續執行,jdk提供了join()來實現這個功能。有2個join方法

public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException

第一個join()表示無限等待,它會阻塞當前線程,直到目標線程執行完畢。第二個方法給出了一個最大等待時間,若是超過給定時間目標線程還在執行,當前線程就會無論繼續往下執行。

join()的本質是讓調用線程wait()在當前線程對象實例上。下面是JDK中join()實現的核心代碼片斷:

while(isAlive()){
  wait(0);
}

它讓調用線程在當前線程對象上等待。當線程執行完成後,會在推出前調用notifyAll()通知全部等待線程繼續執行。

Thread.yield()

public static native void yield();

這是一個靜態方法,它會讓當前線程讓出CPU。在讓出CPU後,還會進行CPU資源的爭奪,至因而否能再次分配,就不必定了。它的調用好像是在說:我已經完成了一些重要的工做了,我能夠休息一下,給其餘線程一些工做機會。

volatile與java內存模型(JMM)

以前提到過:java內存模型圍繞原子性、有序性和可見性展開。

爲了在適當的場合,確保線程間的有序性、可見性和原子性。java使用了一些特殊的操做或者關鍵字來申明、告訴虛擬機,這個地方要特別注意,不能隨意變更優化目標指令。關鍵字volatile就是其中之一。

使用volatile去申明一個變量,能夠保證這個變量的可見性的特色。在以前的例子MultiThreadLong中,long型的 t 改成volatile,使其保證了原子性

public class MultiThreadLong {
   public volatile static long t=0;
   public static class ChangeT implements Runnable{
   ......

volatile對於保證操做的原子性有很是大的幫助,可是volatile並不能替代鎖,它沒法保證一些複合操做的原子性。以下例中,沒法保證i++的原子操做

public class PlusTask implements Runnable {
    public volatile static Integer j = 0;   //1
    public void add(){
        for (int i = 0; i < 10000; i++) {
            j++;
        }
    }
    
    @Override
    public void run() {
//        synchronized (PlusTask.class) {
            add();
//        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        PlusTask task = new PlusTask();
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new PlusTask());
            threads[i].start();
        }
        for (int i = 0; i < 10; i++) {
            threads[i].join();
        }
        System.out.println(j);
    }
}

上面的代碼中,最終的值應該是100000,但實際老是會小於指望值。

volatile除了能夠保證原子性,也能保證數據的可見性有序性。下面看一個例子:

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready);
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReaderThread().start();
        Thread.sleep(1000);
        number = 42;
        ready = true;
        Thread.sleep(10000);
    }
}

因爲系統優化的結果,ReaderThread線程可能沒法「看到」主線程的修改,致使ReaderThread永遠沒法退出,這是一個典型的可見性問題。可是,只要簡單使用volatile來申明ready變量,告訴java虛擬機,這個變量可能會在不一樣的線程中修改。就能夠解決問題了。

分門別類的管理:線程組

在一個系統中,若是線程數量不少,並且功能分配明確,就能夠將相同功能的線程放置在一個線程組中,方便管理。

線程組的使用很簡單:

public class ThreadGroupName implements Runnable {
 
   public static void main(String[] args) {
      ThreadGroup tg = new ThreadGroup("PrintGroup");   //1 創建名爲「PrintGroup」的線程組
      Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");
      Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
      t1.start();
      t2.start();
      System.out.println(tg.activeCount()); //2
      tg.list();    //3
   }
 
   @Override
   public void run() {
      String groupAndName = Thread.currentThread().getThreadGroup().getName() + "-"
            + Thread.currentThread().getName();
      while (true) {
         System.out.println("I am " + groupAndName);
         try {
            Thread.sleep(3000);
         } catch (Exception e) {
            e.printStackTrace();
         }
      }
   }
 
}

代碼1處,創建一個「PrintGroup」的線程組,並將T1和T2兩個線程加入這個組中。二、3處,activeCount()能夠得到活動線程的數量,list()打印線程組中全部線程信息。

線程組有一個stop(),它會中止線程組中全部的線程,可是和Thread.stop()會有相同的問題,要格外謹慎使用。

駐守後臺:守護線程(Daemon)

守護線程是一種特殊的線程,它是系統的守護者,在後臺默默完成一些系統性的服務,好比垃圾回收線程、JIT線程就能夠理解爲守護線程。與之相應的是用戶線程,它完成業務操做。若是用戶線程所有結束,守護線程的對象不存在了,那麼整個應用程序就應該天然結束。所以,在java引用內,只有守護線程時,java虛擬機會天然退出。

下面是一個簡單的守護線程:

public class DaemonDemo {
    public static class DaemonT extends Thread{
        public void run(){
            while(true){
                System.out.println("I am alive");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t=new DaemonT();
        t.setDaemon(true);  //1
        t.start();
      
        Thread.sleep(2000); //當主線程執行完畢後,守護線程t也隨之結束。
    }
}

1處將t設置成守護線程,當主線程執行完畢後,守護線程也隨之結束。若不把t設置成守護線程,那麼程序永遠不會結束。

先乾重要的事:線程優先級

java中線程能夠有本身的優先級。優先級高的線程在競爭資源時會更有優點,更可能搶佔資源,固然,這是一個機率問題。這種優先級產生的後果不容易預測,優先級低的線程可能會致使飢餓現象(即便是優先級低,可是也不能餓死它啊)。所以,在要求嚴格的場合,仍是須要本身在應用層解決線程調度問題。

在java中,使用1到10表示線程優先級。通常可使用內置的三個靜態變量標量表示:

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

數字越大則優先級越高,但有效範圍在1到10之間。例子:

public class PriorityDemo {
    public static class HightPriority extends Thread{
        static int count=0;
        public void run(){
            while(true){
                synchronized(PriorityDemo.class){
                    count++;
                    if(count>10000000){
                        System.out.println("HightPriority is complete");
                        break;
                    }
                }
            }
        }
    }
    public static class LowPriority extends Thread{
        static int count=0;
        public void run(){
            while(true){
                synchronized(PriorityDemo.class){
                    count++;
                    if(count>10000000){
                        System.out.println("LowPriority is complete");
                        break;
                    }
                }
            }
        }
    }
    
    /**
     * HightPriority先完成的次數多,可是 不保證
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        Thread high=new HightPriority();
        LowPriority low=new LowPriority();
        high.setPriority(Thread.MAX_PRIORITY);  //1
        low.setPriority(Thread.MIN_PRIORITY);   //2
        low.start();
        high.start();
    }
}

上述代碼中一、2處設置了線程的優先級,因此老是高優先級的線程執行得會快些。

線程安全的概念與synchronized

併發程序開發的一大關注重點就是線程安全。程序並行化是爲了得到更高的執行效率,同時保證程序的正確性。所以,線程安全是並行程序的根本和根基。

public class AccountingVol implements Runnable {
    static AccountingVol accountingVol = new AccountingVol();
    static volatile int i = 0;

    public static void increase() {
        i++;
    }
    @Override
    public void run() {
        for (int j = 0; j < 10000000; j++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(accountingVol);
        Thread t2 = new Thread(accountingVol);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

上述代碼中,線程t一、t2可能同時讀取i爲0,並各自計算獲得i=1,並前後寫入這個結果,所以,雖然i++被執行了2次,但實際i的值只增長了1。

要解決這個問題,咱們就要保證多個線程對i進行操做時徹底同步。就是說,當線程A在寫入時,B不只不能寫,也不能讀。java中,提供了一個重要的關鍵字synchronized來實現這個功能。

synchronized

做用是實現線程間的同步。它的工做是對同步的代碼加鎖,使得每次只能有一個線程進入同步塊。

用法:

  • 指定加鎖對象:對給定對象加鎖,進入同步代碼前要得到給定對象的鎖。
  • 直接做用於實例方法:至關於對當前實例加鎖,進入同步代碼前要得到當前實例的鎖。
  • 直接做用於靜態方法:至關於對當前類加鎖,進入同步代碼前要得到當前類的鎖。

指定加鎖對象

下面程序中,將synchronized做用於給定對象instance。每次進入被synchronized包裹的代碼段,都會請求instance的鎖。如有其餘線程佔用,則必須等待。

public class AccountingSync implements Runnable{
   static AccountingSync instance=new AccountingSync();
   static int i=0;
   @Override
   public void run() {
      for(int j=0;j<10000000;j++){
         synchronized(instance){
            i++;
         }
      }
   }
   //main程序見上例代碼
}

直接做用於實例方法

public class AccountingSync2 implements Runnable{
   static AccountingSync2 instance=new AccountingSync2();
   static int i=0;
   public synchronized void increase(){
      i++;
   }
   @Override
   public void run() {
      for(int j=0;j<10000000;j++){
         increase();
      }
   }
   public static void main(String[] args) throws InterruptedException {
      AccountingSync2 i1=new AccountingSync2();
//    AccountingSync2 i2=new AccountingSync2();
      Thread t1=new Thread(i1);
      Thread t2=new Thread(i1);
      t1.start();t2.start();
      t1.join();t2.join();
      System.out.println(i);
   }
}

上例代碼中,synchronized關鍵字做用於一個實例方法,這就是說在進入increase()方法前,線程必須得到當前對象實例的鎖。在本例中就是instance對象。在此例中,線程t1和t2須要用到相同的Ruanable實例i1,這樣才能關注到同一個對象鎖上。若兩個線程使用不一樣的兩個Runnable實例t1,t2,即兩個線程使用了兩把不一樣的鎖。

可是,咱們能夠把increase()方法改爲static的,這樣方法塊請求的是當前類的鎖,而不是當前實例的,所以,線程能夠同步。以下:

public class AccountingSync2 implements Runnable{
   static AccountingSync2 instance=new AccountingSync2();
   static int i=0;
   public static synchronized void increase(){  //3
      i++;
   }
   @Override
   public void run() {
      for(int j=0;j<10000000;j++){
         increase();
      }
   }
   public static void main(String[] args) throws InterruptedException {
      AccountingSync2 i1=new AccountingSync2();
      AccountingSync2 i2=new AccountingSync2();
      Thread t1=new Thread(i1);     //1
      Thread t2=new Thread(i2);     //2
      t1.start();t2.start();
      t1.join();t2.join();
      System.out.println(i);
   }
}

1和2處使用了兩個不一樣的Runable實例,可是3處的同步方法爲static的,此方法須要的是當前類的鎖而非當前實例的鎖,所以線程間能夠正確同步。

除了用於線程同步、確保線程安全外,synchronized還能夠保證線程間的可見性和有序性。被synchronized限制的多個線程死串行執行的。

程序中的幽靈:隱蔽的錯誤

有異常的異常堆棧好修復,可是,沒有異常、沒有日誌、沒有堆棧的異常,就很讓人抓狂了。

無提示的錯誤案例

若是你運行下面的程序,會發現一個隱藏的錯誤:

int v1 = 1073741827;
int v2 = 1431655768;
int ave = (v1+v2)/2;

把上面ave打印出來,會發現ave的值是-894784850,一個負數。那是由於溢出。這種隱形的錯誤很難找,和幽靈通常。

併發下的ArrayList

ArrayList是一個線程不安全的容器。若是在多線程中使用ArrayList,可能會致使程序出錯,那會出現哪些問題呢?

public class ArrayListMultiThread {
    static ArrayList<Integer> al = new ArrayList<Integer>(10);
    public static class AddThread implements Runnable{
        @Override
        public void run() {
            for (int i=0;i<10000000;i++){
                al.add(i);
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new AddThread());
        Thread t2 = new Thread(new AddThread());
        t1.start();t2.start();
        t1.join(); t2.join();
        System.out.println(al.size()); //拋出異常 返回小於2000的數值。
    }
}

在上面這段錯誤的代碼中,本來輸出的應該是20000000,可是因爲ArrayList不支持,咱們可能會獲得3中不一樣的結果:

  • 第一,程序正常結束,概率極小

  • 第二,程序拋出異常:

    Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 1823230
      at java.util.ArrayList.add(ArrayList.java:459)
      at geym.ch2.ArrayListMultiThread$AddThread.run(ArrayListMultiThread.java:11)
      at java.lang.Thread.run(Thread.java:745)

    這是由於ArrayList在擴容過程當中,內部一致性被破壞,但因爲沒有鎖的保護,另外一個線程訪問到了不一致的內部狀態,致使了越界問題。

  • 第三,出現一個很是隱蔽的問題,打印的值小於指望值20000000。

!!改進的方法很簡單,使用線程安全的Vector代替ArrayList便可。

併發下詭異的HashMap

HashMap一樣不是線程安全的。

public class HashMapMultiThread {
    static Map<String, String> map = new HashMap<String, String>();

    public static class AddThread implements Runnable {
        int start = 0;

        public AddThread(int start) {
            this.start = start;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100000; i += 2) {
                map.put(Integer.toString(i), Integer.toBinaryString(i));
            }
        }

        public static void main(String[] args) throws InterruptedException {

            // 根據你的電腦CPU核數來配置 兩核啓兩個線程就行
            Thread t1 = new Thread(new HashMapMultiThread.AddThread(0));
            Thread t2 = new Thread(new HashMapMultiThread.AddThread(1));
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(map.size());
        }
    }
}

咱們指望獲得100000,可是,實際可能會有三種狀況:

  • 第一,程序正常結束,大小爲預期值。
  • 第二,程序正常結束,可是小於100000。
  • 第三,程序永遠沒法結束

前面兩種狀況和ArrayList相似,對於第三種狀況,因爲多線程的衝突,HashMap中的Entry<K,V>鏈表的結構遭到破壞,鏈表成環了!當鏈表成環時,HashMap.put()方法中的迭代就等於死循環。如圖,展現了最簡單的環狀結構,key1和key2互爲對方的next元素。

初學者常見問題:錯誤的加鎖

public class BadLockOnInteger implements Runnable {
    public static Integer i = 0;
    public static BadLockOnInteger instance = new BadLockOnInteger();
    @Override
    public void run() {
        for (int j = 0; j < 10000000; j++) {
            synchronized (i){   //1
                i++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

上面代碼1處,把鎖加在了i上,彷佛並無什麼問題,然而,咱們運行程序,卻獲得了比預期值20000000要小的數,這是爲何呢?由於Integer屬於不可變對象。就是說Integer的值不能被修改,若是要修改,就要新建一個Integer對象。這樣在多個線程間,並不必定可以看到同一個i對象(i一直在變),每次加鎖都加在了不一樣的對象實例上,從而致使對臨界區代碼控制出現問題。

修正這個問題,只要把鎖加在instance上就能夠了:

synchronized (i){

改成下面代碼便可:

synchronized (instance){
相關文章
相關標籤/搜索