前言
最開始學習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=02、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();
}
}
總結:多線程的技術,一直是我們既嚮往又害怕的部分。嚮往是因爲多線程的威力,害怕的是多線程的複雜性。在學習初,什麼進程、線程、同步、原子、死鎖等各種專業詞彙搞得我們信心全無。只有不斷的學習,總結,其實我們也可以掌握多線程。最後感謝大家的查詢...繼續苦逼寫代碼去了。