線程
線程的基本概念
線程是一個程式裡面不同的執行路徑。
- 程序與線程的差別
- 每個程序都有獨立的代碼和資料空間(程序上下文),程序間的切換開銷大。
- 線程可以看作輕量級的程序,同一類線程共享代碼和資料空間,每個線程有獨立的運作棧和程式計數器(PC),線程的切換開銷小。
- 多程序:在作業系統中能同時運作多個程式。
- 多線程:在同一應用程式中有多個順序流同時執行。
線程的建立與啟動
- Java 的線程是通過 java.lang.Thread 類來實作的。
- VM 啟動時,會有一個由主方法 main 所定義的線程。
- 可以通過建立 Thread 的執行個體來建立新的線程。
- 每個線程都是通過某個特定的 Thread 對象所對應的 run() 方法來完成這個線程要做的任務,方法 run() 成為線程體。
- 通過調用 Thread 類的 start() 方法來啟動一個線程。
- 有兩種方法建立新的線程:
-
第一種(推薦使用):定義線程類實作 Runnable 接口,然後重寫 run 方法, 然後以這個線程類建立 Thread 類,然後調用這個 Thread 類的 start() 方法,就可以開始執行這個線程,這個線程具體要執行的内容在 run 方法裡面。
public class Test {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread th = new Thread(mt);
th.start();
for(int i=0; i<100; i++) { System.out.println("Main: " + i); } }
}
class MyThread implements Runnable {
@Override
public void run() {
for(int i=0; i<100; i++) {
System.out.println("MyThread:" + i);
PS: 如果我們沒有 new一個 Thread 對象出來,而是直接使用 MyThread 的 run 方法(mt.run()),這就是方法調用,而不是啟動線程了,結果就是先後列印語句,而不是并行列印語句了。
-
第二種:可以定義一個 Thread 的子類 myThread,并且重寫 Thread 的 run 方法(Thread 也實作了Runnable 接口),然後生成子類 myThread 的對象,最後調用子類 myThread 對象的 start() 方法即可。
mt.start();
class MyThread extends Thread {for(int i=0; i<100; i++) { System.out.println("------ " + i); } }
線程的狀态轉換
線程控制基本方法
isAlive() // 判斷線程是否還活着,即線程是否還未終止
getPriority() // 獲得線程的優先級數值
setPriority() // 設定線程的優先級數值
Thread.sleep(...) // 将目前線程指定睡眠時間
join() // 将一個線程合并到某個線程上,成為一個線程執行
yield() // 讓出CPU,目前線程進入就緒隊列等待排程
wait() // 目前線程進入對象的 wait pool
notify()/notifyAll() // 喚醒對象 wait pool中的一個/所有等待線程
- sleep 方法
可以調用 Thread 的靜态方法 Thread.sleep(long ms) 使得目前線程休眠。(哪個線程調用了Thread.sleep 方法,哪個線程就 sleep)
import java.util.*;
public class Test {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread th = new Thread(mt);
th.start();
try {
Thread.sleep(10000);
} catch(InterruptedException e) {
}
th.interrupt();
}
}
class MyThread implements Runnable {
private boolean flag = true;
@Override
public void run() {
while(flag) {
System.out.println("=== " + new Date() + " ===");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
flag = false;
}
}
}
}
- join 方法
将一個線程合并到某個線程上,成為一個線程執行。(如下程式就先執行線程 th ,之後才會執行主線程 Main ,而不是并行執行。)
import java.util.*;
public class Test {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread th = new Thread(mt);
th.start();
try {
th.join();
} catch(InterruptedException e) {
}
for(int i=0; i<10; i++) {
System.out.println("Main Thread......");
}
th.interrupt();
}
}
class MyThread implements Runnable {
@Override
public void run() {
for(int i=0; i<10; i++) {
System.out.println("=== " + new Date() + " ===");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}
- yield 方法
讓出CPU,目前線程進入就緒隊列等待排程。
public class Test {
public static void main(String[] args) {
MyThread mt1 = new MyThread("mt1");
MyThread mt2 = new MyThread("mt2");
mt1.start();
mt2.start();
}
}
class MyThread extends Thread {
MyThread(String name) {
super(name);
}
@Override
public void run() {
try {
for (int i = 0; i < 50; i++) {
System.out.println(getName() + " --- " + i);
Thread.sleep(100);
if (i % 10 == 0) {
Thread.yield();
}
}
} catch (InterruptedException e) {
}
}
}
線程的優先級
- Java 提供一個線程排程器來監控程式中啟動後進入就緒狀态的所有線程。線程排程器按照線程的優先級決定應排程哪個線程來執行。
-
線程的優先級用數字表示,範圍從 1 到 10,一個線程的預設優先級是 5。
Thread.MIN_PRIORITY = 1
Thread.MAX_PRIORITY = 10
Thread.NORM_PRIORITY = 5
-
使用下面方法獲得或設定線程對象的優先級:
int getPriority();
void setPriority(int newPriority);
Java 中的線程臨界區
- 系統中每次隻允許一個線程通路的資源叫做臨界資源。
- 對臨界資源進行通路的程式代碼區域叫做臨界區。
- Java 中通過 synchronized 關鍵字和對象鎖機制對臨界區進行管理。
- Java 中的每個對象都可以作為對象鎖使用。
線程同步
我們先看這樣一個程式:
public class Test implements Runnable {
Timer time = new Timer();
public static void main(String args[]){
Test test = new Test();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
@Override
public void run() {
time.add(Thread.currentThread().getName());
}
}
class Timer {
private static int num = 0;
public void add(String name) {
num ++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
System.out.println(name + " 是第 " + num + " 個使用timer的線程");
}
}
我們兩個線程同時執行,而且調用同一個方法,相當于通路同一個共享資源 num,執行的結果為:
t2 是第 2 個使用timer的線程
t1 是第 2 個使用timer的線程
這是怎麼回事呢?按照猜想,雖然 t1 t2 線程并行執行,但是先開啟的 t1 程序,num = 1;在開啟的 t2 線程,num = 2,是以,應該是:t1 是第 1 個使用timer的線程;t2 是第 2 個使用timer的線程 才對啊?
其實,這就涉及到線程同步的問題,如果在一個線程通路一個共享對象的時候沒有給這個共享資源上鎖的話,那麼這個線程操作的共享資源可能就是錯誤的,因為可能别的程序也在通路這個共享資源。
那麼,我們就需要在程序通路這個共享資源的時候,将其上鎖,上鎖的方式有兩種:(還是以上面的程式為例:)
// 方式一
class Timer {
private static int num = 0;
public void add(String name) {
synchronized (this) {} { // 資源上鎖
num ++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
System.out.println(name + " 是第 " + num + " 個使用timer的線程");
}
}
}
synchronized (this) {
// 需要鎖定的内容
synchronized(this)表示: 鎖定目前對象。括号中的語句在一個線程執行的過程中,不會被另一個線程打斷。
// 方式二
class Timer {
private static int num = 0;
synchronized public void add(String name) { // 資源上鎖
num ++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
System.out.println(name + " 是第 " + num + " 個使用timer的線程");
}
}
在 add 函數加上 synchronized 關鍵字,就表示哪個程序調用了add 函數,那麼這個程序在執行這個方法的時候,鎖定目前對象。
- 在 Java 語言中引入了對象互斥鎖的概念,保證共享資料操作的完整性。每個對象都對應于一個可稱為“互斥鎖”的标記,這個标記保證在任意時刻,智能有一個線程通路該對象。
- 關鍵字 synchronized 來與對象的互斥鎖聯系。當某個對象 synchronized 修飾時,表明該對象在任一時刻隻能由一個線程通路。
線程死鎖
什麼是線程死鎖?
如果兩個或多個線程分别擁有不同的資源, 而同時又需要對方釋放資源才能繼續運作時,就會發生死鎖。簡單來說:死鎖就是當一個或多個程序都在等待系統資源,而資源本身又被占用時,所産生的一種狀态。
public class Test implements Runnable {
public int flag = 0;
static Object o1 = new Object();
static Object o2 = new Object();
public static void main(String args[]){
Test test1 = new Test();
Test test2 = new Test();
test1.flag = 1;
test2.flag = 2;
Thread t1 = new Thread(test1);
Thread t2 = new Thread(test2);
t1.start();
t2.start();
}
public void run() {
System.out.println("flag=" + flag);
if(1 == flag) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if(2 == flag) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("2");
}
}
}
}
}
注意:以上代碼中 Object 對象 O1 O2 一定是 static 的,否則不能得到程序死鎖。還有一定要有 Thread.sleep 語句。
舉例:
public class Test implements Runnable {
public int b = 100;
public synchronized void m1() throws Exception{
b = 1000;
Thread.sleep(3000);
System.out.println("m1:b = " + b);
}
public void m2 () {
System.out.println("m2:b = " + b);
b = 2;
System.out.println("m2:b = " + b);
}
public void run() {
try {
m1();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
Test tt = new Test();
Thread t = new Thread(tt);
t.start();
Thread.sleep(1000);
tt.m2();
System.out.println("b = " + tt.b);
}
}
提問:各個列印 b 的值為多少?
m2:b = 1000
m2:b = 2
b = 2
m1:b = 2
為什麼 b 的值可以被修改呢?
因為,如果一個方法加了 synchronized ,而且有一個程序正在通路這個方法,那麼隻能說明别的程序不可以同時通路這個方法,但是并不妨礙别的程序通路其他的方法,如果其他的方法中有對你需要保護的對象(這裡是 b)進行操作的話,也是允許的。
是以,如果要保護一個需要同步的對象的話,對通路這個對象的所有方法考慮加不加 synchronized 。因為一個方法加了鎖,隻是另一個線程不能通路加了鎖的方法,但是可以通路其他的方法,其他的方法可能修改了你需要同步的對象。synchronized 修飾的方法可以防止多個線程同時通路這個對象的synchronized方法(如果一個對象有多個synchronized方法,隻要一個線程通路了其中的一個synchronized方法,其它線程不能同時通路這個對象中任何一個synchronized方法。(是以,上面的 m2 方法也需要加 synchronized 修飾。)
生産者和消費者問題:
/**
* 這裡模拟的是生産者和消費者的問題:這裡生産者生産櫻花膏,消費者吃櫻花膏。然後
* 還有一個裝櫻花膏的籃子 basket. 我們需要做的生産者生産的櫻花膏按照順序放入籃子,
* 吃貨消費者按照順序從頂部依次拿出來吃,是以 basket 的資料結構是棧。
*
* 注意:生産者生産的櫻花膏要和吃貨需要吃的櫻花膏數量一緻,否則程式會wait
*/
public class Test {
public final int NUM = 10;
public static void main(String[] args) {
BasketStack bs = new BasketStack();
Producer p = new Producer(bs);
Consumer c = new Consumer(bs);
new Thread(p).start();
new Thread(p).start();
new Thread(p).start();
new Thread(c).start();
}
}
// 櫻花膏
class Sakura {
private int id = 0;
Sakura (int id) {
this.id = id;
}
@Override
public String toString() {
return ((Integer)id).toString();
}
}
// 籃子
class BasketStack {
int index = 0;
Sakura[] sakuras = new Sakura[6]; // 一個籃子隻能裝6個櫻花膏
public synchronized void push(Sakura sa) {
while(index == (sakuras.length - 1)) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notifyAll();
sakuras[index] = sa;
index ++;
}
public synchronized Sakura pop() {
while(index == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notifyAll();
index--;
return sakuras[index];
}
}
// 生産者
class Producer implements Runnable {
BasketStack bs = null;
Producer(BasketStack bs) {
this.bs = bs;
}
@Override
public void run() {
for(int i=0; i<10; i++) {
Sakura sa = new Sakura(i);
bs.push(sa);
System.out.println("生産者:" + sa.toString());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 吃貨
class Consumer implements Runnable {
BasketStack bs = null;
Consumer(BasketStack bs) {
this.bs = bs;
}
@Override
public void run() {
for(int i=0; i<30; i++) {
System.out.println("吃貨:" + bs.pop().toString());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
wait和sleep的差別:
1.wait之後 鎖就不歸我所有,别的線程可以通路鎖定對象;sleep後 鎖還是我的,别的線程不可以通路鎖定對象。
2.調用 wait方法的時候必須先鎖定對象,否則談何wait。
2.wait是Object的方法,sleep是Thread的方法。
總結(關鍵字聯想)
- 線程/程序的概念
- 建立和啟動線程的方式
- sleep
- join
- yield
- synchronized
- wait
- notify/notifyAll