多線程的神祕面紗...還在恐懼嗎???

前言

最開始學習java時,頭都大了,也沒學好多線程,甚至都不明白啥是多線程...慢慢的不斷學習,發現多線程其實並沒有我們想象中的困難。


進程(Processes)與線程(Threads)

在操作系統裏面,存在着非常多的進程與線程。在每個單核處理器中,某個時刻僅有一個線程在執行。但是爲什麼我們在平時使用中,卻感覺是有多個線程在運行呢?因爲處理器使用了時間分片技術。也就是將處理器的執行時間分割成很多片段,然後將每個時間片段分配給不一樣的線程,由於處理器的運行速度非常快,所以就給我們造成一種假象,處理器在同時執行多個線程。隨着科技的發展,現在出現了多核處理器電腦,並且每個核心可超線程執行任務,由於每個核都是獨立的,大大提高了電腦的併發執行能力。


進程(Processes)

進程通常擁有自己的執行環境,特別的,還擁有私有的內存空間。在某種程度上看,進程可以看做是一個程序或者是應用,比如我們使用win7/win8的任務管理器,可查看到系統的進程列表。爲了讓多個進程之間能夠正常通訊,現代的操作系統一般使用Inter Process Communication(IPC)技術,也稱之爲進程間通訊。通暢使用管道(Pipes)、Socket等。在java裏面,一般一個jvm代表了一個進程。


線程(Threads)

線程有時候也叫做輕量級的進程。在一個進程中,包含了一個或者多個線程,這些線程共享進程的執行環境,資源,內存。所以創建線程以及銷燬線程的代價卻比創建進程小的多。


創建線程

方式1:實現Runnable接口

public class HelloRunnable implements Runnable {
    public void run() {
        System.out.println("Hello from a thread!");
    }
    public static void main(String args[]) {
        (new Thread(new HelloRunnable())).start();
    }
}


方式二:繼承Thread類

public class HelloThread extends Thread {
    public void run() {
        System.out.println("Hello from a thread!");
    }
    public static void main(String args[]) {
        (new HelloThread()).start();
    }
}

注意點:啓動線程使用的是start方法,不是調用run()方法。
在這兩種方式中,我們應該使用哪種呢?
推薦方式一,因爲這樣子代碼看上去更加簡潔,並且靈活性更高。比如一個類本身已經繼承了別的Thread,那麼就無法實現多次繼承Thread。


暫停線程的執行(Pausing Execution)

爲什麼需要暫停線程的執行呢?大家可以回想下,在使用ServerSocket時,有個accept方法,這個方法是阻塞的,也就是會在等待客戶端的socket連接。所以線程的暫停有着非常多的用處,比如等待別的任務執行,等待IO完成,等待網絡等。那麼如何停止一個線程呢?直接使用Thread.sleep(int time)
public class SleepMessages {
    public static void main(String args[])
        throws InterruptedException {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };


        for (int i = 0;
             i < importantInfo.length;
             i++) {
            //Pause for 4 seconds
            Thread.sleep(4000);
            //Print a message
            System.out.println(importantInfo[i]);
        }
    }
}
注意點,Thread.sleep()方法會拋出一個InterruptedException,這個異常的拋出,是由於中斷了這個線程,那麼這個線程就不能繼續存活下去了。


線程的中斷

在一個線程中,可以執行各種操作,假設某個操作非常耗時,或者進入了長時間的睡眠,這時候想要中斷這個線程,那麼可以使用Thread.interrupted()來中斷線程。被中斷的線程會直接被殺死。
for (int i = 0; i < inputs.length; i++) {
    heavyCrunch(inputs[i]);
    if (Thread.interrupted()) {
        throw new InterruptedException();
    }
}


線程的Join

在多個線程中,如果一個線程調用了join(),那麼主線程就會被停止,直到調用了join()的那個線程執行完畢。
public class ThreadDemo{
	public static void main(String[] args){
	    Thread mainThread = Thread.currentThread();
		System.out.println(mainThread.getName() + " start");
		Thread joinThread = new Thread(new Runnable(){
		    @Override
			public void run(){
			    System.out.println("I am joinThread");
				try{
		            Thread.sleep(5000);
		        }catch(InterruptedException e){
		            e.printStackTrace();
		        }  
			}
		});    
		joinThread.start();
		try{
		    joinThread.join();
		}catch(InterruptedException e){
		    e.printStackTrace();
		}
		//this partition code has to wait the joinThread to complete
	    System.out.println("end");
	}
}
在這個例子中,最後主線程部分的System.out.println("end");部分的代碼,需要一直等待joinThread的執行完成,這裏爲了簡單,只有一句輸出,其實可以爲任何的代碼。


下面這個簡單的例子,有兩個線程,一個是MessageLoop線程,不斷的輸出數據,另外一個是Main Thread。Main Thread由jvm直接創建,所以每個程序至少有一個線程,那就是主線程。MessageLoop t啓動之後,就不斷的輸出數據,然後主線程就不斷的詢問該線程t是否存活,如果存活就讓t暫停1秒鐘,這裏使用了t.join(1000),然後判斷程序的運行時間是否已經超過了patience標量,如果是,那就調用t.interrupt()方法來中斷線程,最後主線程輸出了Finally,表示結束。
public class SimpleThreads {


    // Display a message, preceded by
    // the name of the current thread
    static void threadMessage(String message) {
        String threadName =
            Thread.currentThread().getName();
        System.out.format("%s: %s%n",
                          threadName,
                          message);
    }


    private static class MessageLoop
        implements Runnable {
        public void run() {
            String importantInfo[] = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
            };
            try {
                for (int i = 0;
                     i < importantInfo.length;
                     i++) {
                    // Pause for 4 seconds
                    Thread.sleep(4000);
                    // Print a message
                    threadMessage(importantInfo[i]);
                }
            } catch (InterruptedException e) {
                threadMessage("I wasn't done!");
            }
        }
    }


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


        // Delay, in milliseconds before
        // we interrupt MessageLoop
        // thread (default one hour).
        long patience = 1000 * 60 * 60;


        // If command line argument
        // present, gives patience
        // in seconds.
        if (args.length > 0) {
            try {
                patience = Long.parseLong(args[0]) * 1000;
            } catch (NumberFormatException e) {
                System.err.println("Argument must be an integer.");
                System.exit(1);
            }
        }


        threadMessage("Starting MessageLoop thread");
        long startTime = System.currentTimeMillis();
        Thread t = new Thread(new MessageLoop());
        t.start();


        threadMessage("Waiting for MessageLoop thread to finish");
        // loop until MessageLoop
        // thread exits
        while (t.isAlive()) {
            threadMessage("Still waiting...");
            // Wait maximum of 1 second
            // for MessageLoop thread
            // to finish.
            t.join(1000);
            if (((System.currentTimeMillis() - startTime) > patience)
                  && t.isAlive()) {
                threadMessage("Tired of waiting!");
                t.interrupt();
                // Shouldn't be long now
                // -- wait indefinitely
                t.join();
            }
        }
        threadMessage("Finally!");
    }
}

線程的同步

在線程中,由於資源、運行環境、內存等共享,所以會帶來許多的問題,比如著名的死鎖。下面的例子是一個簡單的計數器
class Counter {
    private int c = 0;


    public void increment() {
        c++;
    }


    public void decrement() {
        c--;
    }


    public int value() {
        return c;
    }


}


如果這個類的對象只是被一個線程訪問,那麼每次increment()、decrement(),c就會+1或者-1。但是如果這個對象同時被多個線程持有,並且併發的訪問,那麼程序的預期,將和我們所想象的不一致。上面的程序,使用的是c++,c--語法,但是並不代表c++,c--的執行流程只有一個指令。實際上,c++分爲下面三個步驟。
  1、獲得當前c的值
  2、當前的值+1
  3、將第2步的值保存起來


下面我們假設Thread A,B同時訪問increment(),那麼很可能發生如下的過程:

  1、Thread A獲得c的值,c=0
  2、Thread B獲得c的值,c=0,
  3、Thread A將值+1,c=1
  4、Thread B將值-1,c=-1
  5、Thread A將值保存起來,c=1
  6、Thread B將值保存起來,c=-1
假設c執行之前爲0,那麼之後的結果會是多少呢?沒錯,是-1。這和我們預期的結果0並不一致。
那麼是什麼造成了這個問題呢?大家可以參考下其他文檔,總體來說就是,多個線程對同一個變量的修改,對其他線程來說,可能是不可見的。比如Thread A對c的修改,Thread B是不可見的。


同步方法(Synchronized Methods)
在java中,提供了對同步方法的原語支持,關鍵字就是synchronized。如下例子:
public class SynchronizedCounter {
    private int c = 0;
    public synchronized void increment() {
        c++;
    }
    public synchronized void decrement() {
        c--;
    }
    public synchronized int value() {
        return c;
    }
}


在方法中加入了synchronized,那麼系統就會保證如下的兩個特性:
1、當一個線程訪問一個synchronized方法時,其他方法只能等待。比如在上面例子中,Thread A假設在執行increment(),那麼Thread B就必須等待Thread A的執行完畢。
2、一個線程執行完畢之後的結果,對於其他線程來說,結果是可見的。比如Thread A執行increment()之後,c的值爲1,那麼這時候Thread B獲得的值也就是1。


內部鎖(Intrinsic Locks)

鎖,就是用來保護某一段代碼只能被一個Thread訪問。其他程序如果想要訪問這段代碼,就需要獲得鎖。使用synchronized methods,虛擬機會爲整個方法都加入鎖。在方法中加入synchronized,雖然實現了線程之間的同步,但是卻降低了多線程的併發能力,如果一個方法中,僅僅只有一行代碼需要同步,那麼使用同步方法將會鎖住整個方法,導致其他線程無法訪問。所以在java中,支持同步代碼塊。語法也很簡單,如下所示:
public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}


原子訪問(Atomic Access)

在編程中,一個原子的動作,是不可分割的。類似於數據庫中的原子操作,所有操作要麼執行成功,要麼執行失敗,不存在半執行。在java中,大部分的原始數據類型的操作都是原子的,或者是加入了volatile關鍵字的域。一般而言,使用原子技術會讓代碼更加難以控制,理解起來也更加困難,所以應該避免大範圍的使用。


死鎖(Deadlock)

死鎖就是兩個線程互相等待對方的完成。下面的例子就說明了,兩個friend都在等待對方bowBack(),所以程序會一直無限期的進入等待。
public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s"
                + "  has bowed to me!%n", 
                this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                + " has bowed back to me!%n",
                this.name, bower.getName());
        }
    }


    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}




下面我們通過代碼來演示一個生產者-消費者關係。這裏一共有三個線程,一個是生產者,另外一個是消費者,另外一個是主線程。生產者負責生產,然後每次生產完之後,都需要notify消費者來取數據,消費者負責消費數據,每次消費完之後,都notify生產者開始生產數據。需要注意的一點是,生產者和消費者都是訪問同一個對象,在例子中就是
Drop對象。
//可以假設爲生產線
public class Drop {
    // Message sent from producer
    // to consumer.
    private String message;
    // True if consumer should wait
    // for producer to send message,
    // false if producer should wait for
    // consumer to retrieve message.
    private boolean empty = true;


    public synchronized String take() {
        // Wait until message is
        // available.
        while (empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = true;
        // Notify producer that
        // status has changed.
        notifyAll();
        return message;
    }


    public synchronized void put(String message) {
        // Wait until message has
        // been retrieved.
        while (!empty) {
            try { 
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = false;
        // Store message.
        this.message = message;
        // Notify consumer that status
        // has changed.
        notifyAll();
    }
}


//生產者
import java.util.Random;


public class Producer implements Runnable {
    private Drop drop;


    public Producer(Drop drop) {
        this.drop = drop;
    }


    public void run() {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };
        Random random = new Random();


        for (int i = 0;
             i < importantInfo.length;
             i++) {
            drop.put(importantInfo[i]);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
        drop.put("DONE");
    }
}


//消費者
import java.util.Random;


public class Consumer implements Runnable {
    private Drop drop;


    public Consumer(Drop drop) {
        this.drop = drop;
    }


    public void run() {
        Random random = new Random();
        for (String message = drop.take();
             ! message.equals("DONE");
             message = drop.take()) {
            System.out.format("MESSAGE RECEIVED: %s%n", message);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
    }
}


//主線程
public class ProducerConsumerExample {
    public static void main(String[] args) {
        Drop drop = new Drop();
        (new Thread(new Producer(drop))).start();
        (new Thread(new Consumer(drop))).start();
    }
}




不可變對象

有時候,爲了防止一些多線程問題的出現,可以使用不可變對象來代替可變對象。下面我們來看一個例子,一個顏色類。
public class SynchronizedRGB {


    // Values must be between 0 and 255.
    private int red;
    private int green;
    private int blue;
    private String name;


    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }


    public SynchronizedRGB(int red,
                           int green,
                           int blue,
                           String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }


    public void set(int red,
                    int green,
                    int blue,
                    String name) {
        check(red, green, blue);
        synchronized (this) {
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }
    }


    public synchronized int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }


    public synchronized String getName() {
        return name;
    }


    public synchronized void invert() {
        red = 255 - red;
        green = 255 - green;
        blue = 255 - blue;
        name = "Inverse of " + name;
    }
}




這個類的getRGB()、getName()、invert()都是同步方法,但是還是可能會出現一些問題。比如下面的代碼
SynchronizedRGB color =
    new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB();      //Statement 1
String myColorName = color.getName(); //Statement 2


在Stament1之後,如果一個線程在調用了getRGB()之後,另外一個方法調用set()方法,那麼getName()和getRGB()得到的就不是同一個RGB顏色值了。那麼我們如何防止這種情況的發生呢?沒錯,使用不可變對象的技術,去掉set()方法。
final public class ImmutableRGB {


    // Values must be between 0 and 255.
    final private int red;
    final private int green;
    final private int blue;
    final private String name;


    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }


    public ImmutableRGB(int red,
                        int green,
                        int blue,
                        String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }




    public int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }


    public String getName() {
        return name;
    }


    public ImmutableRGB invert() {
        return new ImmutableRGB(255 - red,
                       255 - green,
                       255 - blue,
                       "Inverse of " + name);
    }
}



不可變對象的基本準則:
  1、不提供setter方法修改fields
  2、將所有fields變成private final
  3、不允許子類override方法,可以使用final關鍵字修飾方法,或者使用private修飾
  4、如果一個對象的field是一個引用對象,這個引用對象是可變的,那麼就不允許修改該field,可以使用final修飾,並且不要將這個引用共享給其他對象,可以使用copy技術,將該引用對象的值賦值一份,再丟給其他對象。


使用java.util.concurrecy.*包來解決併發問題
在這個包中,有個很重要的抽象類,那就是Lock。這個方法的語義更加清晰,需要注意的一點就是,每次使用lock()之後,都需要在finally中,unlock()鎖。下面使用Lock來改寫前面死鎖的例子:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;


public class Safelock {
    static class Friend {
        private final String name;
        private final Lock lock = new ReentrantLock();


        public Friend(String name) {
            this.name = name;
        }


        public String getName() {
            return this.name;
        }


        public boolean impendingBow(Friend bower) {
            Boolean myLock = false;
            Boolean yourLock = false;
            try {
                myLock = lock.tryLock();
                yourLock = bower.lock.tryLock();
            } finally {
                if (! (myLock && yourLock)) {
                    if (myLock) {
                        lock.unlock();
                    }
                    if (yourLock) {
                        bower.lock.unlock();
                    }
                }
            }
            return myLock && yourLock;
        }
            
        public void bow(Friend bower) {
            if (impendingBow(bower)) {
                try {
                    System.out.format("%s: %s has"
                        + " bowed to me!%n", 
                        this.name, bower.getName());
                    bower.bowBack(this);
                } finally {
                    lock.unlock();
                    bower.lock.unlock();
                }
            } else {
                System.out.format("%s: %s started"
                    + " to bow to me, but saw that"
                    + " I was already bowing to"
                    + " him.%n",
                    this.name, bower.getName());
            }
        }


        public void bowBack(Friend bower) {
            System.out.format("%s: %s has" +
                " bowed back to me!%n",
                this.name, bower.getName());
        }
    }


    static class BowLoop implements Runnable {
        private Friend bower;
        private Friend bowee;


        public BowLoop(Friend bower, Friend bowee) {
            this.bower = bower;
            this.bowee = bowee;
        }
    
        public void run() {
            Random random = new Random();
            for (;;) {
                try {
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {}
                bowee.bow(bower);
            }
        }
    }
            


    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new BowLoop(alphonse, gaston)).start();
        new Thread(new BowLoop(gaston, alphonse)).start();
    }
}




在java.util.concurrent.*中,還提供了線程執行器,這個接口就是Executor。在之前的代碼中,我們使用(new Thread(r)).start()來啓動線程,有了執行器之後,可以使用e.execute(r)來啓動一個線程。在Executor中,有兩個比較重要的子類,ExecutorService和ScheduledExecutorService,具體的使用方式也比較簡單,大家可以直接查看java doc便可。


線程池(Thread Pools)

如果你熟悉jdbc的操作,那麼肯定知道數據庫連接池。有關於池的概念,都很類似,就是把一些需要的對象,事先準備好,然後將這些對象放入內存中,以後每次需要的時候,就直接從內存中取出來,這樣子可以大大節省new一個對象的開銷。在java中,提供了幾個不一樣的線程池供我們選擇。
  1、java.util.concurrent.Executors.newCachedThreadPool()提供一個緩存池
  2、java.util.concurrent.Executors.newSingleThreadExecutor()提供一個單線程池
  3、java.util.concurrent.Executors.newFixedThreadPool()提供一個固定大小的線程池


支持多線程的容器(Concurrent Collections)

在java.util.Collection中,提供了很多容器類,但是這些類大部分都不是線程安全的,所以如果程序需要使用線程安全的容器類,那麼可以使用java.util.concurrent包中的容器類,這些類最大的區別就是線程安全的,所以使用上對我們來說沒什麼新的難點,直接查看文檔api便可。


原子變量(Atomic Variables)

在之前的例子中,說到了原子訪問,比如下面的計數器:
class Counter {
    private int c = 0;


    public void increment() {
        c++;
    }


    public void decrement() {
        c--;
    }


    public int value() {
        return c;
    }


}




一種方式可以讓程序變得是線程安全的,一個是使用線程方法,如下代碼:
class SynchronizedCounter {
    private int c = 0;
    public synchronized void increment() {
        c++;
    }
    public synchronized void decrement() {
        c--;
    }
    public synchronized int value() {
        return c;
    }
}




但是這種方式的效率不高,可以使用java.util.concurrent.atomic的原子類,如下代碼:
import java.util.concurrent.atomic.AtomicInteger;


class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);
    public void increment() {
        c.incrementAndGet();
    }
    public void decrement() {
        c.decrementAndGet();
    }
    public int value() {
        return c.get();
    }
}




總結:多線程的技術,一直是我們既嚮往又害怕的部分。嚮往是因爲多線程的威力,害怕的是多線程的複雜性。在學習初,什麼進程、線程、同步、原子、死鎖等各種專業詞彙搞得我們信心全無。只有不斷的學習,總結,其實我們也可以掌握多線程。最後感謝大家的查詢...繼續苦逼寫代碼去了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章