1 線程與進程
什麼是進程?
進程指的是操作系統中運行的一個任務。是一塊包含了某些資源的內存區域,操作系統利用進程把它的工作劃分爲一些功能單元。
什麼是線程?
進程中包含的一個或多個執行單元稱爲線程。一個線程是進程的順序執行流。
線程和進程的區別與聯繫?
一個進程至少有一個線程,線程的劃分尺度小於進程,使得多線程程序的併發性高。另外,進程在執行過程中有獨立的內存單元,而多個線程共享內存,從而極大的提高了程序的執行效率。
線程的使用場合?
線程通常用於在一個程序中需要同時完成多個任務的情況。我們可以將每個任務定義爲一個線程,使它們一起工作。也可以在單一線程中完成但多線程可以更快。比如下載文件。
併發原理:
多個線程同時工作其實只是我們的感官體驗,事實上線程是併發運行的,操作系統將時間劃分爲許多的時間片段(時間片),儘可能均勻分配給每一個線程.獲取時間片的線程被cpu執行,而其他的線程則全部等待。所以微觀上是走走停停,而宏觀上都在運行。這種現象叫併發。但絕不是“同時發生”。
線程狀態:
同一時間只有一個線程處於Running狀態,其他線程處於Runnable等待狀態。
2 線程的創建
2.1 繼承Thread類
Thread類是線程類,其每一個實例表示一個可以併發運行的線程。我們可以通過繼承該類並重寫run方法來定義一個具體的線程。run方法是線程要執行的邏輯,啓動線程需要調用線程的start()方法。start()方法會將當前線程納入線程調度,使當前線程可以併發運行。當線程獲取時間片後自動執行run方法中的邏輯。
示例代碼:
/**
* 測試多線程
* @author Administrator
*
*/
public class TestThread {
public static void main(String[] args) {
Thread t1=new MyThread();
Thread t2=new MyThread();
t1.start();
t2.start();
}
}
/**
* 線程
* @author Administrator
*
*/
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(i);
}
}
}
2.2 實現Runnable接口
實現Runnable接口並重寫run方法來定義線程體。然後在創建線程的時候將Runnable實例傳入。這樣做的好處在於可以將線程與線程要執行的任務分離開,減少耦合。
代碼示例:
public class TestRunnable implements Runnable {
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(i);
}
}
public static void main(String[] args) {
TestRunnable runnable=new TestRunnable();
Thread t=new Thread(runnable);
t.start();
}
}
其實更常用的一種方法是使用匿名內部類的形式創建線程。當一個線程僅需要一個實例時我們用這種方式。
示例代碼:
public static void main(String[] args) {
//使用匿名內部類的方式創建線程
Thread t=new Thread(){
public void run(){
for(int i=0;i<10;i++){
System.out.println(i);
}
}
};
t.start();
}
3 線程操作API
Thread.currentThread方法可以用於獲取運行當前代碼片段的進程。
常用方法:
- long getId() 返回線程的標識符
- String getName() 返回當前線程的名字
- int getPriority() 返回線程的優先級
- boolean isAlive() 測試線程是否處於活動狀態
- boolean isDaemon() 測試線程是否爲守護線程
- boolean isInterrupted() 測試線程是否已經中斷
3.1 線程優先級
線程的切換是由線程調度控制的,我們無法通過代碼干涉。但我們可以通過提高優先級來最大限度改善獲取時間片的概率。線程優先級被劃分爲十級,值爲1-10.其中1最低10最高。線程提供了三個常量來表示最低,最高,和默認優先級:
- Thread.MIN_PRIORITY
- Thread.MAX_PRIORITY
- Thread.NORM_PRIORITY
void setPriority(int priority) 可通過這個方法設置優先級
3.2 守護線程(後臺線程)
守護線程與普通線程沒什麼區別,有個特點是當進程中只剩下守護線程時,所有守護線程將強制終止。
void setDaemon(boolean b)
當參數爲true時設置爲守護線程
3.3 sleep方法
Thread的靜態方法sleep用於使當前線程進入阻塞狀態:
-static void sleep(long ms)
該方法會使當前線程進入阻塞狀態指定時間,當時間過後 當前線程會重新進入Runnable狀態等待分配時間片。
3.4 yield方法
該方法用於使當前線程主動讓出cpu分配的時間片回到Runnable狀態,等待分配時間片。
join()方法用於等待當前線程結束。
示例代碼:
public class TestJoin {
public static void main(String[] args) {
//創建下載線程
final Thread download=new Thread(){
public void run(){
System.out.println("圖片開始下載");
for(int i=1;i<=10;i++){
System.out.println("圖片已下載:"+i*10+"%");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
System.out.println("圖片下載完成");
}
};
//創建顯示線程
Thread show=new Thread(){
public void run(){
System.out.println("等待圖片下載完成");
try {
download.join();//等待下載線程結束
} catch (InterruptedException e) {
}
System.out.println("顯示圖片");
}
};
download.start();
show.start();
}
}
上邊代碼創建了兩個線程,一個下載線程一個顯示線程。顯示線程需要等待下載線程下載完成才能工作,所以我們在它的run方法裏對下載線程執行run方法。下邊是執行結果
4 線程同步
4.1 synchronized關鍵字
多個線程併發讀寫同一個臨界資源時會發生線程安全問題。常見臨界資源有實例變量,靜態公共變量。下邊舉個例子來說明線程安全問題。
示例代碼:
public class SyncDemo3 implements Runnable{
private int count=5;
public void run() {
for(int i=0;i<10;++i){
if(count>0){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread()+":"+count--);
}
}
}
public static void main(String[] args) {
SyncDemo3 he=new SyncDemo3();
Thread h1=new Thread(he);
Thread h2=new Thread(he);
Thread h3=new Thread(he);
h1.start();
h2.start();
h3.start();
}
}
上邊的代碼我們讓每個線程對count進行減一操作,我們希望的情況是當count等於0時就不在輸出了。然而實際的運行結果是:
我們可以看到count甚至成了-1,爲什麼會產生這樣的情況呢,這是因爲可能線程2在運行的時候count是等於1的,然後在睡眠狀態下線程1和線程0得到了運行的機會分別對count進行了減一操作然後線程2又得到時間片繼續運行此時count已經等於-1了。這就是線程安全問題。若想解決線程安全問題,就需要將異步操作變爲同步操作。
- 異步操作:多線程併發的操作,相當於各幹各的。
- 同步操作:有先後順序的操作,相當於你幹完我再幹。
synchronized關鍵字是java中的同步鎖。它的用法是:
synchronized(鎖對象的引用){
同步代碼塊
}
若方法所有代碼都需要同步則可以直接給方法加鎖。每個java對象都可以用做一個實現同步的鎖。線程進入同步代碼塊前會自動獲得鎖,並且在退出這段代碼塊時自動釋放鎖(無論是正常退出還是拋異常退出)。其他線程要想訪問上鎖的代碼塊時必須要等待當前線程釋放鎖,這樣就變成了同步操作。使用synchronized需要對一個對象上鎖以保證線程同步,那麼這個鎖對象應該保證:多個需要同步的線程在訪問該同步塊時應該看到的是同一個對象的引用。我們通常使用this作爲鎖對象。上邊代碼加鎖後
public class SyncDemo3 implements Runnable{
private int count=5;
public void run() {
for(int i=0;i<10;++i){
//加鎖,保證線程安全
synchronized (this){
if(count>0){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread()+":"+count--);
}
}
}
}
public static void main(String[] args) {
SyncDemo3 he=new SyncDemo3();
Thread h1=new Thread(he);
Thread h2=new Thread(he);
Thread h3=new Thread(he);
h1.start();
h2.start();
h3.start();
}
}
運行結果:
可以看到當我們對上述代碼加鎖後就是同步的了。
當我們對靜態方法加鎖時鎖的是類對象。比如:
public synchronized static void xxx()
示例代碼:
public class StaticSyncDemo {
public static void main(String[] args) {
final StaticDemo sd = new StaticDemo();
final StaticDemo sd2 = new StaticDemo();
Thread t1 = new Thread(){
public void run(){
//靜態方法上鎖後,同步是跨對象的
sd.methodB();
}
};
Thread t2 = new Thread(){
public void run(){
sd2.methodB();
}
};
t1.start();
t2.start();
}
}
class StaticDemo{
public synchronized void methodA(){
String name = Thread.currentThread().getName();
System.out.println(name+"調用了methodA方法");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println(name+"調用methodA方法完畢");
}
public synchronized static void methodB(){
String name = Thread.currentThread().getName();
System.out.println(name+"調用了methodB靜態方法");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println(name+"調用methodB靜態方法完畢");
}
}
運行結果:
當一個類中的多個方法被synchronized修飾時,這些方法是互斥的。不能同時進入。如下所示:
public class SyncDemo2 {
public static void main(String[] args) {
final Demo demo = new Demo();
Thread t1 = new Thread(){
public void run(){
demo.methodA();
}
};
Thread t2 = new Thread(){
public void run(){
demo.methodB();
}
};
t1.start();
t2.start();
}
}
class Demo{
public synchronized void methodA(){
String name = Thread.currentThread().getName();
System.out.println(name+"調用了methodA方法");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name+"調用methodA方法完畢");
}
public synchronized void methodB(){
String name = Thread.currentThread().getName();
System.out.println(name+"調用了methodB方法");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name+"調用methodB方法完畢");
}
}
運行結果:
4.2 wait和notify
wait和notify是Object定義的方法,wait導致線程進入等待狀態,直到它被其他線程通過notify()或者notifyAll喚醒。該方法只能在同步方法中調用。notify隨機選擇一個在該對象上調用wait方法的線程,解除其阻塞狀態。該方法只能在同步方法或同步塊內部調用。如果當前線程不是鎖的持有者,該方法拋出一個IllegalMonitorStateException異常。示例代碼:
public class TestWaitAndNotify {
private static boolean isFinish=false;
public static void main(String[] args) {
final Object obj=new Object();
//下載圖片的線程
Thread download=new Thread(){
public void run(){
System.out.println("開始下載圖片");
for(int i=0;i<100;i++){
System.out.println("下載了%"+i);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isFinish=true;
//通知顯示線程可以開始工作了
synchronized(obj){
obj.notify();
}
}
};
//顯示圖片的線程
Thread show=new Thread(){
public void run(){
System.out.println("show:開始顯示圖片");
try {//在obj對象上等待
synchronized(obj){
/*
* wait方法要求:
* 調用哪個對象的wait方法就要將
* 該對象加鎖
*/
obj.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
if(!isFinish){
System.out.println("show:下載失敗了");
}else{
System.out.println("show:下載成功了");
}
}
};
download.start();
show.start();
}
}
上邊代碼當顯示線程啓動時,調用wait方法使obj對象進入等待阻塞狀態,當下載線程完成後調用notify方法解除了對obj對象的阻塞。使顯示線程可以繼續工作。